3073415d33
- strategies/funding_arb_strategy.py: full backtestable funding rate carry module with entry/exit thresholds, position tracking, funding payment accounting, basis stop-loss, max-hold timeout. Includes backtest_funding_arb() and run_funding_discovery() for threshold optimization - live/node_v2.py: replaced naive random fills with QueueAwareFillModel (sim/fills.py) with queue-priority simulation; integrated WQI predictor and funding arb strategies; per-coin WQI signal generation every 3 ticks; funding arb metrics in dashboard - cli.py: added 'funding' command for funding rate distribution analysis and threshold backtesting - tests/test_funding_arb.py: 20 tests covering entry/exit logic, fee accounting, signal generation, backtesting, and node integration 321 tests passing (20 new).
206 lines
7.3 KiB
Python
206 lines
7.3 KiB
Python
"""
|
|
Tests for funding arb strategy and WQI predictor integration.
|
|
"""
|
|
import math
|
|
|
|
|
|
class TestFundingArb:
|
|
def test_no_entry_below_threshold(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30)
|
|
trade = arb.tick(0.10, 100000.0, 0.0)
|
|
assert trade is None
|
|
assert arb.position == 0
|
|
|
|
def test_entry_above_threshold_positive(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30)
|
|
trade = arb.tick(0.50, 100000.0, 0.0)
|
|
assert trade is not None
|
|
assert trade["action"] == "SELL"
|
|
assert arb.position == -1
|
|
|
|
def test_entry_above_threshold_negative(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30)
|
|
trade = arb.tick(-0.50, 100000.0, 0.0)
|
|
assert trade is not None
|
|
assert trade["action"] == "BUY"
|
|
assert arb.position == 1
|
|
|
|
def test_exit_when_apr_fades(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30, apr_exit=0.10)
|
|
arb.tick(0.50, 100000.0, 0.0)
|
|
trade = arb.tick(0.05, 100000.0, 3600.0)
|
|
assert trade is not None
|
|
assert "EXIT" in trade["action"]
|
|
assert arb.position == 0
|
|
|
|
def test_exit_when_funding_flips(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30, apr_exit=0.10)
|
|
arb.tick(0.50, 100000.0, 0.0)
|
|
trade = arb.tick(-0.10, 100000.0, 3600.0)
|
|
assert trade is not None
|
|
assert "EXIT" in trade["action"]
|
|
assert arb.position == 0
|
|
|
|
def test_signal_no_exit_when_apr_still_high(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30)
|
|
arb.tick(0.50, 100000.0, 0.0)
|
|
result = arb.signal(0.60, 100000.0)
|
|
assert result["action"] == "HOLD"
|
|
|
|
def test_signal_hold_when_below_threshold(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30)
|
|
result = arb.signal(0.05)
|
|
assert result["action"] == "HOLD"
|
|
|
|
def test_summary_no_trades(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb()
|
|
s = arb.summary()
|
|
assert s["total_trades"] == 0
|
|
assert s["win_rate"] == 0.0
|
|
|
|
def test_summary_with_trades(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30, apr_exit=0.10)
|
|
arb.tick(0.50, 100000.0, 0.0)
|
|
arb.tick(0.05, 100100.0, 3600.0)
|
|
arb.tick(0.50, 100000.0, 7200.0)
|
|
arb.tick(0.05, 100050.0, 10800.0)
|
|
s = arb.summary()
|
|
assert s["total_trades"] == 2
|
|
assert s["position"] == 0
|
|
|
|
def test_reset(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.30, apr_exit=0.10)
|
|
arb.tick(0.50, 100000.0, 0.0)
|
|
arb.reset()
|
|
assert arb.position == 0
|
|
assert len(arb.trades) == 0
|
|
|
|
def test_backtest_empty(self):
|
|
from strategies.funding_arb_strategy import backtest_funding_arb
|
|
result = backtest_funding_arb([], [])
|
|
assert result["total_trades"] == 0
|
|
|
|
def test_backtest_single_trade(self):
|
|
from strategies.funding_arb_strategy import backtest_funding_arb
|
|
rates = [0.50, 0.06]
|
|
prices = [100000.0, 100000.0]
|
|
result = backtest_funding_arb(rates, prices, apr_threshold=0.30)
|
|
assert result["total_trades"] == 1
|
|
|
|
def test_fee_accounting(self):
|
|
from strategies.funding_arb_strategy import FundingArb
|
|
arb = FundingArb(apr_threshold=0.10, size=0.001, taker_fee_pct=0.00045)
|
|
trade = arb.tick(0.50, 100000.0, 0.0)
|
|
assert trade is not None
|
|
expected_fee = 0.001 * 100000.0 * 0.00045
|
|
assert abs(trade["fee"] - expected_fee) < 0.001
|
|
|
|
|
|
class TestFundingDiscoveryCLI:
|
|
def test_discovery_no_data(self):
|
|
from strategies.funding_arb_strategy import run_funding_discovery
|
|
result = run_funding_discovery(
|
|
data_dir="/tmp/nonexistent_data",
|
|
coin="BTC",
|
|
)
|
|
assert "error" in result
|
|
|
|
def test_backtest_multiple_thresholds(self):
|
|
from strategies.funding_arb_strategy import backtest_funding_arb
|
|
import random
|
|
random.seed(42)
|
|
rates = [abs(random.gauss(0, 0.5)) for _ in range(200)]
|
|
prices = [100000.0 + random.gauss(0, 500) for _ in range(200)]
|
|
|
|
for threshold in [0.10, 0.30, 0.50]:
|
|
result = backtest_funding_arb(rates, prices, apr_threshold=threshold)
|
|
assert "total_trades" in result
|
|
assert "total_net_pnl" in result
|
|
|
|
|
|
class TestWQIIntegration:
|
|
def test_wqi_with_node_interface(self):
|
|
from strategies.wqi_predictor import WQIPredictor
|
|
wqi = WQIPredictor(z_entry=2.0, max_hold_seconds=30)
|
|
bids = [(50000.0, 1.0), (49999.0, 0.5)]
|
|
asks = [(50002.0, 1.0), (50003.0, 0.5)]
|
|
|
|
for _ in range(30):
|
|
wqi.feed_signal(bids, asks, 50001.0)
|
|
|
|
extreme_bids = [(50000.0, 10.0), (49999.0, 5.0)]
|
|
extreme_asks = [(50002.0, 0.5)]
|
|
signal = wqi.feed_signal(extreme_bids, extreme_asks, 50001.0)
|
|
|
|
assert signal["action"] in ("BUY", "HOLD")
|
|
if signal["action"] == "BUY":
|
|
assert wqi.position != 0
|
|
|
|
def test_wqi_exit_on_timeout(self):
|
|
from strategies.wqi_predictor import WQIPredictor
|
|
import time
|
|
wqi = WQIPredictor(z_entry=0.01, z_exit=999.0, wqi_threshold=0.01,
|
|
max_hold_seconds=0.001, max_adverse=999.0)
|
|
|
|
for _ in range(30):
|
|
wqi.feed_signal([(100.0, 1.0)], [(102.0, 1.0)], 101.0)
|
|
|
|
extreme_bids = [(100.0, 20.0), (99.0, 10.0)]
|
|
extreme_asks = [(102.0, 1.0)]
|
|
signal = wqi.feed_signal(extreme_bids, extreme_asks, 101.0)
|
|
|
|
if signal["action"] in ("BUY", "SELL"):
|
|
time.sleep(0.01)
|
|
signal2 = wqi.feed_signal(extreme_bids, extreme_asks, 101.0)
|
|
assert signal2["action"] in ("EXIT", "HOLD")
|
|
|
|
|
|
class TestNodeV2Strategies:
|
|
def test_node_creates_all_strategies(self):
|
|
from live.node_v2 import ProductionNode
|
|
node = ProductionNode(
|
|
coins=["BTC"],
|
|
testnet=True,
|
|
mode="paper",
|
|
max_position_per_coin=0.001,
|
|
base_quote_size=0.0001,
|
|
)
|
|
assert len(node._wqi_predictors) == 1
|
|
assert node._funding_arb is not None
|
|
assert node._fill_model is not None
|
|
|
|
def test_wqi_not_none(self):
|
|
from live.node_v2 import ProductionNode
|
|
node = ProductionNode(
|
|
coins=["BTC"],
|
|
testnet=True,
|
|
mode="paper",
|
|
max_position_per_coin=0.001,
|
|
)
|
|
wqi = node._wqi_predictors.get("BTC")
|
|
assert wqi is not None
|
|
assert wqi._z_entry == 2.0
|
|
assert wqi._max_hold_seconds == 30
|
|
|
|
def test_funding_arb_config(self):
|
|
from live.node_v2 import ProductionNode
|
|
node = ProductionNode(
|
|
coins=["BTC"],
|
|
testnet=True,
|
|
mode="paper",
|
|
max_position_per_coin=0.001,
|
|
)
|
|
arb = node._funding_arb
|
|
assert arb._apr_threshold == 0.30
|
|
assert arb._apr_exit == 0.10
|