Skip to content

Commit

Permalink
Merge pull request #279 from curveresearch/exclude-trades
Browse files Browse the repository at this point in the history
Exclude too-small trades from optimizer
  • Loading branch information
chanhosuh authored Nov 6, 2023
2 parents d82882a + bda3001 commit 103b828
Show file tree
Hide file tree
Showing 20 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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit 103b828

Please sign in to comment.