From c1cf90eb0c319e45cfbd22b3702b4170895419c5 Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 12:25:25 -0400 Subject: [PATCH 1/2] fix: thread mainnet through cfi_hedge and clean up strategy registry Route BTCSWP hedge legs to para:BTCSWP/BTCSWP-PARA on mainnet and YEX/OSRS on testnet via resolve_instrument; fix hedge_agent param name, funding_arb description, and registry count test. Co-authored-by: Cursor --- cli/commands/hedge.py | 32 +++++++++++---- cli/commands/run.py | 4 ++ cli/strategy_registry.py | 4 +- strategies/cfi_hedge.py | 69 ++++++++++++++++++++++++--------- strategies/cfi_hedge_agent.py | 20 +++++++--- tests/test_engine_strategies.py | 2 +- tests/test_hedge_margin_port.py | 2 +- 7 files changed, 98 insertions(+), 35 deletions(-) diff --git a/cli/commands/hedge.py b/cli/commands/hedge.py index b0e42f3..ffcfa63 100644 --- a/cli/commands/hedge.py +++ b/cli/commands/hedge.py @@ -104,7 +104,13 @@ def _position_to_summary(raw: dict, coin_override: Optional[str] = None): ) -def _build_proposal(hl, coin: str): +def _build_proposal( + hl, + coin: str, + *, + mainnet: bool = False, + hedge_instrument: Optional[str] = None, +): """Build a hedge proposal for the user's current `coin` perp position. Returns (proposal, snapshot) or raises typer.Exit if no position open. @@ -115,7 +121,11 @@ def _build_proposal(hl, coin: str): fetch_hl_current_funding_hr, ) - profile = get_cfi_profile(coin) + profile = get_cfi_profile( + coin, + mainnet=mainnet, + hedge_instrument=hedge_instrument, + ) if profile is None: typer.echo(f"Error: no deployed CFI v2 profile for coin '{coin}'", err=True) raise typer.Exit(2) @@ -146,6 +156,8 @@ def _build_proposal(hl, coin: str): position=position, current_funding_hr=current_funding, k_fixed_hr=snapshot.k_fixed_hr, + mainnet=mainnet, + hedge_instrument=hedge_instrument, ) return proposal, snapshot @@ -171,7 +183,7 @@ def propose_cmd( raw_hl = HLProxy(private_key=private_key, testnet=not mainnet) hl = DirectHLProxy(raw_hl) - proposal, snapshot = _build_proposal(hl, coin) + proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) typer.echo(hedge_proposal_block(proposal, snapshot, mainnet=mainnet)) @@ -202,7 +214,7 @@ def execute_cmd( raw_hl = HLProxy(private_key=private_key, testnet=not mainnet) hl = DirectHLProxy(raw_hl) - proposal, snapshot = _build_proposal(hl, coin) + proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) typer.echo(hedge_proposal_block(proposal, snapshot, mainnet=mainnet)) # Size the order in CFI v2 (BTCSWP) units. SDK rounds to szDecimals. @@ -306,7 +318,11 @@ def _refresh(): if h.get("status") != "active": live.append({"job": h, "snapshot": None, "drift_apy": 0.0, "savings_usd": h.get("cumulative_savings_usd", 0.0)}) continue - profile = get_cfi_profile(h.get("coin", "BTC")) + profile = get_cfi_profile( + h.get("coin", "BTC"), + mainnet=mainnet, + hedge_instrument=h.get("instrument"), + ) if profile is None: live.append({"job": h, "snapshot": None, "drift_apy": 0.0, "savings_usd": h.get("cumulative_savings_usd", 0.0)}) continue @@ -502,7 +518,7 @@ def auto_cmd( # Parse + validate the coin list. coin_tuple = tuple(c.strip().upper() for c in coins.split(",") if c.strip()) - unsupported = [c for c in coin_tuple if get_cfi_profile(c) is None] + unsupported = [c for c in coin_tuple if get_cfi_profile(c, mainnet=mainnet) is None] if unsupported: typer.echo( f"{RED}Error: no CFI v2 profile deployed for: {', '.join(unsupported)}{RESET}", @@ -595,7 +611,7 @@ def auto_cmd( coin=coin, perp_notional_usd=notional, active_hedge_coins=active_coins, - profile_vol_mult_l=get_cfi_profile(coin).vol_mult_l, + profile_vol_mult_l=get_cfi_profile(coin, mainnet=mainnet).vol_mult_l, policy=policy, daily=daily, now_ms=now_ms, @@ -616,7 +632,7 @@ def auto_cmd( # Real fire — build proposal + place order via existing helper. try: - proposal, snapshot = _build_proposal(hl, coin) + proposal, snapshot = _build_proposal(hl, coin, mainnet=mainnet) except typer.Exit: msg = f"build-proposal-failed for {coin}; skipping" append_audit_log(msg) diff --git a/cli/commands/run.py b/cli/commands/run.py index c8b6b90..dcd5f5e 100644 --- a/cli/commands/run.py +++ b/cli/commands/run.py @@ -126,6 +126,10 @@ def run_cmd( params = dict(cfg.strategy_params) if model: params["model"] = model + if cfg.strategy == "cfi_hedge": + params["mainnet"] = cfg.mainnet + if cfg.instrument.endswith(("-USDYP", "-PARA", "-OSRS")) or ":" in cfg.instrument: + params["hedge_instrument"] = cfg.instrument # Set up anomaly protection for YEX markets anomaly_thread = None diff --git a/cli/strategy_registry.py b/cli/strategy_registry.py index 27f45d1..66eeeed 100644 --- a/cli/strategy_registry.py +++ b/cli/strategy_registry.py @@ -22,7 +22,7 @@ "hedge_agent": { "path": "strategies.hedge_agent:HedgeAgent", "description": "inventory reducer (delta control)", - "params": {"notional_threshold": 15000.0}, + "params": {"inventory_threshold": 3.0}, }, "cfi_hedge": { "path": "strategies.cfi_hedge_agent:CfiHedgeAgent", @@ -56,7 +56,7 @@ }, "funding_arb": { "path": "strategies.funding_arb:FundingArbStrategy", - "description": "Cross-venue funding rate arbitrage — captures funding dislocations", + "description": "HL funding-rate bias MM — cross-venue arb not wired; HL-only", "params": {"divergence_threshold_bps": 2.0, "max_bias_bps": 5.0}, }, "regime_mm": { diff --git a/strategies/cfi_hedge.py b/strategies/cfi_hedge.py index e293072..1e42e3c 100644 --- a/strategies/cfi_hedge.py +++ b/strategies/cfi_hedge.py @@ -22,9 +22,11 @@ """ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from typing import Iterable, Optional +from common.models import BTCSWP_ASSET, asset_to_coin, asset_to_instrument, is_mainnet + HOURS_PER_YEAR = 8_760 @@ -58,18 +60,29 @@ class CFIAssetProfile: """YEX-local asset index. -1 = not deployed (sentinel).""" -BTCSWP_PROFILE = CFIAssetProfile( - name="BTC", - vol_mult_l=15, - fixed_leg_initial=0.0000029, - k2_beta=0.080042, - baseline_b0=75_000, - scale_s=1_000_000, - hl_coin="BTC", - cfi_asset_name="yex:BTCSWP", - cfi_instrument="BTCSWP-USDYP", - cfi_asset_index=2, -) +def btcswp_profile(mainnet: Optional[bool] = None) -> CFIAssetProfile: + """Network-aware BTCSWP CFI v2 profile. + + Testnet: yex:BTCSWP / BTCSWP-USDYP (YEX yield perp). + Mainnet: para:BTCSWP / BTCSWP-PARA (Paragon swap perp). + """ + on_mainnet = is_mainnet(mainnet) + return CFIAssetProfile( + name="BTC", + vol_mult_l=15, + fixed_leg_initial=0.0000029, + k2_beta=0.080042, + baseline_b0=75_000, + scale_s=1_000_000, + hl_coin="BTC", + cfi_asset_name=asset_to_coin(BTCSWP_ASSET, mainnet=mainnet), + cfi_instrument=asset_to_instrument(BTCSWP_ASSET, mainnet=mainnet), + cfi_asset_index=2 if not on_mainnet else -1, + ) + + +# Back-compat alias — testnet profile (YEX). +BTCSWP_PROFILE = btcswp_profile(mainnet=False) # ETHSWP placeholder — not yet deployed. Filled-in `vol_mult_l` etc. match # ~/hyperliquid-funding-rate-perps/tools/hedge_calculator.py. @@ -87,14 +100,28 @@ class CFIAssetProfile: ) CFI_PROFILES = { - "BTC": BTCSWP_PROFILE, "ETH": ETHSWP_PROFILE, } -def get_cfi_profile(coin: str) -> Optional[CFIAssetProfile]: +def get_cfi_profile( + coin: str, + mainnet: Optional[bool] = None, + hedge_instrument: Optional[str] = None, +) -> Optional[CFIAssetProfile]: """Look up a deployed profile by coin ticker. Returns None for unknowns.""" - return CFI_PROFILES.get(coin.upper()) + upper = coin.upper() + if upper in ("BTC", BTCSWP_ASSET): + base = btcswp_profile(mainnet=mainnet) + if hedge_instrument is None: + return base + from cli.strategy_registry import resolve_instrument + from common.models import instrument_to_coin + + resolved = resolve_instrument(hedge_instrument, mainnet=mainnet) + cfi_coin = instrument_to_coin(resolved, mainnet=mainnet) + return replace(base, cfi_asset_name=cfi_coin, cfi_instrument=resolved) + return CFI_PROFILES.get(upper) # ─── Rate conversions ─────────────────────────────────────────────────────── @@ -229,12 +256,18 @@ def build_cfi_hedge_proposal( k_fixed_hr: float, horizons: Optional[Iterable[tuple]] = None, now_ms: Optional[int] = None, + mainnet: Optional[bool] = None, + hedge_instrument: Optional[str] = None, ) -> Optional[CFIHedgeProposal]: """Build a CFI v2 hedge proposal for an existing HL perp position. Returns None if no CFI profile is deployed for the position's coin. """ - profile = get_cfi_profile(position.coin) + profile = get_cfi_profile( + position.coin, + mainnet=mainnet, + hedge_instrument=hedge_instrument, + ) if profile is None: return None @@ -256,7 +289,7 @@ def build_cfi_hedge_proposal( role="existing", ) hedge_leg = CFIHedgeLeg( - venue="YEX-HIP3", + venue="PARA-HIP3" if is_mainnet(mainnet) else "YEX-HIP3", # CFI v2 long pays you (funding − K2) per unit time. Same-side hedge # by the identity derivation: existing long → CFI long; existing # short → CFI short. diff --git a/strategies/cfi_hedge_agent.py b/strategies/cfi_hedge_agent.py index 806da2d..cbfcd90 100644 --- a/strategies/cfi_hedge_agent.py +++ b/strategies/cfi_hedge_agent.py @@ -6,7 +6,7 @@ strategy loop alongside the inventory-control `HedgeAgent`. This is the canonical "hedge" strategy: it neutralises funding-rate cost -on an existing perp position by opening a 1/L CFI v2 (yex:{COIN}SWP) leg. +on an existing perp position by opening a 1/L CFI v2 ({yex|para}:{COIN}SWP) leg. Contrast with `strategies/hedge_agent.py`, which is a delta/inventory reducer. @@ -23,7 +23,7 @@ import time from typing import List, Optional -from common.models import MarketSnapshot, StrategyDecision, instrument_to_coin +from common.models import MarketSnapshot, StrategyDecision, instrument_to_coin, is_mainnet from sdk.strategy_sdk.base import BaseStrategy, StrategyContext from strategies.cfi_hedge import ( @@ -48,7 +48,7 @@ class CfiHedgeAgent(BaseStrategy): 3. Run the pure `compute_hedge_open_action` gate (trigger + caps + interval). 4. If it fires, size the CFI v2 leg via `build_cfi_hedge_proposal` (1/L ratio) and emit a `place_order` StrategyDecision on the - yex:{COIN}SWP instrument. + network-appropriate CFI v2 instrument (yex:BTCSWP testnet, para:BTCSWP mainnet). State (daily action counter / last-action timestamp) is held in-memory for the life of the strategy instance — the standalone `hl hedge auto` @@ -62,12 +62,16 @@ def __init__( max_hedge_notional: float = 50_000.0, max_per_day: int = 5, min_interval_seconds: int = 300, + mainnet: Optional[bool] = None, + hedge_instrument: Optional[str] = None, ): super().__init__(strategy_id=strategy_id) self.notional_trigger = notional_trigger self.max_hedge_notional = max_hedge_notional self.max_per_day = max_per_day self.min_interval_seconds = min_interval_seconds + self.mainnet = is_mainnet(mainnet) + self.hedge_instrument = hedge_instrument # In-memory daily counters (CLI verb owns the disk-backed copy). self._daily = DailyHedgeState.fresh(today_utc_iso()) # Coins already hedged in this strategy session. @@ -83,11 +87,15 @@ def on_tick( # Normalise the snapshot instrument to a bare ticker (BTC-PERP → BTC, # yex:BTCSWP → BTCSWP) so it keys into the deployed CFI v2 profiles. - coin = instrument_to_coin(snapshot.instrument) + coin = instrument_to_coin(snapshot.instrument, mainnet=self.mainnet) if ":" in coin: coin = coin.split(":", 1)[1] coin = coin.upper() - profile = get_cfi_profile(coin) + profile = get_cfi_profile( + coin, + mainnet=self.mainnet, + hedge_instrument=self.hedge_instrument, + ) if profile is None: return [] @@ -170,6 +178,8 @@ def _action_to_decision( position=position, current_funding_hr=snapshot.funding_rate, k_fixed_hr=profile.fixed_leg_initial, + mainnet=self.mainnet, + hedge_instrument=self.hedge_instrument, ) if proposal is None: return None diff --git a/tests/test_engine_strategies.py b/tests/test_engine_strategies.py index 64a6279..bc81f3a 100644 --- a/tests/test_engine_strategies.py +++ b/tests/test_engine_strategies.py @@ -259,7 +259,7 @@ def test_all_engine_strategies_registered(self): def test_total_strategies(self): from cli.strategy_registry import STRATEGY_REGISTRY - assert len(STRATEGY_REGISTRY) == 18 # 14 original + 4 directional + assert len(STRATEGY_REGISTRY) == 19 # 14 original + 4 directional + cfi_hedge def test_resolve_engine_strategies(self): from cli.strategy_registry import resolve_strategy_path diff --git a/tests/test_hedge_margin_port.py b/tests/test_hedge_margin_port.py index 0aa03d9..841195c 100644 --- a/tests/test_hedge_margin_port.py +++ b/tests/test_hedge_margin_port.py @@ -243,7 +243,7 @@ def fail_persist(hedges): monkeypatch.setattr(cfgmod.TradingConfig, "get_private_key", lambda self: "0x" + "1" * 64) monkeypatch.setattr(proxy_mod, "HLProxy", lambda private_key, testnet: object()) monkeypatch.setattr(adapter_mod, "DirectHLProxy", FakeDirectHLProxy) - monkeypatch.setattr(hedge_cmd, "_build_proposal", lambda hl, coin: (proposal, snapshot)) + monkeypatch.setattr(hedge_cmd, "_build_proposal", lambda hl, coin, **kwargs: (proposal, snapshot)) monkeypatch.setattr("cli.hedge_display.hedge_proposal_block", lambda proposal, snapshot, mainnet=False: "proposal") monkeypatch.setattr(hedge_cmd, "_save_hedges", fail_persist) From 1cb89c1d9842f06e0df01e0f99a0d62a71f916cb Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 12:27:41 -0400 Subject: [PATCH 2/2] test: add cfi_hedge mainnet coverage and doc honesty fixes Add para:BTCSWP/BTCSWP-PARA assertions for mainnet hedge paths, align README/skill hedge_agent and funding_arb descriptions with registry. Co-authored-by: Cursor --- README.md | 4 ++-- cli/skill.md | 2 +- strategies/funding_arb.py | 3 +++ tests/test_hedge_margin_port.py | 34 +++++++++++++++++++++++++++++++++ tests/test_strategy_registry.py | 3 +++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7b8e6f1..f1d1a3b 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Exploit pricing dislocations across venues, instruments, or time horizons. | Strategy | Description | Key Parameters | When to Use | |----------|-------------|----------------|-------------| -| `funding_arb` | Cross-venue funding rate arbitrage — captures funding divergence between HL and external venues. Quoting-engine powered with bias from funding delta. *Requires `quoting_engine` module.* | `divergence_threshold_bps`, `max_bias_bps` | When funding rates diverge between venues. Works well on high-funding instruments. | +| `funding_arb` | Funding-biased MM on HL funding rate — cross-venue feeds not wired; HL-only bias from funding delta. *Requires `quoting_engine` module.* | `divergence_threshold_bps`, `max_bias_bps` | When HL funding is elevated and you want quote bias toward collecting premium. | | `basis_arb` | Trades implied basis from funding rate — enters when annualized basis (contango/backwardation) exceeds threshold. | `basis_threshold_bps`, `size` | Capturing contango/backwardation dislocations. Pairs well with funding_arb. | ### Signal / Directional @@ -188,7 +188,7 @@ Supporting strategies for portfolio management, block liquidity, and autonomous | Strategy | Description | Key Parameters | When to Use | |----------|-------------|----------------|-------------| -| `hedge_agent` | Reduces excess exposure per deterministic mandate. Fires when net notional exceeds threshold. | `notional_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. | +| `hedge_agent` | Reduces excess inventory per deterministic mandate. Fires when absolute position qty exceeds threshold. | `inventory_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. | | `rfq_agent` | Block-size dark RFQ liquidity — quotes for large orders with wider spreads. | `min_size`, `spread_bps` | Institutional/block flow. Provides hidden liquidity for large counterparties. | | `claude_agent` | Multi-model LLM trading agent. Sends market snapshot to an LLM (Gemini, Claude, or OpenAI), receives structured trade decisions. | `model`, `base_size` | **Experimental.** Autonomous decision-making using LLM reasoning. | diff --git a/cli/skill.md b/cli/skill.md index 9cd0388..d227efa 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -198,7 +198,7 @@ Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_chec | regime_mm | MM | Vol-regime adaptive — switches behavior by volatility regime (calm/normal/volatile/extreme) | | grid_mm | MM | Fixed-interval grid levels above and below mid | | liquidation_mm | MM | Provides liquidity during cascade/liquidation events | -| funding_arb | Arb | Cross-venue funding rate arbitrage | +| funding_arb | Arb | HL funding-biased MM (cross-venue feeds not wired) | | basis_arb | Arb | Trades implied basis from funding rate (contango/backwardation) | | mean_reversion | Signal | Trades when price deviates from SMA | | momentum_breakout | Signal | Enters on volume + price breakout above/below N-period range | diff --git a/strategies/funding_arb.py b/strategies/funding_arb.py index 4250e72..e03e467 100644 --- a/strategies/funding_arb.py +++ b/strategies/funding_arb.py @@ -4,6 +4,9 @@ When HL funding diverges from the cross-venue median, biases quotes to collect the premium. Especially valuable for YEX yield perps where funding IS the product. + +Note: external venue feeds (CrossVenueFundingRate) are not wired in +production — today this strategy biases quotes from HL funding only. """ from __future__ import annotations diff --git a/tests/test_hedge_margin_port.py b/tests/test_hedge_margin_port.py index 841195c..8815bed 100644 --- a/tests/test_hedge_margin_port.py +++ b/tests/test_hedge_margin_port.py @@ -27,6 +27,7 @@ from strategies.cfi_hedge import ( BTCSWP_PROFILE, HLPositionSummary, + btcswp_profile, build_cfi_hedge_proposal, ) from strategies.cfi_hedge_agent import CfiHedgeAgent @@ -83,6 +84,30 @@ def test_cfi_hedge_sizing_is_notional_over_L(): assert prop.legs[1].side == "long" +def test_cfi_hedge_mainnet_uses_para_btcswp(): + notional = 150_000.0 + prop = build_cfi_hedge_proposal( + user_address="t", + position=_btc_position(notional), + current_funding_hr=0.00002, + k_fixed_hr=0.0000029, + mainnet=True, + ) + assert prop is not None + assert prop.legs[1].market == "para:BTCSWP" + assert prop.profile.cfi_instrument == "BTCSWP-PARA" + assert prop.legs[1].venue == "PARA-HIP3" + + +def test_btcswp_profile_network_defaults(): + testnet = btcswp_profile(mainnet=False) + mainnet = btcswp_profile(mainnet=True) + assert testnet.cfi_asset_name == "yex:BTCSWP" + assert testnet.cfi_instrument == "BTCSWP-USDYP" + assert mainnet.cfi_asset_name == "para:BTCSWP" + assert mainnet.cfi_instrument == "BTCSWP-PARA" + + def test_cfi_hedge_unknown_coin_returns_none(): pos = HLPositionSummary( coin="DOGE", side="long", size_coin=1.0, entry_px=0.1, mark_px=0.1, @@ -190,6 +215,15 @@ def test_agent_emits_cfi_leg_at_one_over_L(): assert abs(d.size - 10_000.0 / BTCSWP_PROFILE.baseline_b0) < 1e-6 +def test_agent_mainnet_emits_para_btcswp(): + agent = CfiHedgeAgent(mainnet=True) + snap = _btc_snap() + ctx = StrategyContext(snapshot=snap, position_qty=2.0, position_notional=150_000.0) + decisions = agent.on_tick(snap, ctx) + assert len(decisions) == 1 + assert decisions[0].instrument == "BTCSWP-PARA" + + def test_agent_dedupes_and_respects_trigger(): agent = CfiHedgeAgent() snap = _btc_snap() diff --git a/tests/test_strategy_registry.py b/tests/test_strategy_registry.py index f534946..9b64de0 100644 --- a/tests/test_strategy_registry.py +++ b/tests/test_strategy_registry.py @@ -40,6 +40,9 @@ def test_registry_has_params(self): assert "path" in entry assert "description" in entry + def test_hedge_agent_param_matches_class(self): + assert STRATEGY_REGISTRY["hedge_agent"]["params"] == {"inventory_threshold": 3.0} + class TestResolveInstrument: def test_standard_perp_unchanged(self):