Compare commits

..

4 Commits

Author SHA1 Message Date
tim
f3bdfdf97b trigger fixes 2025-03-10 21:09:40 -04:00
tim
be8c8bf019 order pprint touchup 2025-03-10 14:31:55 -04:00
tim
ecf1d21d5f bin/examine.py; readonly state; debug logs for Underfunded 2025-03-10 14:18:40 -04:00
tim
b7ed91d1c0 start of kraken accounting (unfinished) 2025-03-07 19:00:42 -04:00
21 changed files with 358 additions and 65 deletions

15
bin/examine Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
kubectl port-forward postgres-0 5431:5432 &
PF_PID=$!
shutdown () {
kill $PF_PID
wait
}
trap shutdown INT TERM
PYTHONPATH=src python -m dexorder.bin.examine rpc_url=arbitrum_dxod db_url=postgres://dexorder@localhost:5431/dexorder "$@"
shutdown

View File

@@ -1,21 +1,24 @@
aiohappyeyeballs==2.4.3
aiohttp==3.11.12
aiohttp==3.11.13
aiosignal==1.3.1
alembic==1.14.1
alembic==1.15.1
annotated-types==0.7.0
antlr4-python3-runtime==4.9.3
asn1crypto==1.5.1
async-lru==2.0.4
attrs==23.2.0
bip-utils==2.9.3
bitarray==3.0.0
cachetools==5.5.1
bitarray==3.1.1
cachetools==5.5.2
cattrs==24.1.2
cbor2==5.6.4
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.4.1
ckzg==1.0.2
click==8.1.8
coincurve==20.0.0
coremltools==8.2
crcmod==1.7
cytoolz==0.12.3
defaultlist==1.0.0
@@ -31,39 +34,73 @@ eth-rlp==1.0.1
eth-typing==4.4.0
eth-utils==4.1.1
eth_abi==5.2.0
filelock==3.17.0
frozenlist==1.4.1
fsspec==2025.2.0
google-auth==2.35.0
greenlet==3.0.3
hexbytes==0.3.1
hiredis==3.0.0
idna==3.7
imageio==2.37.0
importlib_resources==6.5.2
Jinja2==3.1.6
joblib==1.4.2
jsonschema==4.21.1
jsonschema-specifications==2023.12.1
kraken==5.3.0
kubernetes==31.0.0
lazy_loader==0.4
lightning==2.4.0
lightning-utilities==0.14.0
lru-dict==1.2.0
lxml==5.3.1
Mako==1.3.3
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
mpmath==1.3.0
msgpack-python==0.5.6
multidict==6.0.5
numpy==2.2.2
networkx==3.4.2
numpy==2.0.2
nvidia-cublas-cu12==12.1.3.1
nvidia-cuda-cupti-cu12==12.1.105
nvidia-cuda-nvrtc-cu12==12.1.105
nvidia-cuda-runtime-cu12==12.1.105
nvidia-cudnn-cu12==9.1.0.70
nvidia-cufft-cu12==11.0.2.54
nvidia-curand-cu12==10.3.2.106
nvidia-cusolver-cu12==11.4.5.107
nvidia-cusparse-cu12==12.1.0.106
nvidia-nccl-cu12==2.20.5
nvidia-nvjitlink-cu12==12.8.93
nvidia-nvtx-cu12==12.1.105
oauthlib==3.2.2
omegaconf==2.3.0
orjson==3.10.15
packaging==24.2
pagerduty==1.0.0
parsimonious==0.10.0
pillow==11.1.0
prometheus_client==0.21.1
propcache==0.2.0
protobuf==5.26.1
psycopg2-binary==2.9.10
py-sr25519-bindings==0.2.0
pyaml==25.1.0
pyarrow==19.0.1
pyasn1==0.6.1
pyasn1_modules==0.4.1
pycparser==2.22
pycryptodome==3.20.0
pydantic==2.9.2
pydantic_core==2.23.4
Pygments==2.19.1
PyNaCl==1.5.0
python-bidi==0.6.6
python-dateutil==2.9.0.post0
pytorch-lightning==2.5.0.post0
pytz==2025.1
pyunormalize==15.1.0
PyYAML==6.0.1
@@ -72,18 +109,32 @@ referencing==0.35.0
regex==2024.4.28
requests==2.32.3
requests-oauthlib==2.0.0
rich==13.9.4
rlp==4.0.1
rpds-py==0.18.0
rsa==4.9
scikit-image==0.24.0
scikit-learn==1.5.2
scipy==1.13.1
setuptools==75.8.2
shapely==2.0.7
six==1.16.0
socket.io-emitter==0.1.5.1
sortedcontainers==2.4.0
SQLAlchemy==2.0.38
sympy==1.13.3
threadpoolctl==3.5.0
tifffile==2025.2.18
toolz==0.12.1
torch==2.4.1
torchmetrics==1.6.2
torchvision==0.19.1
tqdm==4.67.1
triton==3.0.0
types-requests==2.32.0.20240914
typing_extensions==4.12.2
urllib3==2.2.1
web3==6.20.3
web3==6.20.4
websocket-client==1.8.0
websockets==14.2
websockets==13.1
yarl==1.17.2

