Skip to content

Commit

Permalink
Exclude too-small trades from optimizers
Browse files Browse the repository at this point in the history
- Exclude trades smaller than pool's minimum trade size from optimization
  in get_arb_trades and multipair_optimal arbitrage
- Scale price errors by target price
- Return price errors as dicts with trading pair as key
- Add ArbTrade class to templates.trader
  • Loading branch information
nagakingg committed Nov 3, 2023
1 parent 71c5518 commit 6c9e2e3
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 76 deletions.
15 changes: 15 additions & 0 deletions changelog.d/20231102_145659_nagakingg_exclude_trades.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Added
-----
- Added ArbTrade class to templates.trader. It is a Trade object with a
target price field, and a method to change the amount_in.

Changed
-------
- Changed arbitrage optimizers to exclude trades smaller than a
pool's minimum trade size from optimization. This reduces variability
in optimization and prevents some potential errors.

- Changed price errors returned by Traders to dicts indicating the
trading pair for each error value.

- Changed price errors to be normalized by target price.
2 changes: 1 addition & 1 deletion curvesim/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def compute_arb_metrics(self, **kwargs):

profits = self._compute_profits(prices, trade_data.trades)
price_error = trade_data.price_errors.apply(
lambda errors: sum(abs(e) for e in errors)
lambda errors: sum(abs(e) for e in errors.values())
)

results = concat([profits, price_error], axis=1)
Expand Down
47 changes: 30 additions & 17 deletions curvesim/pipelines/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from curvesim.logging import get_logger
from curvesim.metrics import metrics as Metrics
from curvesim.templates.trader import ArbTrade

logger = get_logger(__name__)
DEFAULT_METRICS = [
Expand Down Expand Up @@ -55,25 +56,22 @@ def post_trade_price_error(dx, coin_in, coin_out, price_target):
trades = []

for pair in prices:
i, j = pair

if pool.price(i, j) - prices[pair] > 0:
price = prices[pair]
coin_in, coin_out = i, j
elif pool.price(j, i) - 1 / prices[pair] > 0:
price = 1 / prices[pair]
coin_in, coin_out = j, i
else:
trades.append((0, pair, prices[pair]))
coin_in, coin_out, target_price = _get_arb_direction(pair, pool, prices[pair])

lower_bound = pool.get_min_trade_size(coin_in)
profit_per_unit = post_trade_price_error(
lower_bound, coin_in, coin_out, target_price
)
if profit_per_unit <= 0:
trades.append(ArbTrade(coin_in, coin_out, 0, target_price))
continue

high = pool.get_max_trade_size(coin_in, coin_out)
bounds = (0, high)
upper_bound = pool.get_max_trade_size(coin_in, coin_out)
try:
res = root_scalar(
post_trade_price_error,
args=(coin_in, coin_out, price),
bracket=bounds,
args=(coin_in, coin_out, target_price),
bracket=(lower_bound, upper_bound),
method="brentq",
)
size = int(res.root)
Expand All @@ -85,11 +83,26 @@ def post_trade_price_error(dx, coin_in, coin_out, price_target):
coin_in,
coin_out,
pool_price,
price,
pool_price - price,
target_price,
pool_price - target_price,
)
size = 0

trades.append((size, (coin_in, coin_out), price))
trades.append(ArbTrade(coin_in, coin_out, size, target_price))

return trades


def _get_arb_direction(pair, pool, market_price):
i, j = pair
price_error_i = pool.price(i, j) - market_price
price_error_j = pool.price(j, i) - 1 / market_price

if price_error_i >= price_error_j:
target_price = market_price
coin_in, coin_out = i, j
else:
target_price = 1 / market_price
coin_in, coin_out = j, i

return coin_in, coin_out, target_price
21 changes: 11 additions & 10 deletions curvesim/pipelines/simple/trader.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,23 @@ def compute_trades(self, prices):
best_trade = None
price_error = None
for t in trades:
size, coins, price_target = t
i, j = coins
min_trade_size = pool.get_min_trade_size(i)
if size <= min_trade_size:
coin_in, coin_out, amount_in, price_target = t
min_trade_size = pool.get_min_trade_size(coin_in)
if amount_in <= min_trade_size:
continue
with pool.use_snapshot_context():
out_amount, _ = pool.trade(i, j, size)
amount_out, _ = pool.trade(coin_in, coin_out, amount_in)
# assume we transacted at "infinite" depth at target price
# on the other exchange to obtain our in-token
profit = out_amount - size * price_target
profit = amount_out - amount_in * price_target
if profit > max_profit:
max_profit = profit
best_trade = Trade(i, j, size)
price_error = pool.price(i, j) - price_target
best_trade = Trade(coin_in, coin_out, amount_in)
price_error = (
pool.price(coin_in, coin_out) - price_target
) / price_target

