tranche fills

This commit is contained in:
tim
2024-09-15 00:58:33 -04:00
parent fa710db1ce
commit 04a9d963ea
5 changed files with 89 additions and 46 deletions

View File

@@ -4,8 +4,7 @@ from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
from numpy.ma.core import filled from dexorder import timestamp
from dexorder.util import hexbytes from dexorder.util import hexbytes
from dexorder.util.convert import decode_IEEE754 from dexorder.util.convert import decode_IEEE754
@@ -59,8 +58,10 @@ class Line:
intercept: float intercept: float
slope: float slope: float
def value(self, timestamp): def value(self, time: int=None):
return self.intercept + self.slope * timestamp if time is None:
time = timestamp()
return self.intercept + self.slope * time
@staticmethod @staticmethod
def load_from_chain(obj: tuple[int,int]): def load_from_chain(obj: tuple[int,int]):
@@ -162,7 +163,7 @@ class ElaboratedTrancheStatus:
] ]
@staticmethod @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 filledIn, filledOut, activationTime, startTime, endTime, fillsObj = obj
fills = [Fill.load(f) for f in fillsObj] fills = [Fill.load(f) for f in fillsObj]
return ElaboratedTrancheStatus(int(filledIn), int(filledOut), activationTime, startTime, endTime, fills) return ElaboratedTrancheStatus(int(filledIn), int(filledOut), activationTime, startTime, endTime, fills)
@@ -347,11 +348,11 @@ class Tranche:
if self.minLine.intercept or self.minLine.slope: if self.minLine.intercept or self.minLine.slope:
msg += f' >{self.minLine.intercept:.5g}' msg += f' >{self.minLine.intercept:.5g}'
if self.minLine.slope: 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: if self.maxLine.intercept or self.maxLine.slope:
msg += f' <{self.maxLine.intercept:.5g}' msg += f' <{self.maxLine.intercept:.5g}'
if self.maxLine.slope: 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: if self.rateLimitPeriod:
msg += f' {self.rateLimitFraction/MAX_FRACTION:.1%} every {self.rateLimitPeriod/60:.0} minutes' msg += f' {self.rateLimitFraction/MAX_FRACTION:.1%} every {self.rateLimitPeriod/60:.0} minutes'
return msg return msg

View File

@@ -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)}') log.warning(f'No order triggers for fill of {TrancheKey(order.key.vault, order.key.order_index, tranche_index)}')
else: else:
time = await get_block_timestamp(event['blockHash']) 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): async def handle_order_canceled(event: EventData):
# event DexorderCanceled (uint64 orderIndex); # event DexorderCanceled (uint64 orderIndex);

View File

@@ -19,23 +19,48 @@ log = logging.getLogger(__name__)
# We split off the fill information for efficient communication to clients. # We split off the fill information for efficient communication to clients.
@dataclass @dataclass
class OrderFilled: class TrancheFilled:
tranche_fills: list[list[Fill]] activation_time: int # updated next activation time due to any rate limit
fills: 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)
@staticmethod @staticmethod
def load(obj): 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): 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 # todo oco groups
@@ -65,7 +90,8 @@ class Order:
try: try:
return Order.instances[key] return Order.instances[key]
except KeyError: except KeyError:
return Order(key) pass
return Order(key)
@staticmethod @staticmethod
@@ -83,7 +109,9 @@ class Order:
Order.vault_open_orders.listappend(key.vault, key.order_index) 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 # 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 # 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'initialized order_filled[{key}]')
order_log.debug(f'order created {key}') order_log.debug(f'order created {key}')
return order return order
@@ -127,11 +155,11 @@ class Order:
return Order.order_filled[self.key].filledOut if self.is_open else self.status.filledOut return Order.order_filled[self.key].filledOut if self.is_open else self.status.filledOut
def tranche_filled_in(self, tranche_index: int): 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 else self.status.trancheStatus[tranche_index].filledIn
def tranche_filled_out(self, tranche_index: int): 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 else self.status.trancheStatus[tranche_index].filledOut
def tranche_filled(self, tranche_index: int): def tranche_filled(self, tranche_index: int):
@@ -152,14 +180,16 @@ class Order:
def is_open(self): def is_open(self):
return self.state.is_open 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}') order_log.debug(f'tranche fill {self.key}|{tranche_index} in:{filled_in} out:{filled_out}')
try: try:
old = Order.order_filled[self.key] old = Order.order_filled[self.key]
except KeyError: except KeyError:
raise raise
new = copy.deepcopy(old) 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_log.debug(f'updated order_filled: {new}')
Order.order_filled[self.key] = new Order.order_filled[self.key] = new
@@ -180,18 +210,16 @@ class Order:
else: else:
# order_log.debug(f'deleting order_filled[{self.key}]') # order_log.debug(f'deleting order_filled[{self.key}]')
filledIn = filledOut = 0 filledIn = filledOut = 0
for (i,fills) in enumerate(of.tranche_fills): for (i,tf) in enumerate(of.tranche_fills):
fi = fo = 0 tf: TrancheFilled
for fill in fills: fi = tf.filledIn
fill: Fill fo = tf.filledOut
fi += fill.filledIn
fo += fill.filledOut
filledIn += fi filledIn += fi
filledOut += fo filledOut += fo
ts = status.trancheStatus[i] ts = status.trancheStatus[i]
ts.fills = copy.deepcopy(fills)
ts.filledIn = fi ts.filledIn = fi
ts.filledOut = fo ts.filledOut = fo
ts.fills = copy.deepcopy(tf.fills)
status.filledIn = filledIn status.filledIn = filledIn
status.filledOut = filledOut status.filledOut = filledOut
Order.order_statuses[self.key] = status # set the status in order to save it Order.order_statuses[self.key] = status # set the status in order to save it

View File

@@ -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 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) close_order_and_disable_triggers(self.order, final_state)
def fill(self, tx: str, time: int, tranche_index, amount_in, amount_out, fee): 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) 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): if self.triggers[tranche_index].fill(amount_in, amount_out, next_activation_time):
self.check_complete() self.check_complete()
def expire_tranche(self, tranche_index): def expire_tranche(self, tranche_index):
@@ -215,6 +215,7 @@ async def input_amount_is_sufficient(order, token_balance):
if price is None: if price is None:
return token_balance > 0 # we don't know the price so we allow any nonzero amount to be sufficient 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) pool = await get_pool(order.pool_address)
price *= dec(10) ** dec(-pool['decimals'])
inverted = order.order.tokenIn != pool['base'] inverted = order.order.tokenIn != pool['base']
minimum = dec(order.min_fill_amount)*price if inverted else dec(order.min_fill_amount)/price 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') 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) self.set_time(time, current_clock.get().timestamp)
def set_time(self, time: int, time_now: int): def set_time(self, time: int, time_now: int):
if time == self._time:
return
self._time = time self._time = time
if self.active:
# remove old trigger
TimeTrigger.all.remove(self)
self.active = (time_now > time) is self.is_start self.active = (time_now > time) is self.is_start
TimeTrigger.all.remove(self) if self.active:
TimeTrigger.all.add(self) TimeTrigger.all.add(self)
def update(self): def update(self):
# called when our self.time has been reached # called when our self.time has been reached
@@ -321,9 +327,9 @@ class PriceLineTrigger (Trigger):
if is_barrier: if is_barrier:
log.warning('Barriers not supported') log.warning('Barriers not supported')
value_now = line.intercept + line.slope * current_clock.get().timestamp value_now = line.intercept + line.slope * current_clock.get().timestamp
price_above = price_now > value_now 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} {is_min is not price_above}') log.debug(f'initial price line {value_now} {"<" if is_min else ">"} {price_now} {activated}')
super().__init__(3 if is_min else 4, tk, is_min is not price_above) super().__init__(3 if is_min else 4, tk, activated)
self.line = line self.line = line
self.is_min = is_min self.is_min = is_min
self.is_barrier = is_barrier self.is_barrier = is_barrier
@@ -370,8 +376,8 @@ class PriceLineTrigger (Trigger):
line_value = m * time + b line_value = m * time + b
price_diff = sign * (y - line_value) price_diff = sign * (y - line_value)
activated = price_diff > 0 activated = price_diff > 0
# for price, line, s, a in zip(y, line_value, sign, activated): for price, line, s, a in zip(y, line_value, sign, activated):
# log.debug(f'price: {line} {">" if s == -1 else "<"} {price} {a}') log.debug(f'price: {line} {"<" if s == -1 else ">"} {price} {a}')
for t, activated in zip(PriceLineTrigger.triggers, activated): for t, activated in zip(PriceLineTrigger.triggers, activated):
t.handle_result(activated) t.handle_result(activated)
PriceLineTrigger.clear_data() PriceLineTrigger.clear_data()
@@ -436,6 +442,8 @@ class TrancheTrigger:
if tranche.marketOrder: if tranche.marketOrder:
min_trigger = max_trigger = None min_trigger = max_trigger = None
else: 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) pool = await get_pool(order.pool_address)
buy = pool['base'] == order.order.tokenOut buy = pool['base'] == order.order.tokenOut
min_trigger, max_trigger = await asyncio.gather( min_trigger, max_trigger = await asyncio.gather(
@@ -476,7 +484,13 @@ class TrancheTrigger:
log.debug(f'Tranche {tk} initial status {self.status} {self}') 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) remaining = self.order.tranche_remaining(self.tk.tranche_index)
filled = remaining == 0 or remaining < self.order.min_fill_amount filled = remaining == 0 or remaining < self.order.min_fill_amount
if filled: if filled:

View File

@@ -93,7 +93,7 @@ async def create_and_send_transactions():
TransactionJob.chain == current_chain.get(), TransactionJob.chain == current_chain.get(),
TransactionJob.state == TransactionJobState.Requested 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: try:
handler = TransactionHandler.of(job.request.type) handler = TransactionHandler.of(job.request.type)
except KeyError: except KeyError:
@@ -125,7 +125,7 @@ async def create_and_send_transactions():
job.tx_id = ctx.id_bytes job.tx_id = ctx.id_bytes
job.tx_data = ctx.data job.tx_data = ctx.data
db.session.add(job) 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: try:
sent = await w3.eth.send_raw_transaction(job.tx_data) sent = await w3.eth.send_raw_transaction(job.tx_data)
except: except: