b6539802e0
- mv src/multi_swarm → src/multi_swarm_core/multi_swarm_core (member layout) - sed-replace globale degli import: from/import multi_swarm.* → multi_swarm_core.* - 115 occorrenze aggiornate in src/ scripts/ tests/ - multi_swarm_coevolutive (nome repo) preservato dal word boundary Pre-condizione per il setup uv workspace della Fase 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
128 lines
4.8 KiB
Python
128 lines
4.8 KiB
Python
"""Replay diagnostico: per ciascuna strategia conta quanti bar avrebbero
|
|
soddisfatto le condizioni di ciascuna regola sull'ultimo `--days` di storico.
|
|
|
|
Ouput tabellare per branch: total_bars, fires, fire_rate, primo/ultimo fire.
|
|
Esegue anche un backtest grezzo (entry-on-signal, exit-on-flat) per stimare
|
|
n_trades e total_return realistici nel periodo.
|
|
|
|
Esempio:
|
|
docker compose exec multi-swarm-paper \
|
|
python /app/scripts/replay_strategies_window.py --days 30
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from datetime import UTC, datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
|
|
from multi_swarm_core.cerbero.client import CerberoClient
|
|
from multi_swarm_core.config import load_settings
|
|
from multi_swarm_core.data.cerbero_ohlcv import CerberoOHLCVLoader, OHLCVRequest
|
|
from multi_swarm_core.protocol.compiler import _eval_node, compile_strategy
|
|
from multi_swarm_core.protocol.parser import parse_strategy
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
p = argparse.ArgumentParser()
|
|
p.add_argument("--days", type=int, default=30)
|
|
p.add_argument("--strategies-dir", default=str(PROJECT_ROOT / "strategies"))
|
|
return p.parse_args()
|
|
|
|
|
|
def fetch_window(loader: CerberoOHLCVLoader, symbol: str, days: int) -> pd.DataFrame:
|
|
end = datetime.now(UTC).replace(minute=0, second=0, microsecond=0)
|
|
start = end - timedelta(days=days)
|
|
req = OHLCVRequest(
|
|
symbol=symbol, timeframe="1h", start=start, end=end, exchange="deribit"
|
|
)
|
|
return loader._fetch(req) # noqa: SLF001 — bypass cache
|
|
|
|
|
|
def per_branch_fires(strategy_path: Path, ohlcv: pd.DataFrame) -> list[dict]:
|
|
raw = strategy_path.read_text()
|
|
parsed = parse_strategy(raw)
|
|
out = []
|
|
for idx, rule in enumerate(parsed.rules):
|
|
cond_series = _eval_node(rule.condition, ohlcv).fillna(False).astype(bool)
|
|
n = int(cond_series.sum())
|
|
first = ohlcv.index[cond_series.argmax()] if n > 0 else None
|
|
# last fire: argmax on reversed
|
|
last = ohlcv.index[len(cond_series) - 1 - cond_series[::-1].argmax()] if n > 0 else None
|
|
out.append({
|
|
"branch_idx": idx,
|
|
"action": rule.action,
|
|
"fires": n,
|
|
"fire_rate_pct": round(100.0 * n / len(ohlcv), 2),
|
|
"first_fire": first,
|
|
"last_fire": last,
|
|
})
|
|
return out
|
|
|
|
|
|
def quick_pnl(strategy_path: Path, ohlcv: pd.DataFrame, fees_bp: float = 5.0) -> dict:
|
|
"""Approx: at each bar evaluate compiled signal series (long/short/flat),
|
|
apply position to next-bar return, charge fees on changes. No leverage."""
|
|
raw = strategy_path.read_text()
|
|
parsed = parse_strategy(raw)
|
|
sig_fn = compile_strategy(parsed)
|
|
signals = sig_fn(ohlcv) # series of "long"/"short"/"flat"
|
|
# map to position: long=+1, short=-1, flat=0
|
|
pos = signals.map({"long": 1, "short": -1, "flat": 0}).fillna(0).astype(int)
|
|
rets = ohlcv["close"].pct_change().fillna(0.0)
|
|
# next-bar execution: position decided at bar t applies to return t+1 -> shift
|
|
pnl = pos.shift(1).fillna(0) * rets
|
|
# fees on position changes
|
|
changes = pos.diff().abs().fillna(0).astype(int)
|
|
fee_per_change = fees_bp / 10_000.0
|
|
pnl_after_fees = pnl - changes * fee_per_change
|
|
cum = (1 + pnl_after_fees).prod() - 1
|
|
n_trades = int((changes > 0).sum())
|
|
time_in_market = float((pos != 0).mean())
|
|
return {
|
|
"n_trades": n_trades,
|
|
"total_return_pct": round(100.0 * float(cum), 3),
|
|
"time_in_market_pct": round(100.0 * time_in_market, 2),
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
settings = load_settings()
|
|
token = (
|
|
settings.cerbero_mainnet_token.get_secret_value()
|
|
if settings.cerbero_mainnet_token
|
|
else settings.cerbero_testnet_token.get_secret_value()
|
|
)
|
|
cerbero = CerberoClient(
|
|
base_url=settings.cerbero_base_url,
|
|
token=token,
|
|
bot_tag=settings.cerbero_bot_tag,
|
|
)
|
|
loader = CerberoOHLCVLoader(client=cerbero, cache_dir=settings.series_dir)
|
|
|
|
strategies_dir = Path(args.strategies_dir)
|
|
pairs = [
|
|
("BTC-PERPETUAL", sorted(strategies_dir.glob("btc_*.json"))[0]),
|
|
("ETH-PERPETUAL", sorted(strategies_dir.glob("eth_*.json"))[0]),
|
|
]
|
|
|
|
for symbol, strat_path in pairs:
|
|
print(f"\n=== {symbol} strategy={strat_path.name} window={args.days}d ===")
|
|
ohlcv = fetch_window(loader, symbol, args.days)
|
|
print(f"bars: {len(ohlcv)} range: {ohlcv.index[0]} -> {ohlcv.index[-1]}")
|
|
print("\n-- per branch --")
|
|
for row in per_branch_fires(strat_path, ohlcv):
|
|
print(json.dumps(row, default=str))
|
|
print("\n-- quick pnl (next-bar exec, fees=5bp) --")
|
|
print(json.dumps(quick_pnl(strat_path, ohlcv), default=str))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|