View File

@@ -30,3 +30,4 @@ aiohttp
charset-normalizer
pytz
prometheus_client
krakenex

View File

@@ -59,7 +59,7 @@ _cwd() # do this first so that config has the right current working directory
# ordering here is important!
from .base.chain import Blockchain # the singletons are loaded into the dexorder.blockchain.* namespace
from .util import async_yield
from .util import async_yield, json
from .base.fixed import Fixed2, FixedDecimals, Dec18
from .configuration import config
from .base.account import Account

View File

@@ -0,0 +1 @@
from .accounting import *

View File

@@ -0,0 +1,65 @@
import logging
import tempfile
from dataclasses import dataclass
from typing import Optional
import krakenex
from dexorder import timestamp
from dexorder.bin.executable import execute
log = logging.getLogger(__name__)
kraken_api_key=r'HqPHnGsAHunFtaP8YZTFsyh+LauVrcgFHi/US+RseR/4DiT+NG/JpONV'
kraken_api_secret=r'4hvdMdaN5TlNlyk2PShdRCsOE/T4sFzeBrR7ZjC+LUGuAXhBehY8vvWDZSUSyna2OFeOJ9GntPvyXOhrpx70Bg=='
kraken = krakenex.API()
# start and end should be timestamps or datetimes. inclusiveness is [start,end) as usual
def kraken_get_ledger(start=None, end=None):
entries = []
offset=1 # 1-based ffs
if start:
start = timestamp(start) - 1 # kraken start is EXCLUSIVE for some reason
if end:
end = timestamp(end) - 1 # kraken end is INCLUSIVE. :/
while True:
kl = kraken.query_private('Ledgers', {'start':start, 'end':end, 'ofs':offset})
print(repr(kl))
break
if kl.empty:
break
for t in kl.itertuples():
print(t)
# noinspection PyShadowingBuiltins
offset += len(kl)
return entries
@dataclass
class KrakenConfig:
kraken_api_key: Optional[str] = None
kraken_api_secret: Optional[str] = None
kraken_start: Optional[str]= None # timestamp or date
kraken_end: Optional[str] = None # timestamp or date
async def main(kconfig: KrakenConfig):
load_kraken_key(kconfig)
kraken_get_ledger()
def load_kraken_key(kconfig):
temp = tempfile.NamedTemporaryFile()
if not kconfig.kraken_api_key or not kconfig.kraken_api_secret:
log.error("Must set kraken_api_key= and kraken_api_secret= on the command line")
exit(1)
temp.write(kconfig.kraken_api_key.encode())
temp.write(b'\n')
temp.write(kconfig.kraken_api_secret.encode())
temp.write(b'\n')
kraken.load_key(temp.name)
if __name__ == '__main__':
execute(main, parse_args=KrakenConfig)

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass
from enum import Enum
from typing import Optional
from dexorder import timestamp
from dexorder import timestamp, from_timestamp
from dexorder.util import hexbytes
from dexorder.util.convert import decode_IEEE754
@@ -250,6 +250,26 @@ class ElaboratedSwapOrderStatus:
def copy(self):
return copy.deepcopy(self)
def __str__(self):
msg = f'''
SwapOrder
status: {self.state.name}
in: {self.order.tokenIn}
out: {self.order.tokenOut}
exchange: {self.order.route.exchange.name, self.order.route.fee}
amount: {"input" if self.order.amountIsInput else "output"} {self.filledIn if self.order.amountIsInput else self.filledOut}/{self.order.amount}{" to owner" if self.order.outputDirectlyToOwner else ""}
minFill: {self.order.minFillAmount}
inverted: {self.order.inverted}
tranches:
'''
for i in range(len(self.trancheStatus)):
tranche = self.order.tranches[i]
ts = self.trancheStatus[i]
msg += f' {tranche}\n'
for fill in ts.fills:
msg += f' {fill}\n'
return msg
NO_OCO = 18446744073709551615 # max uint64
@@ -344,7 +364,7 @@ class Tranche:
)
def __str__(self):
msg = f'{self.fraction/MAX_FRACTION:.1%} {"start+" if self.startTimeIsRelative else ""}{self.startTime} to {"start+" if self.startTimeIsRelative else ""}{self.endTime}'
msg = f'{self.fraction/MAX_FRACTION:.1%} {"start+" if self.startTimeIsRelative else ""}{from_timestamp(self.startTime)} to {"start+" if self.startTimeIsRelative else ""}{from_timestamp(self.endTime)}'
if self.marketOrder:
# for marketOrders, minLine.intercept is the slippage
msg += f' market order slippage {self.minLine.intercept:.2%}'

