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