1515# limitations under the License.
1616
1717from decimal import Decimal
18- from typing import cast , Callable , Dict , Generator , Iterable , List , Optional
18+ from typing import Any , Awaitable , cast , Callable , Dict , Generator , Iterable , List , Optional
1919import dataclasses
20+ import datetime
2021import decimal
2122import logging
2223
2324from basana .backtesting import account_balances , config , errors , fees , helpers , lending , loan_mgr , liquidity , prices
24- from basana .backtesting .orders import Order
25+ from basana .backtesting .orders import Order , OrderInfo
2526from basana .backtesting .value_map import ValueMap , ValueMapDict
2627from basana .core import bar , dispatcher , helpers as core_helpers , logs
2728from basana .core .enums import OrderOperation
2829from basana .core .pair import Pair
30+ import basana as bs
2931
3032
3133logger = logging .getLogger (__name__ )
3234
3335LiquidityStrategyFactory = Callable [[], liquidity .LiquidityStrategy ]
36+ OrderEventHandler = Callable [["OrderEvent" ], Awaitable [Any ]]
3437
3538
3639@dataclasses .dataclass
3740class ExchangeContext :
38- dispatcher : dispatcher .EventDispatcher
41+ dispatcher : dispatcher .BacktestingDispatcher
3942 account_balances : account_balances .AccountBalances
4043 prices : prices .Prices
4144 fee_strategy : fees .FeeStrategy
@@ -44,12 +47,24 @@ class ExchangeContext:
4447 config : config .Config
4548
4649
50+ class OrderEvent (bs .Event ):
51+ """
52+ An event for order updates.
53+ """
54+
55+ def __init__ (self , when : datetime .datetime , order : OrderInfo ):
56+ super ().__init__ (when )
57+ #: The order.
58+ self .order : OrderInfo = order
59+
60+
4761class OrderManager :
4862 def __init__ (self , exchange_ctx : ExchangeContext ):
4963 self ._ctx = exchange_ctx
5064 self ._liquidity_strategies : Dict [Pair , liquidity .LiquidityStrategy ] = {}
5165 self ._orders = helpers .ExchangeObjectContainer [Order ]()
5266 self ._holds_by_order : Dict [str , ValueMap ] = {}
67+ self ._order_updates = core_helpers .LazyProxy (bs .FifoQueueEventSource )
5368
5469 def on_bar_event (self , bar_event : bar .BarEvent ):
5570 if (liquidity_strategy := self ._liquidity_strategies .get (bar_event .bar .pair )) is None :
@@ -69,7 +84,12 @@ def add_order(self, order: Order):
6984 self ._ctx .account_balances .update (hold_updates = required_balances )
7085 self ._holds_by_order [order .id ] = required_balances
7186
87+ # The order got accepted.
7288 self ._orders .add (order )
89+ # Checking dispatcher.now_available is necessary to avoid calling dispatcher.now() when no events have been
90+ # processed yet.
91+ if self ._order_updates .initialized and self ._ctx .dispatcher .now_available and self ._ctx .dispatcher .now ():
92+ self ._order_updates .push (OrderEvent (self ._ctx .dispatcher .now (), order .get_order_info ()))
7393
7494 except errors .NotEnoughBalance as e :
7595 logger .debug (logs .StructuredMessage (
@@ -95,6 +115,14 @@ def cancel_order(self, order_id: str):
95115 order .cancel ()
96116 self ._order_closed (order )
97117
118+ def subscribe_to_order_events (self , event_handler : OrderEventHandler ):
119+ """
120+ Registers an async callable that will be called when an order is updated.
121+
122+ :param event_handler: The event handler.
123+ """
124+ self ._ctx .dispatcher .subscribe (self ._order_updates .obj , cast (dispatcher .EventHandler , event_handler ))
125+
98126 def _update_balances (self , order : Order , balance_updates : ValueMapDict ):
99127 # If we have holds associated with the order, it may be time to release some/all of those.
100128 hold_updates = {}
@@ -148,31 +176,43 @@ def _borrow(self, required_balances: ValueMap, order: Order):
148176 self ._ctx .loan_mgr .cancel_loan (loan_id )
149177 raise
150178
151- def _repay_loans (self , symbol : str ):
179+ # Add loans to order.
180+ for loan_id in loan_ids :
181+ order .add_loan (loan_id )
182+
183+ def _repay_loans (self , order : Order ):
184+ if order .operation == OrderOperation .BUY :
185+ credit_symbol = order .pair .base_symbol
186+ else :
187+ credit_symbol = order .pair .quote_symbol
188+
152189 candidate_loans = [
153190 loan for loan in self ._ctx .loan_mgr .get_loans (is_open = True )
154- if loan .borrowed_symbol == symbol
191+ if loan .borrowed_symbol == credit_symbol
155192 ]
156193 # Try to cancel bigger loans first.
157194 candidate_loans .sort (key = lambda loan : loan .borrowed_amount , reverse = True )
195+ loan_ids : List [str ] = []
158196 for loan in candidate_loans :
159197 try :
160198 self ._ctx .loan_mgr .repay_loan (loan .id )
199+ loan_ids .append (loan .id )
161200 loan = cast (lending .LoanInfo , self ._ctx .loan_mgr .get_loan (loan .id ))
162201 logger .debug (logs .StructuredMessage ("Repayed loan" , loan = dataclasses .asdict (loan )))
163202 except errors .NotEnoughBalance :
164203 pass
165204
205+ # Add loans to order.
206+ for loan_id in loan_ids :
207+ order .add_loan (loan_id )
208+
166209 def _order_closed (self , order : Order ):
167210 # The order is closed and there might be balances on hold that have to be released.
168211 self ._update_balances (order , {})
169- # If the order has auto_repay set and is filled, either fully or partially, then we need to cancel any open
170- # loans matching the order's base/quote currency.
212+ # If the order has auto_repay set and is filled, either fully or partially, then we need to cancel matching open
213+ # loans
171214 if order .auto_repay and order .amount_filled :
172- if order .operation == OrderOperation .BUY :
173- self ._repay_loans (order .pair .base_symbol )
174- else :
175- self ._repay_loans (order .pair .quote_symbol )
215+ self ._repay_loans (order )
176216
177217 def _process_order (
178218 self , order : Order , bar_event : bar .BarEvent , liquidity_strategy : liquidity .LiquidityStrategy
@@ -182,6 +222,9 @@ def order_not_filled():
182222 logger .debug (logs .StructuredMessage ("Order not filled" , order_id = order .id , order_state = order .state ))
183223 if not order .is_open :
184224 self ._order_closed (order )
225+ # Push the order event since the order is now closed.
226+ if self ._order_updates .initialized :
227+ self ._order_updates .push (OrderEvent (bar_event .when , order .get_order_info ()))
185228
186229 # Calculate balance updates for the current bar.
187230 logger .debug (logs .StructuredMessage (
@@ -225,6 +268,10 @@ def order_not_filled():
225268 if not order .is_open :
226269 self ._order_closed (order )
227270
271+ # Push the order event since the order got updated.
272+ if self ._order_updates .initialized :
273+ self ._order_updates .push (OrderEvent (bar_event .when , order .get_order_info ()))
274+
228275 except errors .NotEnoughBalance as e :
229276 logger .debug (logs .StructuredMessage (
230277 "Balance short processing order" , order = order .get_debug_info (), error = str (e )
0 commit comments