View File

@@ -0,0 +1,77 @@
import argparse
import logging
from dexorder import db, blockchain
from dexorder.base.order import OrderKey
from dexorder.blocks import current_block, get_block
from dexorder.blockstate import current_blockstate
from dexorder.blockstate.blockdata import BlockData
from dexorder.blockstate.db_state import DbState
from dexorder.contract.dexorder import VaultContract
from dexorder.order.orderstate import Order
from dexorder.tokens import adjust_decimals
from dexorder.vault_blockdata import vault_balances, pretty_balances
from dexorder.bin.executable import execute
log = logging.getLogger(__name__)
def command_vault_argparse(subparsers):
parser = subparsers.add_parser('vault', help='show the vault\'s balances and orders')
parser.add_argument('address', help='address of the vault')
parser.add_argument('--all', help='show all orders including closed ones', action='store_true')
# parser.add_argument('--json', help='output in JSON format', action='store_true')
async def command_vault(args):
balances = vault_balances.get(args.address, {})
print(f'Vault {args.address} v{await VaultContract(args.address).version()}')
print(f'Balances:')
print(pretty_balances({k: (await adjust_decimals(k, v)) for k, v in balances.items()}))
print(f'Orders:')
i = 0
while True:
key = OrderKey(args.address, i)
try:
order = Order.of(key)
except KeyError:
break
if args.all or order.is_open:
print(await order.pprint())
i += 1
# for key in Order.open_orders:
# order = Order.of(key)
# if args.json:
# print(json.dumps(order.status.dump()))
# else:
# print()
# print(order)
async def main(args: list):
parser = argparse.ArgumentParser()
parser.add_argument('--chain-id', default=None)
subparsers = parser.add_subparsers(dest='command')
for name in globals():
if name.startswith('command_') and name.endswith('_argparse'):
globals()[name](subparsers)
parsed = parser.parse_args(args)
print(parsed)
try:
subcommand = globals()[f'command_{parsed.command}']
except KeyError:
parser.print_help()
exit(1)
await blockchain.connect()
db.connect()
db_state = DbState(BlockData.by_opt('db'))
with db.transaction():
state = await db_state.load()
state.readonly = True
current_blockstate.set(state)
block = await get_block(state.root_hash)
current_block.set(block)
await subcommand(parsed)
if __name__ == '__main__':
execute(main, parse_args=True)

