Bug fixes - CompositeEngine no longer misroutes bare Chinese-futures codes like RB2410 to GlobalFuturesEngine. _is_china_futures moved into a shared _market_hooks module with a case-normalized product table and a non-CN exchange guard. 9 new regression cases (RB2410 / rb2410 / RB2410.SHFE / CL.NYMEX / M2412.CBOT / CF2412.ICE / AU2412.COMEX / FG2412.EUREX / bare CN-collision still routes Chinese). - Session FTS5 indexes now persist timestamps so cross-session search can sort by date. Same path also fixes a re-upsert that was wall-clocking every session's started_at on title-rename. - Vite dev-mode proxy gained the missing /alpha entry; AlphaZoo page resolves on npm run dev. Cleanup - Deleted swarm/api_models.py + swarm/mailbox.py + SwarmDashboard.tsx (dead modules / orphaned UI after swarm-via-agent flow). - Deleted SessionService.resume_attempt + get_attempts + get_attempt + SessionStore.list_attempts + Attempt.mark_waiting_user chain (half-baked human-in-the-loop from initial commit). - Deleted ChatLLM.achat (async sibling never called) and _extract_balanced_json (only-tested helper). - Deleted american_exercise_value in options_portfolio.py (driver inlines bs_price logic). - Deleted _compute_ic_series_reference in factor_analysis_core.py (kept-as-reference, no callers). - Collapsed MEMORY_TYPES duplication between cli.py and memory.persistent; restored set-equality invariant assert. - Deleted runSwarm (~197 LOC), swarmDash state, swarmCancelRef write-only ref (orphaned by new agent-driven swarm flow). - Dropped 76 unused 'vw = vwap(...)' boilerplate lines across gtja191 alphas + their now-unused vwap imports. - Cleared 11 F401 unused-import sites in tools/, agent/, providers/. Test infrastructure - test_e2e_harness_v2.py (real-LLM e2e) gated behind VIBE_TRADING_RUN_LIVE_E2E so CI no longer flips shape based on env-key presence. CI workflow now has explicit --ignore. - test_auto_compact_with_long_history: monkeypatch TOKEN_THRESHOLD so the test deterministically triggers compact instead of relying on unrelated LLM behaviour. - test_compact_tool_explicit: assertion corrected — compact tool is special-cased in loop._process_tool_calls and does NOT emit tool_call events. Assert on the 'compact' event instead. - TestSwarmE2E: per-test @pytest.mark.timeout overrides matching the internal 300s poll deadlines; cancel test now polls for terminal state instead of fixed sleep(3). Tooling - ruff per-file-ignores for factors/zoo/** (3783 → 0 F401 noise); F841 stays active in zoo to catch real formula bugs. - frontend tsconfig: noUnusedLocals + noUnusedParameters as regression guards. - highlight.js hoisted to direct dependency (was transitive via rehype-highlight). - Vite proxy: dropped unused /run, /skills, /health, /api, /system; added /alpha. Annotations - direction parameter renamed to _direction in 6 calc_commission signatures (reserved for future borrow-fee asymmetry). - _MARGIN_PER_CONTRACT in global_futures.py annotated as reference data. - Backtest engines' _calc_pct_change variants cross-reference each other in docstrings. Docs - 2026-05-18 news entry added to README.md / README_zh.md / README_ja.md / README_ko.md / README_ar.md; 2026-05-15 entry rolled into the Earlier news <details>. - Contributors heading: v0.1.7 cycle → v0.1.8 cycle in all 5 READMEs. 126 files changed, +363 / -1276.
78 lines
3.0 KiB
Python
78 lines
3.0 KiB
Python
"""Crypto perpetual-contract backtest engine.
|
|
|
|
Market rules:
|
|
- 24/7 trading, no restrictions on direction
|
|
- Maker/Taker fee separation
|
|
- Funding fee settlement every 8 hours (00:00/08:00/16:00 UTC)
|
|
- Forced liquidation when maintenance margin ratio <= 100%
|
|
- Fractional position sizes allowed
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pandas as pd
|
|
|
|
from backtest.engines.base import BaseEngine
|
|
from backtest.engines._market_hooks import (
|
|
calc_crypto_funding_fee,
|
|
check_crypto_liquidation,
|
|
)
|
|
|
|
|
|
class CryptoEngine(BaseEngine):
|
|
"""Crypto perpetual contract engine.
|
|
|
|
Config keys:
|
|
- leverage: default 1.0
|
|
- maker_rate: default 0.0002
|
|
- taker_rate: default 0.0005
|
|
- slippage: default 0.0005
|
|
- margin_mode: "isolated" (default) or "cross"
|
|
- funding_rate: fixed rate per settlement, default 0.0001
|
|
"""
|
|
|
|
def __init__(self, config: dict):
|
|
super().__init__(config)
|
|
self.maker_rate: float = config.get("maker_rate", 0.0002)
|
|
self.taker_rate: float = config.get("taker_rate", 0.0005)
|
|
self.slippage_rate: float = config.get("slippage", 0.0005)
|
|
self.funding_rate: float = config.get("funding_rate", 0.0001)
|
|
self._funding_applied: set = set() # (symbol, date, hour) — per-slot dedup
|
|
self._funding_daily_done: set = set() # (symbol, date) — daily fallback dedup
|
|
|
|
def can_execute(self, symbol: str, direction: int, bar: pd.Series) -> bool:
|
|
"""Crypto: 24/7, long/short/close all allowed."""
|
|
return True
|
|
|
|
def round_size(self, raw_size: float, price: float) -> float:
|
|
"""Crypto supports fractional sizes, round to 6 decimals."""
|
|
return round(max(raw_size, 0.0), 6)
|
|
|
|
def calc_commission(self, size: float, price: float, _direction: int, is_open: bool) -> float:
|
|
"""Maker/Taker separated. Opens typically hit taker, closes hit maker.
|
|
|
|
``_direction`` is unused — reserved for future funding-rate asymmetry
|
|
between long/short legs on perp swaps.
|
|
"""
|
|
rate = self.taker_rate if is_open else self.maker_rate
|
|
return size * price * rate
|
|
|
|
def apply_slippage(self, price: float, direction: int) -> float:
|
|
"""Slippage: unfavourable direction."""
|
|
return price * (1 + direction * self.slippage_rate)
|
|
|
|
def on_bar(self, symbol: str, bar: pd.Series, timestamp: pd.Timestamp) -> None:
|
|
"""Crypto per-bar hooks: funding fee + liquidation check."""
|
|
fee = calc_crypto_funding_fee(
|
|
symbol, bar, timestamp, self.positions,
|
|
self.funding_rate, self._funding_applied, self._funding_daily_done,
|
|
)
|
|
self.capital -= fee
|
|
|
|
if check_crypto_liquidation(symbol, bar, self.positions):
|
|
pos = self.positions.get(symbol)
|
|
if pos is not None:
|
|
mark_price = float(bar.get("close", pos.entry_price))
|
|
liq_price = self.apply_slippage(mark_price, -pos.direction)
|
|
self._close_position(symbol, liq_price, timestamp, "liquidation")
|