Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ pools:
- address: "cb85e12ca5d98de95715fc75ae251a66b662ea06" # GOLDST-SILVST pool
- address: "34d7caf576cf9493f054d9eced99dcd463eba4b7" # ETHST-BTCST pool
- address: "72f029994b1003447b4fcae5025ae78fcbec258e" # wstETHST-USDST pool
# Add new stable pools by POOL address only (not token addresses):
- address: "ff2befcd850183170627dcbc377c3fd573789172" # XAUTST-GOLDST pool
- address: "902651e10ab7d64fa7d0480657ac36676d155f2e" # sUSDSST-USDST pool
- address: "5888fbe6d6774c1d5788a7b631fc2a2fe88c44c6" # syrupUCDCST-USDST pool
- address: "41be20683ef9d57884e0a92f203e6c3161cf0aa1" # rETHST-wstETHSTpool

# Trading Parameters
trading:
fee_bps: 30 # Pool fee in basis points (0.3%)
min_profit: 0.01 # $0.01 minimum profit
min_profit: 0.01
slippage_factor_amm: 0.96 # 4% slippage tolerance for AMM pools
slippage_factor_stable: 0.92 # 8% slippage tolerance for stable pools

# Oracle Configuration
oracle:
Expand Down
14 changes: 12 additions & 2 deletions core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,15 @@
# USDST token address
USDST_ADDRESS = "937efa7e3a77e20bbdbd7c0d32b6514f368c1010"

# Tokens that use BlockApps on-chain price oracle instead of Alchemy
BLOCKAPPS_ORACLE_TOKENS = {"GOLDST", "SILVST", "Wrapped wstETH"}
# Tokens that use BlockApps on-chain price oracle instead of Alchemy.
# This includes synthetic/stable assets that are typically unavailable on Alchemy symbol feeds.
BLOCKAPPS_ORACLE_TOKENS = {
"GOLDST",
"SILVST",
"Wrapped wstETH",
"SUSDST",
"USDSST",
"XAUTST",
"rETHST",

}
182 changes: 182 additions & 0 deletions core/math_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from core.constants import WEI_SCALE, BPS_DENOM

STABLE_FEE_DENOMINATOR = 10_000_000_000
STABLE_A_PRECISION = 100

