Skip to content

Commit

Permalink
lh-api (#4)
Browse files Browse the repository at this point in the history
* skeleton

* Add Redis repository for storing order book (#5)

Redis repo, first CRUD operations

* after merge

* fix proto

* before merge

* Co-authored-by: Luke Rogerson <[email protected]>

* amountOut both sides redis

* gitignore

* store orders

* lh side

* amountOut re-factor

Co-authored-by: Luke Rogerson <[email protected]>

* tests for amount out

* fix iter state

* lint

* remove cmd lh

* rename

* rename lh

* add ctx

* ctx in tests

* add logs better error

---------

Co-authored-by: Luke Rogerson <[email protected]>
  • Loading branch information
uv-orbs and Luke-Rogerson authored Oct 24, 2023
1 parent 9976946 commit 79f2376
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 1 deletion.
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,
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,
}
}
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

0 comments on commit 79f2376

Please sign in to comment.