if not best_trade:
return [], {"price_errors": []}
return [], {"price_errors": {}}

return [best_trade], {"price_errors": [price_error]}
return [best_trade], {"price_errors": {(coin_in, coin_out): price_error}}
4 changes: 3 additions & 1 deletion curvesim/pipelines/vol_limited_arb/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ def _compute_volume_limits(sample, vol_mult):

limits = {key: volumes[key] * vol_mult[key] for key in volumes}
reversed_limits = {(j, i): lim * prices[(i, j)] for (i, j), lim in limits.items()}
return {**limits, **reversed_limits}
all_limits = {**limits, **reversed_limits}

return {key: int(val * 10**18) for key, val in all_limits.items()}
154 changes: 111 additions & 43 deletions curvesim/pipelines/vol_limited_arb/trader.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pprint import pformat

from numpy import isnan
from scipy.optimize import least_squares

Expand Down Expand Up @@ -71,37 +73,32 @@ def multipair_optimal_arbitrage( # noqa: C901 pylint: disable=too-many-locals
res : scipy.optimize.OptimizeResult
Results object from the numerical optimizer.
"""
init_trades = get_arb_trades(pool, prices)
all_trades = get_arb_trades(pool, prices)
input_trades, skipped_trades = _apply_volume_limits(all_trades, limits, pool)

# Limit trade size, add size bounds
limited_init_trades = []
for t in init_trades:
size, pair, price_target = t
limit = int(limits[pair] * 10**18)
t = min(size, limit), pair, price_target, 0, limit + 1
limited_init_trades.append(t)
if not input_trades:
price_errors = _make_price_errors(skipped_trades=skipped_trades, pool=pool)
return [], price_errors, None

# Order trades in terms of expected size
limited_init_trades = sorted(limited_init_trades, reverse=True, key=lambda t: t[0])
sizes, coins, price_targets, lo, hi = zip(*limited_init_trades)
input_trades = _sort_trades_by_size(input_trades)
least_squares_inputs = _make_least_squares_inputs(input_trades, limits)

def post_trade_price_error_multi(dxs, price_targets, coins):
def post_trade_price_error_multi(amounts_in, price_targets, coin_pairs):
with pool.use_snapshot_context():
for k, pair in enumerate(coins):
if isnan(dxs[k]):
for coin_pair, amount_in in zip(coin_pairs, amounts_in):
if isnan(amount_in):
dx = 0
else:
dx = int(dxs[k])
dx = int(amount_in)

coin_in, coin_out = pair
min_size = pool.get_min_trade_size(coin_in)
min_size = pool.get_min_trade_size(coin_pair[0])
if dx > min_size:
pool.trade(coin_in, coin_out, dx)
pool.trade(*coin_pair, dx)

errors = []
for k, pair in enumerate(coins):
price = pool.price(*pair, use_fee=True)
errors.append(price - price_targets[k])
for coin_pair, price_target in zip(coin_pairs, price_targets):
price = pool.price(*coin_pair, use_fee=True)
errors.append(price - price_target)

return errors

Expand All @@ -111,38 +108,109 @@ def post_trade_price_error_multi(dxs, price_targets, coins):
try:
res = least_squares(
post_trade_price_error_multi,
x0=sizes,
args=(price_targets, coins),
bounds=(lo, hi),
**least_squares_inputs,
gtol=10**-15,
xtol=10**-15,
)

# Format trades into tuples, ignore if dx=0
dxs = res.x

for k, amount_in in enumerate(dxs):
# Record optimized trades
for trade, amount_in in zip(input_trades, res.x):
if isnan(amount_in):
continue

amount_in = int(amount_in)
coin_in, coin_out = coins[k]
min_size = pool.get_min_trade_size(coin_in)
min_size = pool.get_min_trade_size(trade.coin_in)
if amount_in > min_size:
trades.append(Trade(coin_in, coin_out, amount_in))
trades.append(Trade(trade.coin_in, trade.coin_out, amount_in))

errors = res.fun
price_errors = _make_price_errors(input_trades, res.fun, skipped_trades, pool)

except Exception:
logger.error(
"Optarbs args: x0: %s, lo: %s, hi: %s, prices: %s",
sizes,
lo,
hi,
price_targets,
exc_info=True,
)
errors = post_trade_price_error_multi([0] * len(sizes), price_targets, coins)
res = []
logger.error("Opt Arbs:\n %s", pformat(least_squares_inputs), exc_info=True)
price_errors = _make_price_errors(skipped_trades=all_trades, pool=pool)
res = None

return trades, price_errors, res


def _apply_volume_limits(arb_trades, limits, pool):
"""
Returns list of ArbTrades with amount_in set to min(limit, amount_in). Any trades
limited to less than the pool's minimum trade size are excluded.
"""

limited_arb_trades = []
excluded_trades = []
for trade in arb_trades:
pair = trade.coin_in, trade.coin_out
limited_amount_in = min(limits[pair], trade.amount_in)
lim_trade = trade.replace_amount_in(limited_amount_in)

if lim_trade.amount_in > pool.get_min_trade_size(lim_trade.coin_in):
limited_arb_trades.append(lim_trade)
else:
excluded_trades.append(lim_trade)

return limited_arb_trades, excluded_trades


def _sort_trades_by_size(trades):
"""Sorts trades by amount_in."""
sorted_trades = sorted(trades, reverse=True, key=lambda t: t.amount_in)
return sorted_trades


def _make_least_squares_inputs(trades, limits):
"""
Returns a dict of trades, bounds, and targets formatted as kwargs for least_squares.
"""

return trades, errors, res
coins_in, coins_out, amounts_in, price_targets = zip(*trades)
coin_pairs = tuple(zip(coins_in, coins_out))

low_bound = [0] * len(trades)
high_bound = [limits[pair] + 1 for pair in coin_pairs]

return {
"x0": amounts_in,
"kwargs": {"price_targets": price_targets, "coin_pairs": coin_pairs},
"bounds": (low_bound, high_bound),
}


def _make_price_errors(trades=None, trade_errors=None, skipped_trades=None, pool=None):
"""
Returns a dict mapping coin pairs to price errors.
Parameters
----------
trades :
Trades input into the least_squares optimizer
trade_errors :
Price errors returned by the optimizer
skipped_trades :
Trades excluded from optimization
pool :
The pool used to compute the pool price for skipped trades
Returns
-------
price_errors: Dict
Maps coin pairs (tuple) to price_errors
"""
price_errors = {}
if trades:
for trade, price_error in zip(trades, trade_errors):
coin_pair = trade.coin_in, trade.coin_out
price_errors[coin_pair] = price_error / trade.price_target

if skipped_trades:
for trade in skipped_trades:
coin_pair = trade.coin_in, trade.coin_out
price_error = pool.price(*coin_pair, use_fee=True) - trade.price_target
price_errors[coin_pair] = price_error / trade.price_target

return price_errors
19 changes: 15 additions & 4 deletions curvesim/templates/trader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from abc import ABC, abstractmethod
from dataclasses import fields
from typing import Union

from curvesim.logging import get_logger
Expand All @@ -16,8 +17,19 @@ class Trade:
amount_in: int

def __iter__(self):
# pylint: disable=no-member
return (getattr(self, attr) for attr in self.__slots__)
return (getattr(self, field.name) for field in fields(self))


@dataclass(frozen=True, slots=True)
class ArbTrade(Trade):
"""Trade object specifying an arbitrage trade."""

price_target: float

def replace_amount_in(self, new_amount_in):
"""Returns self, replacing amount_in."""
coin_in, coin_out, _, price_target = self
return ArbTrade(coin_in, coin_out, new_amount_in, price_target)


@dataclass(slots=True)
Expand All @@ -31,8 +43,7 @@ class TradeResult:
fee: int

def __iter__(self):
# pylint: disable=no-member
return (getattr(self, attr) for attr in self.__slots__)
return (getattr(self, field.name) for field in fields(self))

def set_attrs(self, **kwargs):
"""Sets multiple attributes defined by keyword arguments."""
Expand Down
Loading

0 comments on commit 6c9e2e3

Please sign in to comment.