diff --git a/src/dexorder/base/orderlib.py b/src/dexorder/base/orderlib.py index 01c516e..fcc4d04 100644 --- a/src/dexorder/base/orderlib.py +++ b/src/dexorder/base/orderlib.py @@ -4,8 +4,7 @@ from dataclasses import dataclass from enum import Enum from typing import Optional -from numpy.ma.core import filled - +from dexorder import timestamp from dexorder.util import hexbytes from dexorder.util.convert import decode_IEEE754 @@ -59,8 +58,10 @@ class Line: intercept: float slope: float - def value(self, timestamp): - return self.intercept + self.slope * timestamp + def value(self, time: int=None): + if time is None: + time = timestamp() + return self.intercept + self.slope * time @staticmethod def load_from_chain(obj: tuple[int,int]): @@ -162,7 +163,7 @@ class ElaboratedTrancheStatus: ] @staticmethod - def load(obj: tuple[str,str,int,int,int]): + def load(obj: tuple[str,str,int,int,int,list]): filledIn, filledOut, activationTime, startTime, endTime, fillsObj = obj fills = [Fill.load(f) for f in fillsObj] return ElaboratedTrancheStatus(int(filledIn), int(filledOut), activationTime, startTime, endTime, fills) @@ -347,11 +348,11 @@ class Tranche: if self.minLine.intercept or self.minLine.slope: msg += f' >{self.minLine.intercept:.5g}' if self.minLine.slope: - msg += f'{self.minLine.slope:+.5g}/s' + msg += f'{self.minLine.slope:+.5g}/s({self.minLine.value():5g})' if self.maxLine.intercept or self.maxLine.slope: msg += f' <{self.maxLine.intercept:.5g}' if self.maxLine.slope: - msg += f'{self.maxLine.slope:+.5g}/s' + msg += f'{self.maxLine.slope:+.5g}/s({self.maxLine.value():5g})' if self.rateLimitPeriod: msg += f' {self.rateLimitFraction/MAX_FRACTION:.1%} every {self.rateLimitPeriod/60:.0} minutes' return msg diff --git a/src/dexorder/event_handler.py b/src/dexorder/event_handler.py index de82ab6..6f83236 100644 --- a/src/dexorder/event_handler.py +++ b/src/dexorder/event_handler.py @@ -80,7 +80,7 @@ async def handle_swap_filled(event: EventData): log.warning(f'No order triggers for fill of {TrancheKey(order.key.vault, order.key.order_index, tranche_index)}') else: time = await get_block_timestamp(event['blockHash']) - triggers.fill(hexstr(event['transactionHash']), time, tranche_index, amount_in, amount_out, fill_fee) + triggers.fill(hexstr(event['transactionHash']), time, tranche_index, amount_in, amount_out, fill_fee, next_execution_time) async def handle_order_canceled(event: EventData): # event DexorderCanceled (uint64 orderIndex); diff --git a/src/dexorder/order/orderstate.py b/src/dexorder/order/orderstate.py index d6f5efc..a58429c 100644 --- a/src/dexorder/order/orderstate.py +++ b/src/dexorder/order/orderstate.py @@ -19,23 +19,48 @@ log = logging.getLogger(__name__) # We split off the fill information for efficient communication to clients. @dataclass -class OrderFilled: - tranche_fills: list[list[Fill]] - - @property - def filledIn(self): - return sum(tf.filledIn for tfs in self.tranche_fills for tf in tfs) - - @property - def filledOut(self): - return sum(tf.filledOut for tfs in self.tranche_fills for tf in tfs) +class TrancheFilled: + activation_time: int # updated next activation time due to any rate limit + fills: list[Fill] @staticmethod def load(obj): - return OrderFilled([[Fill.load(tf) for tf in tfs] for tfs in obj]) + activation_time, fills = obj + return TrancheFilled(activation_time, [Fill.load(f) for f in fills]) def dump(self): - return [[tf.dump() for tf in tfs] for tfs in self.tranche_fills] + return [self.activation_time, [f.dump() for f in self.fills]] + + @property + def filledIn(self): + return sum(f.filledIn for f in self.fills) + + @property + def filledOut(self): + return sum(f.filledOut for f in self.fills) + + +@dataclass +class OrderFilled: + # This class is used as a minimal datastructure for communicating updated fill information. In open orders, it is + # stored separately from the ElaboratedOrderStatus and pasted in dynamically on access. This allows the main + # part of the ElaboratedOrderStatus to remain static, providing a lighter weight update. + tranche_fills: list[TrancheFilled] + + @property + def filledIn(self): + return sum(tf.filledIn for tf in self.tranche_fills) + + @property + def filledOut(self): + return sum(tf.filledOut for tf in self.tranche_fills) + + @staticmethod + def load(obj): + return OrderFilled([TrancheFilled.load(tf) for tf in obj]) + + def dump(self): + return [tf.dump() for tf in self.tranche_fills] # todo oco groups @@ -65,7 +90,8 @@ class Order: try: return Order.instances[key] except KeyError: - return Order(key) + pass + return Order(key) @staticmethod @@ -83,7 +109,9 @@ class Order: Order.vault_open_orders.listappend(key.vault, key.order_index) # Start with an empty set of fills, even if the chain says otherwise, because we will process the fill # events later and add them in - Order.order_filled[key] = OrderFilled([[] for _ in range(len(order.order.tranches))]) + Order.order_filled[key] = OrderFilled([ + TrancheFilled(ts.activationTime, []) # empty fills + for ts in order.status.trancheStatus]) order_log.debug(f'initialized order_filled[{key}]') order_log.debug(f'order created {key}') return order @@ -127,11 +155,11 @@ class Order: return Order.order_filled[self.key].filledOut if self.is_open else self.status.filledOut def tranche_filled_in(self, tranche_index: int): - return sum(tf.filledIn for tf in Order.order_filled[self.key].tranche_fills[tranche_index]) if self.is_open \ + return Order.order_filled[self.key].tranche_fills[tranche_index].filledIn if self.is_open \ else self.status.trancheStatus[tranche_index].filledIn def tranche_filled_out(self, tranche_index: int): - return sum(tf.filledOut for tf in Order.order_filled[self.key].tranche_fills[tranche_index]) if self.is_open \ + return Order.order_filled[self.key].tranche_fills[tranche_index].filledOut if self.is_open \ else self.status.trancheStatus[tranche_index].filledOut def tranche_filled(self, tranche_index: int): @@ -152,14 +180,16 @@ class Order: def is_open(self): return self.state.is_open - def add_fill(self, tx: str, time: int, tranche_index: int, filled_in: int, filled_out: int, fee: int): + def add_fill(self, tx: str, time: int, tranche_index: int, filled_in: int, filled_out: int, fee: int, next_activation_time: int): order_log.debug(f'tranche fill {self.key}|{tranche_index} in:{filled_in} out:{filled_out}') try: old = Order.order_filled[self.key] except KeyError: raise new = copy.deepcopy(old) - new.tranche_fills[tranche_index].append(Fill(tx, time, filled_in, filled_out, fee)) + tranche_fill = new.tranche_fills[tranche_index] + tranche_fill.activationTime = next_activation_time + tranche_fill.fills.append(Fill(tx, time, filled_in, filled_out, fee)) order_log.debug(f'updated order_filled: {new}') Order.order_filled[self.key] = new @@ -180,18 +210,16 @@ class Order: else: # order_log.debug(f'deleting order_filled[{self.key}]') filledIn = filledOut = 0 - for (i,fills) in enumerate(of.tranche_fills): - fi = fo = 0 - for fill in fills: - fill: Fill - fi += fill.filledIn - fo += fill.filledOut + for (i,tf) in enumerate(of.tranche_fills): + tf: TrancheFilled + fi = tf.filledIn + fo = tf.filledOut filledIn += fi filledOut += fo ts = status.trancheStatus[i] - ts.fills = copy.deepcopy(fills) ts.filledIn = fi ts.filledOut = fo + ts.fills = copy.deepcopy(tf.fills) status.filledIn = filledIn status.filledOut = filledOut Order.order_statuses[self.key] = status # set the status in order to save it diff --git a/src/dexorder/order/triggers.py b/src/dexorder/order/triggers.py index eb1be8a..734ad01 100644 --- a/src/dexorder/order/triggers.py +++ b/src/dexorder/order/triggers.py @@ -77,9 +77,9 @@ class OrderTriggers: final_state = SwapOrderState.Filled if self.order.remaining == 0 or self.order.remaining < self.order.min_fill_amount else SwapOrderState.Expired close_order_and_disable_triggers(self.order, final_state) - def fill(self, tx: str, time: int, tranche_index, amount_in, amount_out, fee): - self.order.add_fill(tx, time, tranche_index, amount_in, amount_out, fee) - if self.triggers[tranche_index].fill(amount_in, amount_out): + def fill(self, tx: str, time: int, tranche_index, amount_in, amount_out, fee, next_activation_time): + self.order.add_fill(tx, time, tranche_index, amount_in, amount_out, fee, next_activation_time) + if self.triggers[tranche_index].fill(amount_in, amount_out, next_activation_time): self.check_complete() def expire_tranche(self, tranche_index): @@ -215,6 +215,7 @@ async def input_amount_is_sufficient(order, token_balance): if price is None: return token_balance > 0 # we don't know the price so we allow any nonzero amount to be sufficient pool = await get_pool(order.pool_address) + price *= dec(10) ** dec(-pool['decimals']) inverted = order.order.tokenIn != pool['base'] minimum = dec(order.min_fill_amount)*price if inverted else dec(order.min_fill_amount)/price log.debug(f'order minimum amount is {order.min_fill_amount} '+ ("input" if order.amount_is_input else f"output @ {price} = {minimum} ")+f'< {token_balance} balance') @@ -275,10 +276,15 @@ class TimeTrigger (Trigger): self.set_time(time, current_clock.get().timestamp) def set_time(self, time: int, time_now: int): + if time == self._time: + return self._time = time + if self.active: + # remove old trigger + TimeTrigger.all.remove(self) self.active = (time_now > time) is self.is_start - TimeTrigger.all.remove(self) - TimeTrigger.all.add(self) + if self.active: + TimeTrigger.all.add(self) def update(self): # called when our self.time has been reached @@ -321,9 +327,9 @@ class PriceLineTrigger (Trigger): if is_barrier: log.warning('Barriers not supported') value_now = line.intercept + line.slope * current_clock.get().timestamp - price_above = price_now > value_now - # log.debug(f'initial price line {value_now} {">" if is_min else "<"} {price_now} {is_min is not price_above}') - super().__init__(3 if is_min else 4, tk, is_min is not price_above) + activated = value_now < price_now if is_min else value_now > price_now + log.debug(f'initial price line {value_now} {"<" if is_min else ">"} {price_now} {activated}') + super().__init__(3 if is_min else 4, tk, activated) self.line = line self.is_min = is_min self.is_barrier = is_barrier @@ -370,8 +376,8 @@ class PriceLineTrigger (Trigger): line_value = m * time + b price_diff = sign * (y - line_value) activated = price_diff > 0 - # for price, line, s, a in zip(y, line_value, sign, activated): - # log.debug(f'price: {line} {">" if s == -1 else "<"} {price} {a}') + for price, line, s, a in zip(y, line_value, sign, activated): + log.debug(f'price: {line} {"<" if s == -1 else ">"} {price} {a}') for t, activated in zip(PriceLineTrigger.triggers, activated): t.handle_result(activated) PriceLineTrigger.clear_data() @@ -436,6 +442,8 @@ class TrancheTrigger: if tranche.marketOrder: min_trigger = max_trigger = None else: + # tranche minLine and maxLine are relative to the pool and will be flipped from the orderspec if the + # order is selling the base and buying the quote. pool = await get_pool(order.pool_address) buy = pool['base'] == order.order.tokenOut min_trigger, max_trigger = await asyncio.gather( @@ -476,7 +484,13 @@ class TrancheTrigger: log.debug(f'Tranche {tk} initial status {self.status} {self}') - def fill(self, _amount_in, _amount_out ): + def fill(self, _amount_in, _amount_out, _next_activation_time ): + if _next_activation_time != DISTANT_PAST: + # rate limit + if self.activation_trigger is None: + self.activation_trigger = TimeTrigger(True, self.tk, _next_activation_time, timestamp()) + else: + self.activation_trigger.time = _next_activation_time remaining = self.order.tranche_remaining(self.tk.tranche_index) filled = remaining == 0 or remaining < self.order.min_fill_amount if filled: diff --git a/src/dexorder/transactions.py b/src/dexorder/transactions.py index 2ef16c9..d8bc9fa 100644 --- a/src/dexorder/transactions.py +++ b/src/dexorder/transactions.py @@ -93,7 +93,7 @@ async def create_and_send_transactions(): TransactionJob.chain == current_chain.get(), TransactionJob.state == TransactionJobState.Requested ): - log.info(f'building transaction request for {job.request.__class__.__name__} {job.id}') + # log.info(f'building transaction request for {job.request.__class__.__name__} {job.id}') try: handler = TransactionHandler.of(job.request.type) except KeyError: @@ -125,7 +125,7 @@ async def create_and_send_transactions(): job.tx_id = ctx.id_bytes job.tx_data = ctx.data db.session.add(job) - log.info(f'servicing transaction request {job.request.__class__.__name__} {job.id} with tx {ctx.id}') + log.info(f'servicing job {job.request.__class__.__name__} {job.id} with tx {ctx.id}') try: sent = await w3.eth.send_raw_transaction(job.tx_data) except: