tranche fills
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user