Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lh-api #4

Merged
merged 22 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vscode/
80 changes: 80 additions & 0 deletions data/redisrepo/order_iter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package redisrepo

import (
"context"

"github.com/google/uuid"
"github.com/orbs-network/order-book/models"
"github.com/orbs-network/order-book/utils/logger"
"github.com/orbs-network/order-book/utils/logger/logctx"
)

// ////////////////////////////////////////////////
type OrderIter struct {
index int
ids []string
redis *redisRepository
}

func (i *OrderIter) Next(ctx context.Context) *models.Order {
if i.index >= len(i.ids) {
logctx.Error(ctx, "Error iterator reached last element")
return nil
}

// increment index
i.index = i.index + 1
// get order
orderId, err := uuid.Parse(i.ids[i.index])
if err != nil {
logctx.Error(ctx, "Error parsing bid order id", logger.Error(err))
return nil
}
order, err := i.redis.FindOrderById(ctx, orderId)
if err != nil {
logctx.Error(ctx, "Error fetching order", logger.Error(err))
return nil
}

return order
}

func (i *OrderIter) HasNext() bool {
return i.index < (len(i.ids) - 1)
}

// ////////////////////////////////////////////////
func (r *redisRepository) GetMinAsk(ctx context.Context, symbol models.Symbol) models.OrderIter {
key := CreateSellSidePricesKey(symbol)

// Min ask price for selling
orderIDs, err := r.client.ZRange(ctx, key, 0, -1).Result()
if err != nil {
logctx.Error(ctx, "Error fetching asks", logger.Error(err))
}
// create order iter
return &OrderIter{
index: -1,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why initialise this with -1? Better to start with 0 so it aligns with how we usually think about array indexes?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i like the use of -1 as in "not been used" rather than 0 which is more implicit in case of only one order in the array

ids: orderIDs,
redis: r,
}

}

// ////////////////////////////////////////////////
func (r *redisRepository) GetMaxBid(ctx context.Context, symbol models.Symbol) models.OrderIter {
key := CreateBuySidePricesKey(symbol)

// Min ask price for selling
orderIDs, err := r.client.ZRevRange(ctx, key, 0, -1).Result()

if err != nil {
logctx.Error(ctx, "Error fetching bids", logger.Error(err))
}
// create order iter
return &OrderIter{
index: -1,
ids: orderIDs,
redis: r,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you are using this redis property anywhere? Can it be removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will remove it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taking it back, ofcourse it is used in t he redis impl

}
}
12 changes: 12 additions & 0 deletions data/redisrepo/store_auction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package redisrepo

import (
"context"

"github.com/orbs-network/order-book/models"
)

func (r *redisRepository) StoreAuction(ctx context.Context, auctionID string, fillOrders []models.FilledOrder) error {
// auctionId:<ID>: [{orderID: <ID>, filledAmount: <amount>}, ...}]
panic("not implemented")
}
21 changes: 21 additions & 0 deletions mocks/order_iter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package mocks

import (
"context"

"github.com/orbs-network/order-book/models"
)

type OrderIter struct {
Error error
Order *models.Order
ShouldHaveNext bool
}

func (o *OrderIter) HasNext() bool {
return o.ShouldHaveNext
}

func (o *OrderIter) Next(ctx context.Context) *models.Order {
return o.Order
}
13 changes: 13 additions & 0 deletions mocks/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type MockOrderBookStore struct {
Order *models.Order
Orders []models.Order
MarketDepth models.MarketDepth
OrderIter models.OrderIter
}

func (m *MockOrderBookStore) StoreOrder(ctx context.Context, order models.Order) error {
Expand Down Expand Up @@ -51,3 +52,15 @@ func (m *MockOrderBookStore) GetMarketDepth(ctx context.Context, symbol models.S
}
return m.MarketDepth, nil
}

func (m *MockOrderBookStore) StoreAuction(ctx context.Context, auctionID string, fillOrders []models.FilledOrder) error {
return m.Error
}

func (m *MockOrderBookStore) GetMinAsk(ctx context.Context, symbol models.Symbol) models.OrderIter {
return m.OrderIter
}

func (m *MockOrderBookStore) GetMaxBid(ctx context.Context, symbol models.Symbol) models.OrderIter {
return m.OrderIter
}
5 changes: 5 additions & 0 deletions mocks/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type MockOrderBookService struct {
Order *models.Order
Orders []models.Order
MarketDepth models.MarketDepth
AmountOut models.AmountOut
}

func (m *MockOrderBookService) ProcessOrder(ctx context.Context, input service.ProcessOrderInput) (models.Order, error) {
Expand All @@ -36,3 +37,7 @@ func (m *MockOrderBookService) GetOrderById(ctx context.Context, orderId uuid.UU
func (m *MockOrderBookService) GetMarketDepth(ctx context.Context, symbol models.Symbol, depth int) (models.MarketDepth, error) {
return m.MarketDepth, m.Error
}

func (m *MockOrderBookService) GetAmountOut(ctx context.Context, auctionId string, symbol models.Symbol, side models.Side, amountIn decimal.Decimal) (models.AmountOut, error) {
return m.AmountOut, m.Error
}
1 change: 1 addition & 0 deletions models/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ var ErrNoUserInContext = errors.New("no user in context")
var ErrUnauthorized = errors.New("user not allowed to perform this action")
var ErrOrderNotOpen = errors.New("order must be status open to perform this action")
var ErrTransactionFailed = errors.New("transaction failed")
var ErrInsufficientLiquity = errors.New("not enough liquidity in book to satisfy amountIn")
16 changes: 16 additions & 0 deletions models/liquidity_hub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package models

import (
"github.com/google/uuid"
"github.com/shopspring/decimal"
)

type AmountOut struct {
AmountOut decimal.Decimal
FillOrders []FilledOrder
}

type FilledOrder struct {
OrderId uuid.UUID
Amount decimal.Decimal
}
8 changes: 8 additions & 0 deletions models/order_iter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package models

import "context"

type OrderIter interface {
HasNext() bool
Next(ctx context.Context) *Order
}
3 changes: 3 additions & 0 deletions models/symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type Symbol string
const (
BTC_ETH Symbol = "BTC-ETH"
USDC_ETH Symbol = "USDC-ETH"
ETH_USD Symbol = "ETH-USD"
)

var ErrInvalidSymbol = errors.New("invalid symbol")
Expand All @@ -17,6 +18,8 @@ func StrToSymbol(s string) (Symbol, error) {
return BTC_ETH, nil
case "USDC-ETH":
return USDC_ETH, nil
case "ETH-USD":
return ETH_USD, nil
// TODO: add more symbols
default:
return "", ErrInvalidSymbol
Expand Down
109 changes: 109 additions & 0 deletions service/amount_out.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package service

import (
"context"
"fmt"

"github.com/orbs-network/order-book/models"
"github.com/orbs-network/order-book/utils/logger"
"github.com/orbs-network/order-book/utils/logger/logctx"
"github.com/shopspring/decimal"
)

// orderID->amount bought or sold in A token always

func (s *Service) GetAmountOut(ctx context.Context, auctionId string, symbol models.Symbol, side models.Side, amountIn decimal.Decimal) (models.AmountOut, error) {

var it models.OrderIter
var res models.AmountOut
var err error
if side == models.BUY {
it = s.orderBookStore.GetMinAsk(ctx, symbol)
res, err = getAmountOutInAToken(ctx, it, amountIn)

} else { // SELL
it = s.orderBookStore.GetMaxBid(ctx, symbol)
res, err = getAmountOutInBToken(ctx, it, amountIn)
}
if err != nil {
logctx.Error(ctx, "getAmountOutIn failed", logger.Error(err))
return models.AmountOut{}, err
}
err = s.orderBookStore.StoreAuction(ctx, auctionId, res.FillOrders)
if err != nil {
logctx.Error(ctx, "StoreAuction failed", logger.Error(err))
return models.AmountOut{}, err
}
return res, nil
}

// PAIR/SYMBOL A-B (ETH-USDC)
// amount in B token (USD)
// amount out A token (ETH)
func getAmountOutInAToken(ctx context.Context, it models.OrderIter, amountInB decimal.Decimal) (models.AmountOut, error) {
amountOutA := decimal.NewFromInt(0)
var fillOrders []models.FilledOrder
var order *models.Order
for it.HasNext() && amountInB.IsPositive() {
order = it.Next(ctx)
// max Spend in B token for this order
orderSizeB := order.Price.Mul(order.Size)
// spend the min of orderSizeB/amountInB
spendB := decimal.Min(orderSizeB, amountInB)

// Gain
gainA := spendB.Div(order.Price)

// sub-add
amountInB = amountInB.Sub(spendB)
logctx.Info(ctx, "StoreAuction failed")
amountOutA = amountOutA.Add(gainA)

// res
logctx.Info(ctx, fmt.Sprintf("append FilledOrder gainA: %s", gainA.String()))
logctx.Info(ctx, fmt.Sprintf("append FilledOrder spendB: %s", spendB.String()))
fillOrders = append(fillOrders, models.FilledOrder{OrderId: order.Id, Amount: gainA})
}
// not all is Spent - error
if amountInB.IsPositive() {
logctx.Warn(ctx, models.ErrInsufficientLiquity.Error())
return models.AmountOut{}, models.ErrInsufficientLiquity
}
logctx.Info(ctx, fmt.Sprintf("append FilledOrder amountOutA: %s", amountOutA.String()))
return models.AmountOut{AmountOut: amountOutA, FillOrders: fillOrders}, nil
}

// PAIR/SYMBOL A-B (ETH-USDC)
// amount in A token (ETH)
// amount out B token (USD)
func getAmountOutInBToken(ctx context.Context, it models.OrderIter, amountInA decimal.Decimal) (models.AmountOut, error) {
amountOutB := decimal.NewFromInt(0)
var order *models.Order
var fillOrders []models.FilledOrder
for it.HasNext() && amountInA.IsPositive() {
order = it.Next(ctx)

// Spend
spendA := decimal.Min(order.Size, amountInA)
fmt.Println("sizeA ", spendA.String())

// Gain
gainB := order.Price.Mul(spendA)
fmt.Println("gainB ", gainB.String())

// sub-add
amountInA = amountInA.Sub(spendA)
amountOutB = amountOutB.Add(gainB)

// res
logctx.Info(ctx, fmt.Sprintf("append FilledOrder spendA: %s", spendA.String()))
logctx.Info(ctx, fmt.Sprintf("append FilledOrder gainB: %s", gainB.String()))
fillOrders = append(fillOrders, models.FilledOrder{OrderId: order.Id, Amount: spendA})
}
if amountInA.IsPositive() {
logctx.Warn(ctx, models.ErrInsufficientLiquity.Error())
return models.AmountOut{}, models.ErrInsufficientLiquity
}
logctx.Info(ctx, fmt.Sprintf("append FilledOrder amountOutB: %s", amountOutB.String()))
return models.AmountOut{AmountOut: amountOutB, FillOrders: fillOrders}, nil
}
Loading
Loading