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 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

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)}')
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);

View File

@@ -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,6 +90,7 @@ class Order:
try:
return Order.instances[key]
except KeyError:
pass
return Order(key)
@@ -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

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
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,9 +276,14 @@ 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
self.active = (time_now > time) is self.is_start
if self.active:
# remove old trigger
TimeTrigger.all.remove(self)
self.active = (time_now > time) is self.is_start
if self.active:
TimeTrigger.all.add(self)
def update(self):
@@ -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:

View File

@@ -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: