diff --git a/src/dexorder/order/triggers.py b/src/dexorder/order/triggers.py index 9c685c3..e7a168e 100644 --- a/src/dexorder/order/triggers.py +++ b/src/dexorder/order/triggers.py @@ -81,8 +81,8 @@ class OrderTriggers: 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() + self.triggers[tranche_index].fill(amount_in, amount_out, next_activation_time) + self.check_complete() def expire_tranche(self, tranche_index): self.triggers[tranche_index].expire() @@ -119,28 +119,24 @@ async def end_trigger_updates(): Call once after all updates have been handled. This updates the active_tranches array based on final trigger state. """ PriceLineTrigger.end_updates(current_clock.get().timestamp) - # dirty can change - global _dirty while _dirty: - working_set = _dirty - _dirty = set() - for tk in working_set: - log.debug(f'check dirty tranche {tk}') - if _trigger_state.get(tk,0) == 0: - # all clear for execution. add to active list with any necessary proofs - active_tranches[tk] = PriceProof(0) - else: - # blocked by one or more triggers being False (nonzero mask) - # check expiry constraint - try: - TrancheTrigger.all[tk].check_expire() - except KeyError: - pass - # delete from active list. - try: - del active_tranches[tk] - except KeyError: - pass + tk = _dirty.pop() + log.debug(f'check dirty tranche {tk}') + if _trigger_state.get(tk,0) == 0: + # all clear for execution. add to active list with any necessary proofs + active_tranches[tk] = PriceProof(0) + else: + # blocked by one or more triggers being False (nonzero mask) + # check expiry constraint + try: + TrancheTrigger.all[tk].check_expire() + except KeyError: + pass + # delete from active list. + try: + del active_tranches[tk] + except KeyError: + pass def _Order__disable_triggers(order): @@ -187,17 +183,14 @@ class Trigger: @value.setter def value(self, value): - state = _trigger_state.get(self.tk) - if state is not None and value == (state & (1 << self.position) == 0): # NOTE: state is inverted - return - if state is None: - state = 0 - _dirty.add(self.tk) + state = _trigger_state.get(self.tk, 0) if not value: # this conditional is inverted _trigger_state[self.tk] = state | (1 << self.position) # set else: _trigger_state[self.tk] = state & ~(1 << self.position) # clear - self._value_changed() + _dirty.add(self.tk) + if value != (state & (1 << self.position) == 0): + self._value_changed() def _value_changed(self): pass @@ -315,7 +308,7 @@ class TimeTrigger (Trigger): time = self._time next_active = time_now < time activate = not self.active and next_active - log.debug(f'update_active {self} | {self.active} => {next_active} = {activate}') + # log.debug(f'update_active {self} | {self.active} => {next_active} = {activate}') if activate: # log.debug(f'adding time trigger {self}') TimeTrigger.all.add(self) @@ -359,6 +352,7 @@ class TimeTrigger (Trigger): class PriceLineTrigger (Trigger): by_pool: dict[str,set['PriceLineTrigger']] = defaultdict(set) + diagonals: set['PriceLineTrigger'] = set() @staticmethod def create(tk: TrancheKey, inverted: bool, price: dec, line: Line, is_min: bool, is_barrier: bool): @@ -382,7 +376,11 @@ class PriceLineTrigger (Trigger): self.is_barrier = is_barrier self.pool_address = Order.of(tk).pool_address self.index: Optional[int] = None + self.active = True + self.last_price = price_now PriceLineTrigger.by_pool[self.pool_address].add(self) + if self.line.slope != 0: + PriceLineTrigger.diagonals.add(self) # lines that need evaluating add their data to these arrays, which are then sent to SIMD for evaluation. each # array must always have the same size as the others. @@ -406,21 +404,32 @@ class PriceLineTrigger (Trigger): # oldPrice = price if self.inverted: price = 1/price + self.last_price = price log.debug(f'price trigger {price}') if self not in PriceLineTrigger.triggers_set: - self.index = len(PriceLineTrigger.y) - PriceLineTrigger.y.append(price) - PriceLineTrigger.m.append(self.line.slope) - PriceLineTrigger.b.append(self.line.intercept) - PriceLineTrigger.sign.append(1 if self.is_min else -1) - PriceLineTrigger.triggers.append(self) - PriceLineTrigger.triggers_set.add(self) + self.add_computation(price) else: # update an existing equation's price PriceLineTrigger.y[self.index] = price + def touch(self): + if self not in PriceLineTrigger.triggers_set: + self.add_computation(self.last_price) + + def add_computation(self, price): + self.index = len(PriceLineTrigger.y) + PriceLineTrigger.y.append(price) + PriceLineTrigger.m.append(self.line.slope) + PriceLineTrigger.b.append(self.line.intercept) + PriceLineTrigger.sign.append(1 if self.is_min else -1) + PriceLineTrigger.triggers.append(self) + PriceLineTrigger.triggers_set.add(self) + @staticmethod def end_updates(time: int): + for t in PriceLineTrigger.diagonals: + t.touch() # always evaluate any line with a slope + # here we use numpy to compute all dirty lines using SIMD y, m, b, sign = map(np.array, (PriceLineTrigger.y, PriceLineTrigger.m, PriceLineTrigger.b, PriceLineTrigger.sign)) @@ -434,14 +443,19 @@ class PriceLineTrigger (Trigger): PriceLineTrigger.clear_data() def handle_result(self, value: bool): - if not self.is_barrier or value: # barriers that are False do not update their values to False + if self.active and (not self.is_barrier or value): # barriers that are False do not update their values to False self.value = value def remove(self): + self.active = False try: PriceLineTrigger.by_pool[self.pool_address].remove(self) except KeyError: pass + try: + PriceLineTrigger.diagonals.remove(self) + except KeyError: + pass async def activate_orders(): @@ -501,7 +515,7 @@ class TrancheTrigger: inverted = order.order.inverted min_trigger = PriceLineTrigger.create(tk, inverted, price, tranche.minLine, True, tranche.minIsBarrier) max_trigger = PriceLineTrigger.create(tk, inverted, price, tranche.maxLine, False, tranche.maxIsBarrier) - return TrancheTrigger(order, tk, balance_trigger, activation_trigger, expiration_trigger, min_trigger, max_trigger) + return TrancheTrigger(order, tk, balance_trigger, activation_trigger, expiration_trigger, min_trigger, max_trigger, tranche.marketOrder) def __init__(self, order: Order, tk: TrancheKey, balance_trigger: BalanceTrigger, @@ -509,6 +523,7 @@ class TrancheTrigger: expiration_trigger: Optional[TimeTrigger], min_trigger: Optional[PriceLineTrigger], max_trigger: Optional[PriceLineTrigger], + market_order: bool, ): assert order.key.vault == tk.vault and order.key.order_index == tk.order_index tranche = order.order.tranches[tk.tranche_index] @@ -521,6 +536,7 @@ class TrancheTrigger: self.expiration_trigger = expiration_trigger self.min_trigger = min_trigger self.max_trigger = max_trigger + self.market_order = market_order self.slippage = tranche.minLine.intercept if tranche.marketOrder else 0 self.slash_count = 0 @@ -556,8 +572,9 @@ class TrancheTrigger: self.disable() else: order_log.debug(f'tranche part-filled {self.tk} in:{_amount_in} out:{_amount_out} remaining:{remaining}') + if self.market_order: + self.expire() self.slash_count = 0 # reset slash count - return filled def touch(self): _dirty.add(self.tk)