diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..08d5211 --- /dev/null +++ b/CHANGELOG.md @@ -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. \ No newline at end of file diff --git a/README.md b/README.md index 66d1528..1242dfe 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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 @@ -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 @@ -497,8 +497,8 @@ Finishing the dummy implementation of `oneBuyToManySell` with the actual logic i
Hint -> 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.
diff --git a/geniusyield-orderbot-framework/CHANGELOG.md b/geniusyield-orderbot-framework/CHANGELOG.md index 078f451..b3b3bae 100644 --- a/geniusyield-orderbot-framework/CHANGELOG.md +++ b/geniusyield-orderbot-framework/CHANGELOG.md @@ -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. diff --git a/geniusyield-orderbot-framework/geniusyield-orderbot-framework.cabal b/geniusyield-orderbot-framework/geniusyield-orderbot-framework.cabal index e986ba1..1850d00 100644 --- a/geniusyield-orderbot-framework/geniusyield-orderbot-framework.cabal +++ b/geniusyield-orderbot-framework/geniusyield-orderbot-framework.cabal @@ -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 @@ -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 @@ -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 diff --git a/geniusyield-orderbot-framework/lib-common/GeniusYield/OrderBot/Types.hs b/geniusyield-orderbot-framework/lib-common/GeniusYield/OrderBot/Types.hs index f333e7a..9639baa 100644 --- a/geniusyield-orderbot-framework/lib-common/GeniusYield/OrderBot/Types.hs +++ b/geniusyield-orderbot-framework/lib-common/GeniusYield/OrderBot/Types.hs @@ -12,6 +12,7 @@ module GeniusYield.OrderBot.Types , OrderAssetPair (OAssetPair, currencyAsset, commodityAsset) , OrderType (..) , SOrderType (..) + , SOrderTypeI (..) , Volume (..) , Price (..) , mkOrderInfo @@ -20,15 +21,20 @@ module GeniusYield.OrderBot.Types , mkOrderAssetPair , equivalentAssetPair , mkEquivalentAssetPair + , FillType (..) + , MatchExecutionInfo (..) + , completeFill + , partialFill ) 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 (..)) @@ -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 ------------------------------------------------------------------------------- @@ -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 diff --git a/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook.hsig b/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook.hsig index 5fe3ad4..fbcff0c 100644 --- a/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook.hsig +++ b/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook.hsig @@ -27,12 +27,20 @@ signature GeniusYield.OrderBot.OrderBook ( -- * Order book construction populateOrderBook, buildOrderBookList, + emptyOrders, + unconsOrders, + insertOrder, + deleteOrder, -- * Order book queries lowestSell, + lowestSellMaybe, highestBuy, + highestBuyMaybe, withoutTip, foldlOrders, foldrOrders, + foldlMOrders, + filterOrders, ordersLTPrice, ordersLTEPrice, ordersGTPrice, @@ -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 ) @@ -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 @@ -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 @@ -185,6 +221,8 @@ volumeGTPrice :: Price -> Orders t -> Volume volumeGTEPrice :: Price -> Orders t -> Volume +nullOrders :: Orders t -> Bool + ------------------------------------------------------------------------------- -- MultiAssetOrderBook reading utilities ------------------------------------------------------------------------------- diff --git a/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook/Extra.hs b/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook/Extra.hs new file mode 100644 index 0000000..9f70b75 --- /dev/null +++ b/geniusyield-orderbot-framework/lib-orderbook/GeniusYield/OrderBot/OrderBook/Extra.hs @@ -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 : support@geniusyield.co +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 \ No newline at end of file diff --git a/geniusyield-orderbot-framework/lib-strategies/GeniusYield/OrderBot/Strategies.hsig b/geniusyield-orderbot-framework/lib-strategies/GeniusYield/OrderBot/Strategies.hsig new file mode 100644 index 0000000..27a37a2 --- /dev/null +++ b/geniusyield-orderbot-framework/lib-strategies/GeniusYield/OrderBot/Strategies.hsig @@ -0,0 +1,58 @@ +{-| +Module : GeniusYield.OrderBot.Strategies +Synopsis : The strategies for matching orders on a DEX. +Copyright : (c) 2023 GYELD GMBH +License : Apache 2.0 +Maintainer : support@geniusyield.co +Stability : develop + +-} +signature GeniusYield.OrderBot.Strategies ( + BotStrategy, + allStrategies, + MatchResult, + IndependentStrategy, + mkIndependentStrategy, +) where + +import Data.Aeson (ToJSON, FromJSON) +import System.Envy (Var) +import GeniusYield.OrderBot.Types (OrderAssetPair, MatchExecutionInfo) +import GeniusYield.OrderBot.OrderBook (OrderBook) +import GHC.Natural (Natural) + +-- | Every bot strategy must be named here. +data BotStrategy + +instance ToJSON BotStrategy +instance FromJSON BotStrategy +instance Var BotStrategy +instance Show BotStrategy +instance Eq BotStrategy + +{- | A list containing all implemented strategies. This list is used for the + tests and for the error message during env variable parsing. +-} +allStrategies :: [BotStrategy] + +{- | The result of order matching - should contain information to perform execute order and LP transactions. + +Essentially, all orders (and pool swaps) in a list of 'MatchExecutionInfo's are matched with each other. + +All of their tokens are put into one big transaction bucket, which is then auto balanced to pay each other. +Any extra tokens are returned to the bot wallet - this is known as arbitrage profit. +-} +type MatchResult = [MatchExecutionInfo] + +{- | A matching strategy has access to the 'OrderBook' for a single asset pair, +alongside all its relevant query functions. It must produce a 'MatchResult' which +has information on how to execute the order matching transaction. +-} +type IndependentStrategy = (OrderAssetPair -> OrderBook -> [MatchResult]) + +{- | Given a bot strategy and a max amount of orders per transaction, creates + an independent strategy. +-} +mkIndependentStrategy :: BotStrategy -> Natural -> IndependentStrategy + + diff --git a/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/MatchingStrategy.hs b/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/MatchingStrategy.hs index c53eece..4e2c03a 100644 --- a/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/MatchingStrategy.hs +++ b/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/MatchingStrategy.hs @@ -17,84 +17,18 @@ module GeniusYield.OrderBot.MatchingStrategy , matchExecutionInfoUtxoRef ) where -import Data.Aeson (ToJSON (toJSON), (.=)) -import qualified Data.Aeson as Aeson import Data.Maybe (fromJust) -import Data.Text (Text) -import Numeric.Natural (Natural) - import GeniusYield.Api.Dex.PartialOrder (PORefs, PartialOrderInfo (poiOfferedAmount), fillMultiplePartialOrders') import GeniusYield.Api.Dex.Types (GYDexApiMonad) -import GeniusYield.OrderBot.OrderBook (OrderBook) +import GeniusYield.OrderBot.Strategies (IndependentStrategy, + MatchResult) import GeniusYield.OrderBot.Types import GeniusYield.TxBuilder (GYTxSkeleton) import GeniusYield.Types.PlutusVersion (PlutusVersion (PlutusV2)) -import GeniusYield.Types.TxOutRef (GYTxOutRef, showTxOutRef) - -{- | A matching strategy has access to the 'OrderBook' for a single asset pair, -alongside all its relevant query functions. It must produce a 'MatchResult' which -has information on how to execute the order matching transaction. --} -type IndependentStrategy = (OrderAssetPair -> OrderBook -> [MatchResult]) - -data MatchExecutionInfo - = forall t. OrderExecutionInfo !FillType {-# UNPACK #-} !(OrderInfo t) - --- Smart Constructors -completeFill :: OrderInfo t -> MatchExecutionInfo -completeFill = OrderExecutionInfo CompleteFill - -partialFill :: OrderInfo t -> Natural -> MatchExecutionInfo -partialFill o n = OrderExecutionInfo (PartialFill n) o - -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" - -{- | The result of order matching - should contain information to perform execute order and LP transactions. - -Essentially, all orders (and pool swaps) in a list of 'MatchExecutionInfo's are matched with each other. +import GeniusYield.Types.TxOutRef (GYTxOutRef) -All of their tokens are put into one big transaction bucket, which is then auto balanced to pay each other. -Any extra tokens are returned to the bot wallet - this is known as arbitrage profit. --} -type MatchResult = [MatchExecutionInfo] - -{- | "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) executionSkeleton :: GYDexApiMonad m a diff --git a/geniusyield-orderbot/src/OrderBotConfig.hs b/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/OrderBotConfig.hs similarity index 97% rename from geniusyield-orderbot/src/OrderBotConfig.hs rename to geniusyield-orderbot-framework/src/GeniusYield/OrderBot/OrderBotConfig.hs index b7b4abd..fafc5c0 100644 --- a/geniusyield-orderbot/src/OrderBotConfig.hs +++ b/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/OrderBotConfig.hs @@ -1,12 +1,12 @@ {-| -Module : OrderBotConfig +Module : GeniusYield.OrderBot.OrderBotConfig Copyright : (c) 2023 GYELD GMBH License : Apache 2.0 Maintainer : support@geniusyield.co Stability : develop -} -module OrderBotConfig where +module GeniusYield.OrderBot.OrderBotConfig where import Control.Exception ( throwIO ) import Control.Monad ( (<=<) ) @@ -41,7 +41,7 @@ import Cardano.Api ( AsType (AsSigningKey, AsPaymentKey) , deserialiseFromTextEnvelope ) -import Strategies ( BotStrategy(..), allStrategies, mkIndependentStrategy ) +import GeniusYield.OrderBot.Strategies ( BotStrategy, allStrategies, mkIndependentStrategy ) -- | Order bot vanilla config. data OrderBotConfig = diff --git a/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/Run.hs b/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/Run.hs new file mode 100644 index 0000000..61ff445 --- /dev/null +++ b/geniusyield-orderbot-framework/src/GeniusYield/OrderBot/Run.hs @@ -0,0 +1,49 @@ +{-| +Module : GeniusYield.OrderBot.Run +Copyright : (c) 2023 GYELD GMBH +License : Apache 2.0 +Maintainer : support@geniusyield.co +Stability : develop + +-} +module GeniusYield.OrderBot.Run ( run ) where + +import Control.Exception (throwIO) +import GeniusYield.Api.Dex.Constants (dexInfoDefaultMainnet, + dexInfoDefaultPreprod) +import GeniusYield.GYConfig +import GeniusYield.OrderBot (runOrderBot) +import GeniusYield.Types (GYNetworkId (..)) +import GeniusYield.OrderBot.OrderBotConfig (buildOrderBot, readBotConfig) +import System.Environment (getArgs) + +parseArgs :: IO (String, FilePath, Maybe FilePath) +parseArgs = do + args <- getArgs + case args of + [action, providerConfigFile, botConfigFile] -> return ( action + , providerConfigFile + , Just botConfigFile + ) + [action, providerConfigFile] -> return (action, providerConfigFile, Nothing) + _ -> throwIO . userError $ unlines + [ "Expected two or three command line arguments, in order:" + , "\t1. Action to execute: 'run'" + , "\t2. Path to the Atlas provider configuration file" + , "\t3. Path to the OrderBot config-file (only when reading config from file)" + ] + +run :: IO () +run = do + (action, pConfFile,obConfFile) <- parseArgs + obc <- readBotConfig obConfFile + cfg <- coreConfigIO pConfFile + di <- + case cfgNetworkId cfg of + GYTestnetPreprod -> pure dexInfoDefaultPreprod + GYMainnet -> pure dexInfoDefaultMainnet + _ -> throwIO $ userError "Only Preprod and Mainnet are supported." + ob <- buildOrderBot obc + case action of + "run" -> runOrderBot cfg di ob + _ -> throwIO . userError $ unwords ["Action: ", show action, " not supported."] diff --git a/geniusyield-orderbot.cabal b/geniusyield-orderbot.cabal index deedf0c..b0517bb 100644 --- a/geniusyield-orderbot.cabal +++ b/geniusyield-orderbot.cabal @@ -1,6 +1,6 @@ cabal-version: 3.4 name: geniusyield-orderbot -version: 0.1.0.0 +version: 0.2.0 synopsis: Smart Order Router description: Open-source Smart Order Router framework to connect liquidity from the GeniusYield DEX to empowers users to deploy their own arbitrage @@ -16,6 +16,7 @@ category: Blockchain, Cardano, Framework homepage: https://github.com/geniusyield/smart-order-router#readme bug-reports: https://github.com/geniusyield/smart-order-router/issues extra-source-files: README.md +extra-doc-files: CHANGELOG.md -- Common sections @@ -111,6 +112,7 @@ library datasource-providers , geniusyield-orderbot-framework:common , geniusyield-dex-api exposed-modules: GeniusYield.OrderBot.DataSource.Providers + visibility: public library orderbook-list import: common-lang @@ -123,31 +125,21 @@ library orderbook-list , geniusyield-dex-api exposed-modules: GeniusYield.OrderBot.OrderBook.List + visibility: public -library geniusyield-strategies - import: common-lang - import: common-deps - import: common-ghc-opts - hs-source-dirs: geniusyield-orderbot/src +library strategies-impl + import: common-lang + import: common-deps + import: common-ghc-opts + hs-source-dirs: impl/strategies-impl build-depends: - , geniusyield-orderbot-framework + , envy , geniusyield-orderbot-framework:common - , geniusyield-orderbot:datasource-providers - , geniusyield-orderbot:orderbook-list + , geniusyield-orderbot-framework:orderbook , geniusyield-dex-api - , envy - , cardano-api - mixins: - , geniusyield-orderbot:orderbook-list requires - ( GeniusYield.OrderBot.DataSource as GeniusYield.OrderBot.DataSource.Providers ) - , geniusyield-orderbot-framework requires - ( GeniusYield.OrderBot.DataSource as GeniusYield.OrderBot.DataSource.Providers - , GeniusYield.OrderBot.OrderBook as GeniusYield.OrderBot.OrderBook.List - ) exposed-modules: - Strategies - ghc-options: - -O2 + GeniusYield.OrderBot.Strategies.Impl + visibility: public -- The primary orderbot executable - this must be instantiated with the signature -- implementations. @@ -158,9 +150,6 @@ executable geniusyield-orderbot-exe import: common-ghc-opts hs-source-dirs: geniusyield-orderbot/src main-is: Main.hs - other-modules: - OrderBotConfig - Strategies build-depends: , cardano-api , envy @@ -168,7 +157,7 @@ executable geniusyield-orderbot-exe , geniusyield-orderbot-framework:common , geniusyield-orderbot:datasource-providers , geniusyield-orderbot:orderbook-list - , geniusyield-orderbot:geniusyield-strategies + , geniusyield-orderbot:strategies-impl , geniusyield-dex-api , plutus-ledger-api , ply-core @@ -178,6 +167,11 @@ executable geniusyield-orderbot-exe , geniusyield-orderbot-framework requires ( GeniusYield.OrderBot.DataSource as GeniusYield.OrderBot.DataSource.Providers , GeniusYield.OrderBot.OrderBook as GeniusYield.OrderBot.OrderBook.List + , GeniusYield.OrderBot.Strategies as GeniusYield.OrderBot.Strategies.Impl + ) + , geniusyield-orderbot:strategies-impl requires + ( GeniusYield.OrderBot.OrderBook as GeniusYield.OrderBot.OrderBook.List + , GeniusYield.OrderBot.DataSource as GeniusYield.OrderBot.DataSource.Providers ) ghc-options: -O2 -threaded -rtsopts -with-rtsopts=-N @@ -200,7 +194,7 @@ test-suite strategies-tests , geniusyield-orderbot-framework:common , geniusyield-orderbot:datasource-providers , geniusyield-orderbot:orderbook-list - , geniusyield-orderbot:geniusyield-strategies + , geniusyield-orderbot:strategies-impl , geniusyield-dex-api , QuickCheck , tasty @@ -211,4 +205,9 @@ test-suite strategies-tests , geniusyield-orderbot-framework requires ( GeniusYield.OrderBot.DataSource as GeniusYield.OrderBot.DataSource.Providers , GeniusYield.OrderBot.OrderBook as GeniusYield.OrderBot.OrderBook.List + , GeniusYield.OrderBot.Strategies as GeniusYield.OrderBot.Strategies.Impl + ) + , geniusyield-orderbot:strategies-impl requires + ( GeniusYield.OrderBot.OrderBook as GeniusYield.OrderBot.OrderBook.List + , GeniusYield.OrderBot.DataSource as GeniusYield.OrderBot.DataSource.Providers ) diff --git a/geniusyield-orderbot/src/Main.hs b/geniusyield-orderbot/src/Main.hs index 91b04b2..92a178d 100644 --- a/geniusyield-orderbot/src/Main.hs +++ b/geniusyield-orderbot/src/Main.hs @@ -8,42 +8,8 @@ Stability : develop -} module Main ( main ) where -import Control.Exception (throwIO) -import GeniusYield.Api.Dex.Constants (dexInfoDefaultMainnet, - dexInfoDefaultPreprod) -import GeniusYield.GYConfig -import GeniusYield.OrderBot (runOrderBot) -import GeniusYield.Types (GYNetworkId (..)) -import OrderBotConfig (buildOrderBot, readBotConfig) -import System.Environment (getArgs) +import GeniusYield.OrderBot.Run (run) -parseArgs :: IO (String, FilePath, Maybe FilePath) -parseArgs = do - args <- getArgs - case args of - [action, providerConfigFile, botConfigFile] -> return ( action - , providerConfigFile - , Just botConfigFile - ) - [action, providerConfigFile] -> return (action, providerConfigFile, Nothing) - _ -> throwIO . userError $ unlines - [ "Expected two or three command line arguments, in order:" - , "\t1. Action to execute: 'run'" - , "\t2. Path to the Atlas provider configuration file" - , "\t3. Path to the OrderBot config-file (only when reading config from file)" - ] main :: IO () -main = do - (action, pConfFile,obConfFile) <- parseArgs - obc <- readBotConfig obConfFile - cfg <- coreConfigIO pConfFile - di <- - case cfgNetworkId cfg of - GYTestnetPreprod -> pure dexInfoDefaultPreprod - GYMainnet -> pure dexInfoDefaultMainnet - _ -> throwIO $ userError "Only Preprod and Mainnet are supported." - ob <- buildOrderBot obc - case action of - "run" -> runOrderBot cfg di ob - _ -> throwIO . userError $ unwords ["Action: ", show action, " not supported."] +main = run diff --git a/geniusyield-orderbot/test/Main.hs b/geniusyield-orderbot/test/Main.hs index 574388c..1808fbe 100644 --- a/geniusyield-orderbot/test/Main.hs +++ b/geniusyield-orderbot/test/Main.hs @@ -2,8 +2,7 @@ module Main where import Test.Tasty (defaultMain, testGroup, TestTree) import Test.Tasty.QuickCheck (testProperty) - -import Strategies +import GeniusYield.OrderBot.Strategies.Impl import Tests.Prop.Strategies import Tests.Prop.Orderbook diff --git a/impl/orderbook-list/GeniusYield/OrderBot/OrderBook/List.hs b/impl/orderbook-list/GeniusYield/OrderBot/OrderBook/List.hs index c09e4a1..b08efd4 100644 --- a/impl/orderbook-list/GeniusYield/OrderBot/OrderBook/List.hs +++ b/impl/orderbook-list/GeniusYield/OrderBot/OrderBook/List.hs @@ -19,13 +19,21 @@ module GeniusYield.OrderBot.OrderBook.List ( -- * Order book construction populateOrderBook, buildOrderBookList, + emptyOrders, + unconsOrders, + insertOrder, + deleteOrder, -- * Order book queries lowestSell, + lowestSellMaybe, highestBuy, + highestBuyMaybe, withoutTip, foldlOrders, foldrOrders, + foldlMOrders, + filterOrders, ordersLTPrice, ordersLTEPrice, ordersGTPrice, @@ -34,18 +42,19 @@ module GeniusYield.OrderBot.OrderBook.List ( volumeLTEPrice, volumeGTPrice, volumeGTEPrice, - + nullOrders, -- * MultiAssetOrderBook reading utilities withEachAsset, ) where import Data.Aeson (ToJSON, object, toJSON) -import Data.Foldable (foldl') -import Data.List (sortOn) +import Data.Foldable (foldl', foldlM) +import Data.List (delete, insertBy, sortOn) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Ord (Down (Down)) +import Data.Maybe (listToMaybe) import GeniusYield.Api.Dex.Constants (DEXInfo) import GeniusYield.OrderBot.DataSource (Connection, withEachAssetOrders) @@ -96,21 +105,52 @@ buildOrderBookList acc (# oap, buyOrders, sellOrders #) = (oap, OrderBook (Orders $ sortOn price sellOrders) (Orders $ sortOn (Down . price) buyOrders)) : acc +emptyOrders :: Orders t +emptyOrders = Orders [] + +unconsOrders :: Orders t -> Maybe (OrderInfo t, Orders t) +unconsOrders (Orders []) = Nothing +unconsOrders (Orders (x : xs)) = Just (x, Orders xs) + +insertOrder :: OrderInfo t -> Orders t -> Orders t +insertOrder oi (Orders os) = Orders $ + case orderType oi of + SBuyOrder -> insertBy (\oadd opresent -> compare (price opresent) (price oadd)) oi os + SSellOrder -> insertBy (\oadd opresent -> compare (price oadd) (price opresent)) oi os + +deleteOrder :: OrderInfo t -> Orders t -> Orders t +deleteOrder oi (Orders os) = Orders $ delete oi os + lowestSell :: Orders 'SellOrder -> OrderInfo 'SellOrder lowestSell = head . unOrders +lowestSellMaybe :: Orders 'SellOrder -> Maybe (OrderInfo 'SellOrder) +lowestSellMaybe = listToMaybe . unOrders + highestBuy :: Orders 'BuyOrder -> OrderInfo 'BuyOrder highestBuy = head . unOrders +highestBuyMaybe :: Orders 'BuyOrder -> Maybe (OrderInfo 'BuyOrder) +highestBuyMaybe = listToMaybe . unOrders + withoutTip :: Orders t -> Orders t withoutTip = Orders . drop 1 . unOrders foldlOrders :: forall a t. (a -> OrderInfo t -> a) -> a -> Orders t -> a foldlOrders f e = foldl' f e . unOrders +foldlMOrders :: forall a t m. Monad m => (a -> OrderInfo t -> m a) -> a -> Orders t -> m a +foldlMOrders f e = foldlM f e . unOrders + foldrOrders :: forall a t. (OrderInfo t -> a -> a) -> a -> Orders t -> a foldrOrders f e = foldr f e . unOrders +filterOrders :: (OrderInfo t -> Bool) -> Orders t -> Orders t +filterOrders f = Orders . filter f . unOrders + +nullOrders :: Orders t -> Bool +nullOrders = null . unOrders + ordersLTPrice :: Price -> Orders t -> Orders t ordersLTPrice maxPrice = Orders . filter (\oi -> price oi < maxPrice) . unOrders diff --git a/geniusyield-orderbot/src/Strategies.hs b/impl/strategies-impl/GeniusYield/OrderBot/Strategies/Impl.hs similarity index 53% rename from geniusyield-orderbot/src/Strategies.hs rename to impl/strategies-impl/GeniusYield/OrderBot/Strategies/Impl.hs index e880ae0..add4880 100644 --- a/geniusyield-orderbot/src/Strategies.hs +++ b/impl/strategies-impl/GeniusYield/OrderBot/Strategies/Impl.hs @@ -1,39 +1,32 @@ +{-# LANGUAGE MultiWayIf #-} {-| -Module : Strategies +Module : GeniusYield.OrderBot.Strategies.Impl Copyright : (c) 2023 GYELD GMBH License : Apache 2.0 Maintainer : support@geniusyield.co Stability : develop -} -module Strategies - ( BotStrategy(..) - , allStrategies - , mkIndependentStrategy - ) where +module GeniusYield.OrderBot.Strategies.Impl ( + BotStrategy (..), + allStrategies, + MatchResult, + IndependentStrategy, + mkIndependentStrategy, +) where import Control.Monad.State.Strict (State, execState, modify') -import Data.Aeson (FromJSON, ToJSON, parseJSON, withText) -import Data.Aeson.Types (Parser) import Data.Text (Text) +import Data.Aeson.Types (Parser) +import Data.Aeson +import GeniusYield.OrderBot.Types +import GeniusYield.OrderBot.OrderBook +import GeniusYield.OrderBot.OrderBook.Extra import Data.Data (Typeable) import GHC.Generics (Generic) import GHC.Natural (Natural) import System.Envy (Var (..)) -import GeniusYield.OrderBot.MatchingStrategy ( IndependentStrategy - , MatchResult - , completeFill - , partialFill - ) -import GeniusYield.OrderBot.OrderBook.List (OrderBook(..), unOrders) -import GeniusYield.OrderBot.Types ( OrderInfo (..) - , OrderType (BuyOrder, SellOrder) - , Volume (..) - , Price - ) - --- | Every bot strategy must be named here. data BotStrategy = OneSellToManyBuy deriving stock (Show, Eq, Generic) deriving anyclass (ToJSON, Typeable) @@ -51,15 +44,13 @@ instance Var BotStrategy where _ -> Nothing toVar = show -{- | A list containing all implemented strategies. This list is used for the - tests and for the error message during env variable parsing. --} allStrategies :: [BotStrategy] allStrategies = [OneSellToManyBuy] -{- | Given a bot strategy and a max amount of orders per transaction, creates - an independent strategy. --} +type MatchResult = [MatchExecutionInfo] + +type IndependentStrategy = (OrderAssetPair -> OrderBook -> [MatchResult]) + mkIndependentStrategy :: BotStrategy -> Natural -> IndependentStrategy mkIndependentStrategy bs maxOrders _ bk = case bs of @@ -68,13 +59,13 @@ mkIndependentStrategy bs maxOrders _ bk = -- | Strategy state containing the matchings found and the remaining buy orders. data StrategyState = StrategyState { matchResults :: ![MatchResult] - , remainingOrders :: ![OrderInfo 'BuyOrder] + , remainingOrders :: !(Orders 'BuyOrder) } -- | Utility function for updating the state, after one run of the strategy. updateStrategyState :: MatchResult - -> [OrderInfo 'BuyOrder] + -> Orders 'BuyOrder -> StrategyState -> StrategyState updateStrategyState [] bos' ss = ss { remainingOrders = bos' } @@ -87,10 +78,10 @@ updateStrategyState mr' bos' StrategyState { matchResults = mr } = `maxOrders`) buy orders. -} oneSellToManyBuy :: Natural -> OrderBook -> [MatchResult] -oneSellToManyBuy maxOrders OrderBook{sellOrders, buyOrders} = +oneSellToManyBuy maxOrders ob = matchResults - $ execState (mapM_ go $ unOrders sellOrders) - $ StrategyState {matchResults = [], remainingOrders = unOrders buyOrders} + $ execState (mapMOrders_ go $ sellOrders ob) + $ StrategyState {matchResults = [], remainingOrders = buyOrders ob} where go :: OrderInfo 'SellOrder -> State StrategyState () @@ -104,36 +95,39 @@ multiFill . Natural -> (Price -> Price -> Bool) -> OrderInfo b - -> [OrderInfo b'] - -> (MatchResult, [OrderInfo b']) + -> Orders b' + -> (MatchResult, Orders b') multiFill maxOrders checkPrices order = go (maxOrders - 1) vh where (Volume vl vh) = volume order checkPrice = checkPrices $ price order - go :: Natural -> Natural -> [OrderInfo b'] -> (MatchResult, [OrderInfo b']) + go :: Natural -> Natural -> Orders b' -> (MatchResult, Orders b') go _ 0 os = ([completeFill order], os) go 0 v os | (vh - v) >= vl = ([partialFill order (vh - v)], os) | otherwise = ([], os) - go _ v [] - | (vh - v) > vl = ([partialFill order (vh - v)], []) - | otherwise = ([], []) - go limitO remVol (o : os) - | remVol == maxFillX && checkPrice xP = - let !b = completeFill o - in ([completeFill order, b], os) - | remVol > maxFillX && remVol >= minFillX && checkPrice xP = - case go (limitO - 1) (remVol - maxFillX) os of - ([], _) -> updateRemaining o $ go limitO remVol os - (bs, s) -> (completeFill o : bs, s) - | remVol < maxFillX - && remVol >= minFillX - && checkPrice xP = - ([completeFill order, partialFill o remVol], os) - | otherwise = updateRemaining o $ go limitO remVol os - where - xP = price o - (Volume minFillX maxFillX) = volume o + go limitO remVol os' = + case unconsOrders os' of + Nothing -> + if | (vh - remVol) > vl -> ([partialFill order (vh - remVol)], emptyOrders) + | otherwise -> ([], emptyOrders) + Just (o, os) -> + if | remVol == maxFillX && checkPrice xP -> + let !b = completeFill o + in ([completeFill order, b], os) + | remVol > maxFillX && remVol >= minFillX && checkPrice xP -> + case go (limitO - 1) (remVol - maxFillX) os of + ([], _) -> updateRemaining o $ go limitO remVol os + (bs, s) -> (completeFill o : bs, s) + | remVol < maxFillX + && remVol >= minFillX + && checkPrice xP -> + ([completeFill order, partialFill o remVol], os) + | otherwise -> updateRemaining o $ go limitO remVol os + where + xP = price o + (Volume minFillX maxFillX) = volume o + + updateRemaining x (a, b) = (a, insertOrder x b) - updateRemaining x (a, b) = (a, x : b)