View File

@@ -33,12 +33,12 @@ def split_args():
omegaconf_args = []
regular_args = []
for arg in sys.argv[1:]:
if '=' in arg:
if '=' in arg and not arg.startswith('--'):
key, value = arg.split('=', 1)
if hasattr(Config, key):
omegaconf_args.append(arg)
else:
regular_args.append(arg)
continue
regular_args.append(arg)
return omegaconf_args, regular_args
@@ -67,19 +67,21 @@ def execute(main:Callable[...,Coroutine[Any,Any,Any]], shutdown=None, *, parse_l
log.info('Logging configured to default')
xconf = None
if parse_args:
if callable(parse_args) or isinstance(parse_args, type):
omegaconf_args, regular_args = split_args()
else:
omegaconf_args = None
# NOTE: there is special command-line argument handling in config/load.py to get a config filename.
# The -c/--config flag MUST BE FIRST if present.
# The rest of the arguments are split by format into key=value for omegaconf and anything else is "regular args"
omegaconf_args, regular_args = split_args()
configuration.parse_args(omegaconf_args)
if callable(parse_args):
# noinspection PyUnboundLocalVariable
xconf = parse_args(regular_args)
elif isinstance(parse_args, type):
# must check for `type` before `callable`, because types are also callables
if isinstance(parse_args, type):
# noinspection PyUnboundLocalVariable
xconf = OmegaConf.merge(OmegaConf.structured(parse_args), OmegaConf.from_cli(regular_args))
elif callable(parse_args):
# noinspection PyUnboundLocalVariable
xconf = parse_args(regular_args)
else:
# just pass the regular args to main
xconf = regular_args
init_alerts()

View File

@@ -22,6 +22,11 @@ from dexorder.util import hexbytes
log = logging.getLogger(__name__)
def blocktime():
""" timestamp of the most recent block seen in real-time, NOT the current block being worked on """
return latest_block[current_chain.get().id].timestamp
async def get_block_timestamp(block_id: Union[bytes,int]) -> int:
block = await get_block(block_id)
if block is None:

View File

@@ -64,7 +64,7 @@ class BlockData (Generic[T]):
if self.lazy_getitem:
lazy = self.lazy_getitem(self, item)
if lazy is not NARG:
state.set(state.root_fork, self.series, item, lazy)
state.set(state.root_fork, self.series, item, lazy, readonly_override=True)
result = lazy
if result is NARG:
raise KeyError

View File

@@ -53,7 +53,10 @@ class BlockState:
with a diff height of the root branch or older is always part of the finalized blockchain.
"""
class ReadOnlyError(Exception): ...
def __init__(self):
self.readonly = False
self._root_branch: Optional[Branch] = None
self._root_fork: Optional[Fork] = None
self.height: int = 0 # highest branch seen
@@ -80,6 +83,8 @@ class BlockState:
@root_branch.setter
def root_branch(self, value: Branch):
if self.readonly:
raise self.ReadOnlyError()
self._root_branch = value
self._root_fork = Fork([value])
@@ -92,6 +97,8 @@ class BlockState:
return self._root_branch.head
def init_root_block(self, root_block: Block) -> Fork:
if self.readonly:
raise self.ReadOnlyError()
assert self.root_branch is None
return self.add_branch(Branch.from_block(root_block))
@@ -113,6 +120,8 @@ class BlockState:
should only be set to False when it is assured that the branch may be joined by height alone, because
the branch join is known to be at a live-blockchain-finalized height.
"""
if self.readonly:
raise self.ReadOnlyError()
assert branch.id not in self.branches_by_id
if self.root_branch is None:
@@ -155,6 +164,8 @@ class BlockState:
def remove_branch(self, branch: Branch, *, remove_series_diffs=True):
if self.readonly:
raise self.ReadOnlyError()
if branch.height == self.height and len(self.branches_by_height[branch.height]) == 1:
# this is the only branch at this height: compute the new lower height
other_heights = [b.height for b in self.branches_by_id.values() if b is not branch]
@@ -210,7 +221,9 @@ class BlockState:
return DELETE
def set(self, fork: Fork, series, key, value, overwrite=True):
def set(self, fork: Fork, series, key, value, overwrite=True, *, readonly_override=False):
if not readonly_override and self.readonly:
raise self.ReadOnlyError()
# first look for an existing value
branch = fork.branch
diffs = self.diffs_by_series.get(series,{}).get(key)
@@ -236,6 +249,8 @@ class BlockState:
return old_value
def unload(self, fork: Optional[Fork], series, key):
if self.readonly:
raise self.ReadOnlyError()
self.unloads[fork.branch_id].append((series, key))
def iteritems(self, fork: Optional[Fork], series):
@@ -285,6 +300,8 @@ class BlockState:
Returns the set of diffs for the promoted fork.
"""
if self.readonly:
raise self.ReadOnlyError()
found_root = False
promotion_branches = []
for branch in reversed(fork.branches):
@@ -350,6 +367,7 @@ class FinalizedBlockState:
"""
def __init__(self):
self.readonly = False
self.data = {}
self.by_hash = {}
@@ -361,6 +379,8 @@ class FinalizedBlockState:
def set(self, _fork: Optional[Fork], series, key, value, overwrite=True):
assert overwrite
if self.readonly:
raise BlockState.ReadOnlyError()
self.data.setdefault(series, {})[key] = value
def iteritems(self, _fork: Optional[Fork], series):
@@ -373,6 +393,8 @@ class FinalizedBlockState:
return self.data.get(series,{}).values()
def delete_series(self, _fork: Optional[Fork], series: str):
if self.readonly:
raise BlockState.ReadOnlyError()
del self.data[series]

View File

@@ -8,7 +8,7 @@ from omegaconf.errors import OmegaConfBaseException
from .schema import Config
schema = OmegaConf.structured(Config())
schema = OmegaConf.structured(Config(), flags={'struct': False})
_config_file = 'dexorder.toml'

View File

@@ -16,6 +16,7 @@ class Config:
ws_url: Optional[str] = 'ws://localhost:8545'
rpc_urls: Optional[dict[str,str]] = field(default_factory=dict)
db_url: Optional[str] = 'postgresql://dexorder:redroxed@localhost/dexorder'
db_readonly: bool = False
dump_sql: bool = False
redis_url: Optional[str] = 'redis://localhost:6379'
@@ -47,6 +48,9 @@ class Config:
slash_delay_mul: float = 2 # double the delay each time
slash_delay_max: int = 15 * 60
# Tranches are paused for this long after they trigger a slippage control
slippage_control_delay: float = 10 # matches the 10-second TWAP used by our uniswap router
walker_name: str = 'default'
walker_flush_interval: float = 300
walker_stop: Optional[int] = None # block number of the last block the walker should process

View File

@@ -1,24 +0,0 @@
import logging
from dexorder import db
from dexorder.contract import ERC20, CONTRACT_ERRORS
log = logging.getLogger(__name__)
async def token_decimals(addr):
key = f'td|{addr}'
try:
return db.kv[key]
except KeyError:
# noinspection PyBroadException
try:
decimals = await ERC20(addr).decimals()
except CONTRACT_ERRORS:
log.debug(f'token {addr} has no decimals()')
decimals = 0
except Exception:
log.debug(f'could not get token decimals for {addr}')
return None
db.kv[key] = decimals
return decimals

View File

@@ -3,7 +3,7 @@ import logging
from contextvars import ContextVar
import sqlalchemy
from sqlalchemy import Engine
from sqlalchemy import Engine, event
from sqlalchemy.orm import Session, SessionTransaction
from .migrate import migrate_database
@@ -99,7 +99,7 @@ class Db:
_session.set(None)
# noinspection PyShadowingNames
def connect(self, url=None, migrate=True, reconnect=False, dump_sql=None):
def connect(self, url=None, migrate=True, reconnect=False, dump_sql=None, readonly:bool=None):
if _engine.get() is not None and not reconnect:
return None
if url is None:
@@ -114,6 +114,19 @@ class Db:
if dump_sql is None:
dump_sql = config.dump_sql
engine = sqlalchemy.create_engine(url, echo=dump_sql, json_serializer=json.dumps, json_deserializer=json.loads)
if readonly is None:
readonly = config.db_readonly
if readonly:
@event.listens_for(engine, "connect")
def set_readonly(dbapi_connection, _connection_record):
cursor = dbapi_connection.cursor()
try:
cursor.execute("SET default_transaction_read_only = on;")
log.info('database connection set to READ ONLY')
finally:
cursor.close()
if migrate:
migrate_database(url)
with engine.connect() as connection:

View File

@@ -58,7 +58,7 @@ async def handle_order_placed(event: EventData):
log.debug(f'raw order status {obj}')
order = Order.create(addr, index, event['transactionHash'], obj)
await activate_order(order)
log.debug(f'new order {order.key}{order}')
log.debug(f'new order {order.key} {await order.pprint()}')
async def handle_swap_filled(event: EventData):
@@ -137,7 +137,7 @@ async def handle_transfer(transfer: EventData):
vault = None
if vault is not None:
await adjust_balance(vault, token_address, amount)
await update_balance_triggers(vault, token_address, amount)
await update_balance_triggers(vault, token_address)
# This wuold double-count fill fees. Instead, we book the transfer when sending money to the account as part of a refill.
# if is_tracked_address(to_address):
# # noinspection PyTypeChecker

View File

@@ -3,13 +3,14 @@ import logging
from dataclasses import dataclass
from typing import overload
from dexorder import DELETE, db, order_log
from dexorder import DELETE, db, order_log, from_timestamp
from dexorder.base.chain import current_chain
from dexorder.base.order import OrderKey, TrancheKey
from dexorder.base.orderlib import SwapOrderState, ElaboratedSwapOrderStatus, Fill
from dexorder.blockstate import BlockDict, BlockSet
from dexorder.database.model.orderindex import OrderIndex
from dexorder.routing import pool_address
from dexorder.tokens import adjust_decimals
from dexorder.util import json
from dexorder.vault_blockdata import vault_owners
@@ -287,6 +288,33 @@ class Order:
Order.vault_recently_closed_orders.listremove(key.vault, key.order_index)
def __str__(self):
return str(self.key)
async def pprint(self):
amount_token = self.order.tokenIn if self.order.amountIsInput else self.order.tokenOut
msg = f'''
SwapOrder {self.key}
status: {self.state.name}
placed: {from_timestamp(self.status.startTime)}
in: {self.order.tokenIn}
out: {self.order.tokenOut}
exchange: {self.order.route.exchange.name, self.order.route.fee}
amount: {"input" if self.order.amountIsInput else "output"} {await adjust_decimals(amount_token, self.filled):f}/{await adjust_decimals(amount_token, self.amount):f}{" to owner" if self.order.outputDirectlyToOwner else ""}
minFill: {await adjust_decimals(amount_token, self.min_fill_amount):f}
inverted: {self.order.inverted}
tranches:
'''
for i in range(len(self.order.tranches)):
tranche = self.order.tranches[i]
msg += f' {tranche}'
filled_amount = self.tranche_filled(i)
if filled_amount:
msg += f' filled {await adjust_decimals(amount_token, filled_amount)}'
msg += '\n'
return msg
# ORDER STATE
# various blockstate fields hold different aspects of an order's state.
@@ -318,8 +346,6 @@ class Order:
'of', db=True, redis=True, pub=pub_order_fills,
str2key=OrderKey.str2key, value2str=lambda v: json.dumps(v.dump()), str2value=lambda s:OrderFilled.load(json.loads(s)))
def __str__(self):
return str(self.order)
# "active" means the order wants to be executed now. this is not BlockData because it's cleared every block
active_orders: dict[OrderKey,Order] = {}