def get_optimal_input(
reserve_in: int,
reserve_out: int,
Expand Down Expand Up @@ -147,3 +150,182 @@ def find_optimal_trade_auto(

return ("Pool price equals oracle price (no arbitrage opportunity)", None)


def _stable_dynamic_fee(xpi: int, xpj: int, fee: int, offpeg_fee_multiplier: int) -> int:
if offpeg_fee_multiplier <= STABLE_FEE_DENOMINATOR:
return fee
xps2 = (xpi + xpj) * (xpi + xpj)
if xps2 <= 0:
return fee
num = offpeg_fee_multiplier * fee
den = (((offpeg_fee_multiplier - STABLE_FEE_DENOMINATOR) * 4 * xpi * xpj) // xps2) + STABLE_FEE_DENOMINATOR
return num // den if den > 0 else fee


def _stable_get_d(xp0: int, xp1: int, amp: int) -> int:
if xp0 <= 0 or xp1 <= 0:
return 0
s = xp0 + xp1
d = s
ann = amp * 2
for _ in range(256):
d_p = d
d_p = (d_p * d) // xp0
d_p = (d_p * d) // xp1
d_p //= 4 # n^n for n=2
d_prev = d
num = (((ann * s) // STABLE_A_PRECISION) + (d_p * 2)) * d
den = ((((ann - STABLE_A_PRECISION) * d) // STABLE_A_PRECISION) + (3 * d_p))
if den <= 0:
return 0
d = num // den
if abs(d - d_prev) <= 1:
return d
return 0


def _stable_get_y(i: int, j: int, x: int, xp0: int, xp1: int, amp: int, d: int) -> int:
if i == j or i < 0 or j < 0 or i > 1 or j > 1:
return 0
ann = amp * 2
c = d
s_ = 0
for idx in (0, 1):
if idx == i:
_x = x
elif idx != j:
_x = xp0 if idx == 0 else xp1
else:
continue
if _x <= 0:
return 0
s_ += _x
c = (c * d) // (_x * 2)

c = (c * d * STABLE_A_PRECISION) // (ann * 2)
b = s_ + ((d * STABLE_A_PRECISION) // ann)
y = d
for _ in range(256):
y_prev = y
den = (2 * y + b - d)
if den <= 0:
return 0
y = ((y * y) + c) // den
if abs(y - y_prev) <= 1:
return y
return 0


def _stable_quote_output(
dx: int,
reserve_x: int,
reserve_y: int,
is_x_to_y: bool,
amp: int,
fee: int,
offpeg_fee_multiplier: int
) -> int:
if dx <= 0 or reserve_x <= 0 or reserve_y <= 0 or amp <= 0:
return 0

xp0 = reserve_x
xp1 = reserve_y
i, j = (0, 1) if is_x_to_y else (1, 0)
xp_i = xp0 if i == 0 else xp1
xp_j = xp1 if j == 1 else xp0
x = xp_i + dx # rates ~= 1e18 for these pools, so xp increment equals token units
d = _stable_get_d(xp0, xp1, amp)
if d <= 0:
return 0
y = _stable_get_y(i, j, x, xp0, xp1, amp, d)
if y <= 0:
return 0
dy = xp_j - y - 1
if dy <= 0:
return 0
dy_fee = (dy * _stable_dynamic_fee((xp_i + x) // 2, (xp_j + y) // 2, fee, offpeg_fee_multiplier)) // STABLE_FEE_DENOMINATOR
dy_net = dy - dy_fee
return dy_net if dy_net > 0 else 0


def find_optimal_trade_stable_auto(
reserve_x: int,
reserve_y: int,
oracle_price_xy: int, # Y per X, 1e18
balance_x: int,
balance_y: int,
fee_bps: int,
min_profit: int, # token Y wei (caller converts from USD)
stable_params: Optional[dict] = None
) -> Tuple[Optional[str], Optional[Tuple[str, int, int, int]]]:
"""
Stable-pool arbitrage sizing heuristic.
Uses a bounded depth (0.10% to 5.00% of reserve) based on price divergence.
"""
if reserve_x <= 0 or reserve_y <= 0 or oracle_price_xy <= 0:
return ("Invalid inputs (reserve_x={}, reserve_y={}, oracle_price_xy={})".format(reserve_x, reserve_y, oracle_price_xy), None)
if balance_x <= 0 and balance_y <= 0:
return ("No balances (balance_x={}, balance_y={})".format(balance_x, balance_y), None)
if not (0 <= fee_bps < BPS_DENOM):
return ("Invalid fee_bps ({})".format(fee_bps), None)

pool_price_xy = (reserve_y * WEI_SCALE) // reserve_x
if pool_price_xy <= 0:
return ("Invalid pool price ({})".format(pool_price_xy), None)

diff = abs(pool_price_xy - oracle_price_xy)
divergence_bps = (diff * BPS_DENOM) // oracle_price_xy
# Keep stable trades conservative while still responsive to mispricing.
depth_bps = max(10, min(500, divergence_bps)) # 0.10% .. 5.00%

params = stable_params or {}
amp = int(params.get("amp", 100 * STABLE_A_PRECISION))
fee_1e10 = int(params.get("fee", fee_bps * 1_000_000))
offpeg_fee_multiplier = int(params.get("offpeg_fee_multiplier", STABLE_FEE_DENOMINATOR))

if pool_price_xy < oracle_price_xy:
# Pool underprices X -> buy X with Y (Y->X)
dy_cap = (reserve_y * depth_bps) // BPS_DENOM
dy = min(dy_cap, balance_y)
if dy <= 0:
return ("No input available for Y->X (dy_cap={}, balance_y={})".format(dy_cap, balance_y), None)
x_out = _stable_quote_output(
dx=dy,
reserve_x=reserve_x,
reserve_y=reserve_y,
is_x_to_y=False,
amp=amp,
fee=fee_1e10,
offpeg_fee_multiplier=offpeg_fee_multiplier,
)
if x_out <= 0:
return ("No output for Y->X (x_out={})".format(x_out), None)
profit_y = (x_out * oracle_price_xy) // WEI_SCALE - dy
if profit_y > 0 and profit_y >= min_profit:
return (None, ("Y->X", dy, x_out, profit_y))
return ("Profit too low for Y->X (profit={:.6f}, min_profit={:.6f})".format(profit_y / WEI_SCALE, min_profit / WEI_SCALE), None)

if pool_price_xy > oracle_price_xy:
# Pool overprices X -> sell X for Y (X->Y)
dx_cap = (reserve_x * depth_bps) // BPS_DENOM
dx = min(dx_cap, balance_x)
if dx <= 0:
return ("No input available for X->Y (dx_cap={}, balance_x={})".format(dx_cap, balance_x), None)
y_out = _stable_quote_output(
dx=dx,
reserve_x=reserve_x,
reserve_y=reserve_y,
is_x_to_y=True,
amp=amp,
fee=fee_1e10,
offpeg_fee_multiplier=offpeg_fee_multiplier,
)
if y_out <= 0:
return ("No output for X->Y (y_out={})".format(y_out), None)
profit_y = y_out - (dx * oracle_price_xy) // WEI_SCALE
if profit_y > 0 and profit_y >= min_profit:
return (None, ("X->Y", dx, y_out, profit_y))
return ("Profit too low for X->Y (profit={:.6f}, min_profit={:.6f})".format(profit_y / WEI_SCALE, min_profit / WEI_SCALE), None)

return ("Pool price equals oracle price (no arbitrage opportunity)", None)

32 changes: 22 additions & 10 deletions engine/arb_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from onchain.token import Token
from onchain.pool import Pool
from market.oracle import PriceOracle
from core.math_utils import find_optimal_trade_auto
from core.math_utils import find_optimal_trade_auto, find_optimal_trade_stable_auto
from engine.helpers import check_gas_balance, check_sell_pnl, update_cumulative_profit

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -141,16 +141,28 @@ def scan_for_opportunity(self) -> Optional[ArbitrageOpportunity]:
logger.info(f"Oracle price: {oracle_price / WEI_SCALE:.6f} {self.token_b.symbol} per {self.token_a.symbol}")
logger.info(f"Price diff: {price_diff / WEI_SCALE:.6f} {self.token_b.symbol} ({price_diff_pct:.2f}%)")

# Use auto-direction selection to find optimal trade (with gas-adjusted balances)
reason, result = find_optimal_trade_auto(
reserve_x=reserve_a,
reserve_y=reserve_b,
oracle_price_xy=oracle_price,
balance_x=check_gas_balance(self.token_a, self.token_a.balance),
balance_y=check_gas_balance(self.token_b, self.token_b.balance),
fee_bps=self.fee_bps,
min_profit=min_profit_token_b
# Use stable-aware sizing for stable pools and AMM closed-form sizing for AMM pools.
common_kwargs = {
"reserve_x": reserve_a,
"reserve_y": reserve_b,
"oracle_price_xy": oracle_price,
"balance_x": check_gas_balance(self.token_a, self.token_a.balance),
"balance_y": check_gas_balance(self.token_b, self.token_b.balance),
"fee_bps": self.fee_bps,
"min_profit": min_profit_token_b,
}
is_stable_pool = self.pool.is_stable_pool()
logger.info(
f"pricing path selected: {'stable' if is_stable_pool else 'amm'} "
f"(isStable={is_stable_pool}, source=on-chain BlockApps-Pool.isStable)"
)
if is_stable_pool:
reason, result = find_optimal_trade_stable_auto(
**common_kwargs,
stable_params=self.pool.get_stable_params(),
)
else:
reason, result = find_optimal_trade_auto(**common_kwargs)

if result is None:
logger.info("No arbitrage opportunity found - {}".format(reason))
Expand Down
21 changes: 17 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,25 @@ def init_components(self):
trade_cfg = self.cfg["trading"]
min_profit = Decimal(str(trade_cfg["min_profit"]))
min_profit_wei = int(min_profit * WEI_SCALE)
slippage_factor_amm = float(trade_cfg.get("slippage_factor_amm", 0.96))
slippage_factor_stable = float(trade_cfg.get("slippage_factor_stable", 0.92))

exec_cfg = self.cfg.get("execution", {})
self.vault_addr = exec_cfg.get("vault_addr", "")

# Initialize executor for each pool
for pool_config in pools:
pool_addr = pool_config.get("address")

pool = Pool(pool_addr, fee_bps=fee_bps)
pool_fee_bps = int(pool_config.get("fee_bps", fee_bps))

pool_slippage_amm = float(pool_config.get("slippage_factor_amm", slippage_factor_amm))
pool_slippage_stable = float(pool_config.get("slippage_factor_stable", slippage_factor_stable))
pool = Pool(
pool_addr,
fee_bps=pool_fee_bps,
slippage_factor_amm=pool_slippage_amm,
slippage_factor_stable=pool_slippage_stable,
)
pool.fetch_pool_data()

# Auto-register BlockApps tokens based on token names
Expand All @@ -85,15 +95,18 @@ def init_components(self):
token_b=pool.token_b,
pool=pool,
oracle=self.oracle,
fee_bps=fee_bps,
fee_bps=pool_fee_bps,
min_profit_usd=min_profit_wei,
)

# Ensure pool approvals
ensure_pool_approvals(pool.token_a, pool.token_b, pool, self.vault_addr)

self.executors.append(executor)
log.info(f"initialized {pool.token_a.symbol}-{pool.token_b.symbol} pool at {pool_addr}")
log.info(
f"initialized {pool.token_a.symbol}-{pool.token_b.symbol} pool at {pool_addr} "
f"(isStable={pool.is_stable_pool()}, source=on-chain BlockApps-Pool.isStable)"
)

# Pre-fetch prices for all tokens in all pools
all_token_symbols = set()
Expand Down
Loading