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

feat(#111): allow SOR codebase to be used as library #112

Merged
merged 7 commits into from
Oct 16, 2024
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Revision history for geniusyield-orderbot

## 0.2.0

Uses revamped geniusyield-orderbot-framework, strategies is moved into a signature with corresponding implementation.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,8 @@ For running the tests we can just simply execute `make orderbot-tests`.
The SOR is organized into 5 main folders:

- [`geniusyield-orderbot-framework`](./geniusyield-orderbot-framework), implement the main abstract tools for the SOR.
- [`geniusyield-orderbot`](./geniusyield-orderbot), the executable is implemented here, together with the strategies.
- [`impl`](./impl), specific implementations of the orderbook and data-provider.
- [`impl`](./impl), specific implementations of the orderbook, data-provider and strategies.
- [`geniusyield-orderbot`](./geniusyield-orderbot), simply runs the executable.

### Backpack

Expand All @@ -439,8 +439,8 @@ To get started with Backpack, please see the following example: [A really small

## Strategies

On the [`Strategies`](./geniusyield-orderbot/src/Strategies.hs) module, you can find all the strategies
implemented by the SOR. Currently, there is only one called [`OneSellToManyBuy`](./geniusyield-orderbot/src/Strategies.hs#L36C20-L36C36),
On the [`GeniusYield.OrderBot.Strategies.Impl`](./impl/strategies-impl/GeniusYield/OrderBot/Strategies/Impl.hs) module, you can find all the strategies
implemented by the SOR. Currently, there is only one called `OneSellToManyBuy`,
which basically takes the best sell order (the one with the lowest price) and searches for many buy
orders (starting from the one with the highest price), ideally buying the total amount of offered
tokens, or until it reaches the maxOrderMatches.
Expand All @@ -464,7 +464,7 @@ data BotStrategy = OneSellToManyBuy
```

We must adjust some straightforward instances with the new constructor: `FromJSON` and `Var`.
As is the case with [`mkIndependentStrategy`](./geniusyield-orderbot/src/Strategies.hs#L56-L59),
As is the case with `mkIndependentStrategy`,
adding a new particular case for `OneBuyToManySell`

```haskell
Expand All @@ -484,7 +484,7 @@ oneBuyToManySell :: Natural -> OrderBook -> [MatchResult]
oneBuyToManySell _ _ = []
```

Even more! We can add the new constructor `OneBuyToManySell` to the `allStrategies` [list](https://github.com/geniusyield/smart-order-router/blob/75aeeb733ea2c747595e2b231460601d80ed2866/geniusyield-orderbot/src/Strategies.hs#L58)
Even more! We can add the new constructor `OneBuyToManySell` to the `allStrategies` list
and this should be enough to start testing with our custom strategy by running the tests.

```haskell
Expand All @@ -497,8 +497,8 @@ Finishing the dummy implementation of `oneBuyToManySell` with the actual logic i
<details>
<summary>Hint</summary>

> Checking [`multiFill`](./geniusyield-orderbot/src/Strategies.hs#L95-L132),
can help to realize that it's enough to use [`oneSellToManyBuy`](./geniusyield-orderbot/src/Strategies.hs#L82-L92)
> Checking `multiFill`,
can help to realize that it's enough to use [`oneSellToManyBuy`]
as inspiration and "flip" something.
</details>

Expand Down
4 changes: 4 additions & 0 deletions geniusyield-orderbot-framework/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Revision history for geniusyield-orderbot-framework

## 0.5.0

Adds strategy signature, utilities to different orderbook, etc. signatures and more modules related to order bot configuration and command line parsing.

## 0.4.0

Conway era support. Note that this update is not compatible with Babbage era and so must be employed on Mainnet after Chang HF.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
cabal-version: 3.4
name: geniusyield-orderbot-framework
synopsis: Smart Order Router framework
version: 0.4.0
version: 0.5.0
build-type: Simple
license: Apache-2.0
copyright: (c) 2023 GYELD GMBH
Expand Down Expand Up @@ -136,6 +136,24 @@ library orderbook
, geniusyield-dex-api
signatures:
GeniusYield.OrderBot.OrderBook
exposed-modules:
GeniusYield.OrderBot.OrderBook.Extra

library strategies
import: common-lang
import: common-ghc-opts
visibility: public
hs-source-dirs: lib-strategies
build-depends:
, aeson
, atlas-cardano
, base
, envy
, geniusyield-orderbot-framework:common
, geniusyield-orderbot-framework:orderbook
, geniusyield-dex-api
signatures:
GeniusYield.OrderBot.Strategies

-- Indefinite library exposing the OrderBot orchestration types and functions.
library
Expand All @@ -144,12 +162,18 @@ library
import: common-ghc-opts
hs-source-dirs: src
build-depends:
, cardano-api
, envy
, geniusyield-orderbot-framework:common
, geniusyield-orderbot-framework:datasource
, geniusyield-orderbot-framework:orderbook
, geniusyield-orderbot-framework:strategies
, geniusyield-dex-api
, vector
exposed-modules:
GeniusYield.OrderBot
GeniusYield.OrderBot.MatchingStrategy
GeniusYield.OrderBot.OrderBotConfig
GeniusYield.OrderBot.Run
ghc-options:
-O2
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ module GeniusYield.OrderBot.Types
, OrderAssetPair (OAssetPair, currencyAsset, commodityAsset)
, OrderType (..)
, SOrderType (..)
, SOrderTypeI (..)
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 defined this class for writing lookupBest function inside OrderBook.Extra module.

, Volume (..)
, Price (..)
, mkOrderInfo
Expand All @@ -20,15 +21,20 @@ module GeniusYield.OrderBot.Types
, mkOrderAssetPair
, equivalentAssetPair
, mkEquivalentAssetPair
, FillType (..)
, MatchExecutionInfo (..)
, completeFill
, partialFill
Comment on lines +24 to +27
Copy link
Member Author

Choose a reason for hiding this comment

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

Not new definitions, but rather moved here from another module.

) where

import Data.Aeson (ToJSON, (.=))
import qualified Data.Aeson as Aeson
import Data.Kind (Type)
import Data.Ratio (denominator, numerator, (%))
import Data.Text (Text)
import Numeric.Natural (Natural)

import GeniusYield.Types.TxOutRef (GYTxOutRef)
import GeniusYield.Types.TxOutRef (GYTxOutRef, showTxOutRef)
import GeniusYield.Types.Value (GYAssetClass (..))

import GeniusYield.Api.Dex.PartialOrder (PartialOrderInfo (..))
Expand Down Expand Up @@ -135,13 +141,22 @@ isBuyOrder _ = False

data OrderType = BuyOrder | SellOrder deriving stock (Eq, Show)

data SOrderType t where
SBuyOrder :: SOrderType BuyOrder
SSellOrder :: SOrderType SellOrder
data SOrderType (t :: OrderType) where
SBuyOrder :: SOrderType 'BuyOrder
SSellOrder :: SOrderType 'SellOrder

deriving stock instance Eq (SOrderType t)
deriving stock instance Show (SOrderType t)

class SOrderTypeI (t :: OrderType) where
sOrderType :: SOrderType t

instance SOrderTypeI 'BuyOrder where
sOrderType = SBuyOrder

instance SOrderTypeI 'SellOrder where
sOrderType = SSellOrder

-------------------------------------------------------------------------------
-- Order components
-------------------------------------------------------------------------------
Expand Down Expand Up @@ -237,3 +252,50 @@ mkOrderType
mkOrderType asked oap
| commodityAsset oap == asked = BuyOrder
| otherwise = SellOrder

{- | "Fill" refers to the _volume_ of the order filled. Therefore, its unit is always the 'commodityAsset'.

Of course, 'CompleteFill' just means the whole order is filled, whether it's buy or sell.

'PartialFill' means slightly different things for the two order types. But the 'Natural' field within
always designates the 'commodityAsset'.

For sell orders, `PartialFill n` indicates that n amount of commodity tokens will be sold from the order,
and the respective payment will be made in the currency asset.

For buy orders, `PartialFill n` indicates that n amount of
commodity tokens should be bought, and the corresponding price (orderPrice * n), _floored_ if necessary,
must be paid by the order.

**NOTE**: The 'n' in 'PartialFill n' must not be the max volume of the order. Use 'CompleteFill' in those scenarios.
-}
data FillType = CompleteFill | PartialFill Natural deriving stock (Eq, Show)

data MatchExecutionInfo
= forall t. OrderExecutionInfo !FillType {-# UNPACK #-} !(OrderInfo t)

instance ToJSON MatchExecutionInfo where
toJSON (OrderExecutionInfo fillT OrderInfo { orderRef, orderType, assetInfo
, volume
, price = Price {getPrice = x}
}) =
Aeson.object
[ "utxoRef" .= showTxOutRef orderRef
, "volumeMin" .= volumeMin volume
, "volumeMax" .= volumeMax volume
, "price" .= x
, "commodity" .= commodityAsset assetInfo
, "currency" .= currencyAsset assetInfo
, "type" .= prettySOrderType orderType
, "fillType" .= show fillT
]
where
prettySOrderType :: SOrderType t -> Text
prettySOrderType SBuyOrder = "Buy"
prettySOrderType SSellOrder = "Sell"

completeFill :: OrderInfo t -> MatchExecutionInfo
completeFill = OrderExecutionInfo CompleteFill

partialFill :: OrderInfo t -> Natural -> MatchExecutionInfo
partialFill o n = OrderExecutionInfo (PartialFill n) o
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,20 @@ signature GeniusYield.OrderBot.OrderBook (
-- * Order book construction
populateOrderBook,
buildOrderBookList,
emptyOrders,
Copy link
Member Author

Choose a reason for hiding this comment

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

New utilities to make strategy implementation agnostic of underlying orderbook implementation.

unconsOrders,
insertOrder,
deleteOrder,
-- * Order book queries
lowestSell,
lowestSellMaybe,
highestBuy,
highestBuyMaybe,
withoutTip,
foldlOrders,
foldrOrders,
foldlMOrders,
filterOrders,
ordersLTPrice,
ordersLTEPrice,
ordersGTPrice,
Expand All @@ -41,18 +49,19 @@ signature GeniusYield.OrderBot.OrderBook (
volumeLTEPrice,
volumeGTPrice,
volumeGTEPrice,
nullOrders,
-- * MultiAssetOrderBook reading utilities
withEachAsset
) where

import Prelude (IO)
import Prelude (Bool, IO, Maybe, Monad)

import Data.Aeson (ToJSON)
import Data.Kind (Type)

import GeniusYield.OrderBot.Types ( OrderAssetPair(..)
, OrderType (BuyOrder, SellOrder)
, OrderInfo, Price, Volume
, OrderInfo, Price, Volume
)
import GeniusYield.OrderBot.DataSource ( Connection )

Expand Down Expand Up @@ -129,29 +138,50 @@ buildOrderBookList

-- Components

-- | An empty 'Orders' data structure.
emptyOrders :: Orders t

-- | If the 'Orders' data structure is empty, return 'Nothing', else return the tip and the rest.
unconsOrders :: Orders t -> Maybe (OrderInfo t, Orders t)

-- | Insert an order into the 'Orders' data structure.
insertOrder :: OrderInfo t -> Orders t -> Orders t

-- | Delete an order from the 'Orders' data structure.
deleteOrder :: OrderInfo t -> Orders t -> Orders t

buyOrders :: OrderBook -> Orders 'BuyOrder

sellOrders :: OrderBook -> Orders 'SellOrder

-- Minima & Maxima

-- | The lowest sell order in the 'Orders' data structure. Fails if the 'Orders' data structure is empty.
lowestSell :: Orders 'SellOrder -> OrderInfo 'SellOrder

-- | The lowest sell order in the 'Orders' data structure. Returns 'Nothing' if the 'Orders' data structure is empty.
lowestSellMaybe :: Orders 'SellOrder -> Maybe (OrderInfo 'SellOrder)

-- | The highest buy order in the 'Orders' data structure. Fails if the 'Orders' data structure is empty.
highestBuy :: Orders 'BuyOrder -> OrderInfo 'BuyOrder

-- | The highest buy order in the 'Orders' data structure. Returns 'Nothing' if the 'Orders' data structure is empty.
highestBuyMaybe :: Orders 'BuyOrder -> Maybe (OrderInfo 'BuyOrder)

-- Slicing

withoutTip :: Orders t -> Orders t

-- Folds

-- TODO: Document that it should be strict in accumulator.
{- | Left associative fold over the 'Orders' data structure.

The order in which each 'OrderInfo' is passed onto the function, depends on the type of
'Orders'.

For sell orders, it should act like a 'foldr' on a list with _ascending_ orders based on price.
For buy orders, it should act like a 'foldr' on a list with _descending_ orders based on price.
For sell orders, it should act like a 'foldl' on a list with _ascending_ orders based on price.
For buy orders, it should act like a 'foldl' on a list with _descending_ orders based on price.
-}
foldlOrders :: forall a t. (a -> OrderInfo t -> a) -> a -> Orders t -> a

Expand All @@ -160,11 +190,17 @@ foldlOrders :: forall a t. (a -> OrderInfo t -> a) -> a -> Orders t -> a
The order in which each 'OrderInfo' is passed onto the function, depends on the type of
'Orders'.

For sell orders, it should act like a 'foldl' on a list with _ascending_ orders based on price.
For buy orders, it should act like a 'foldl' on a list with _descending_ orders based on price.
For sell orders, it should act like a 'foldr' on a list with _ascending_ orders based on price.
For buy orders, it should act like a 'foldr' on a list with _descending_ orders based on price.
-}
foldrOrders :: forall a t. (OrderInfo t -> a -> a) -> a -> Orders t -> a

-- | @foldlM@ variant for 'Orders', you should almost always be using @foldlMOrders'@ instead.
foldlMOrders :: forall a t m. Monad m => (a -> OrderInfo t -> m a) -> a -> Orders t -> m a

-- | Filter orders based on a predicate.
filterOrders :: (OrderInfo t -> Bool) -> Orders t -> Orders t

-- Price queries

ordersLTPrice :: Price -> Orders t -> Orders t
Expand All @@ -185,6 +221,8 @@ volumeGTPrice :: Price -> Orders t -> Volume

volumeGTEPrice :: Price -> Orders t -> Volume

nullOrders :: Orders t -> Bool

-------------------------------------------------------------------------------
-- MultiAssetOrderBook reading utilities
-------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{-|
Module : GeniusYield.OrderBot.OrderBook.Extra
Synopsis : Extra utilities when working with order books.
Copyright : (c) 2023 GYELD GMBH
License : Apache 2.0
Maintainer : [email protected]
Stability : develop
-}
module GeniusYield.OrderBot.OrderBook.Extra (
foldlMOrders',
mapMOrders_,
lookupBest,
) where

import Prelude (Maybe, Monad, (*>), pure)
import GeniusYield.OrderBot.Types (OrderInfo, SOrderTypeI (..), SOrderType (..), OrderType)
import GeniusYield.OrderBot.OrderBook

-- | @foldlM'@ variant for 'Orders' which is strict in accumulator.
foldlMOrders' :: forall a t m. Monad m => (a -> OrderInfo t -> m a) -> a -> Orders t -> m a
foldlMOrders' f = foldlMOrders (\(!acc) -> f acc)

-- | @mapM_@ variant for 'Orders'.
mapMOrders_ :: forall a t m. Monad m => (OrderInfo t -> m a) -> Orders t -> m ()
mapMOrders_ f os = foldlMOrders' (\_ oi -> f oi *> pure ()) () os

-- | In case we have buy orders, return the best buy order (highest price). And in case we have sell orders, return the best sell order (lowest price).
lookupBest :: forall (t :: OrderType). SOrderTypeI t => Orders t -> Maybe (OrderInfo t)
lookupBest os = case (sOrderType @t) of
SBuyOrder -> highestBuyMaybe os
SSellOrder -> lowestSellMaybe os
Loading
Loading