View File

@@ -2,8 +2,9 @@ import asyncio
import logging
from abc import abstractmethod
from collections import defaultdict
from datetime import timedelta
from enum import Enum, auto
from typing import Optional, Sequence
from typing import Optional, Sequence, Union
import numpy as np
from sortedcontainers import SortedList
@@ -99,7 +100,8 @@ def start_trigger_updates():
PriceLineTrigger.clear_data()
async def update_balance_triggers(vault: str, token: str, balance: int):
async def update_balance_triggers(vault: str, token: str):
balance = vault_balances.get(vault, {}).get(token)
updates = [bt.update(balance) for bt in BalanceTrigger.by_vault_token.get((vault, token), [])]
await asyncio.gather(*updates)
@@ -261,7 +263,7 @@ class BalanceTrigger (Trigger):
async def update(self, balance):
self.value = await input_amount_is_sufficient(self.order, balance)
# log.debug(f'update balance {balance} was sufficient? {self.value}')
# log.debug(f'update balance {balance} was sufficient? {self.value} {self.order.key}')
def remove(self):
try:
@@ -316,8 +318,8 @@ class TimeTrigger (Trigger):
if time == self._time:
return
self._time = time
self.remove()
self.update_active(time_now)
in_future = time_now >= time
self.value = in_future is self.is_start
def update_active(self, time_now: int = None, time: int = None):
if time_now is None:
@@ -599,7 +601,8 @@ class TrancheTrigger:
else:
order_log.debug(f'tranche part-filled {self.tk} in:{_amount_in} out:{_amount_out} remaining:{remaining}')
if self.market_order:
self.expire()
order_log.debug(f'tranche {self.tk} delayed {config.slippage_control_delay} seconds due to slippage control')
self.deactivate(config.slippage_control_delay)
self.slash_count = 0 # reset slash count
def touch(self):
@@ -631,15 +634,24 @@ class TrancheTrigger:
self.kill()
else:
delay = round(config.slash_delay_base * config.slash_delay_mul ** (self.slash_count-1))
self.deactivate(timestamp()+delay)
self.deactivate(delay)
def deactivate(self, until):
def deactivate(self, interval: Union[timedelta, int, float]):
# todo this timestamp should be consistent with the trigger time which is blockchain
now = current_clock.get().timestamp
self.deactivate_until(now + (interval.total_seconds() if isinstance(interval, timedelta) else interval))
def deactivate_until(self, until):
# Temporarily deactivate the tranche due to a rate limit. Use disable() to permanently halt the trigger.
log.debug(f'deactivating tranche {self.tk} until {from_timestamp(until)}')
if self.activation_trigger is None:
self.activation_trigger = TimeTrigger.create(True, self.tk, until)
else:
self.activation_trigger.time = until
try:
del active_tranches[self.tk]
except KeyError:
pass
def disable(self):
# permanently stop this trigger and deconstruct

View File

@@ -2,12 +2,12 @@ import asyncio
import functools
import logging
from dexorder import current_pub
from dexorder import current_pub, dec
from dexorder.base.chain import current_chain
from dexorder.blockstate import BlockDict
from dexorder.contract import ERC20, CONTRACT_ERRORS
from dexorder.contract.dexorder import VaultContract, vault_address
from dexorder.util import json
from dexorder.util import json, align_decimal
log = logging.getLogger(__name__)
@@ -102,3 +102,6 @@ async def refresh_vault_balances(vault, *tokens):
result[t] = a
return result
vault_balances.modify(vault, functools.partial(_adjust, vault, tokens, amounts))
def pretty_balances(b: dict[str,dec], padding=8) -> str:
return '\n'.join(f'{k:>} {align_decimal(v,padding)}' for k,v in b.items())