206 lines
7.9 KiB
Python
206 lines
7.9 KiB
Python
"""
|
|
Compare two Kronos advisor configurations via backtest.
|
|
|
|
Config A: "conservative" — текущие параметры
|
|
- pause при range > 5%
|
|
- step = max(base*0.7, min(base*1.5, advice))
|
|
- center_offset clamp ±0.2%
|
|
|
|
Config B: "adjusted" — мягкие
|
|
- pause при range > 8% (или bias>0.7)
|
|
- step = max(base*0.85, min(base*1.3, advice)) # меньше снижение
|
|
- center_offset clamp ±0.15%
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import math
|
|
import sys
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def simulate_grid(df, start, horizon, step_pct, levels,
|
|
qty=0.001, center_offset_pct=0.0, fee_pct=0.001):
|
|
if start >= len(df) - 1:
|
|
return 0.0, 0, 0
|
|
center = float(df["close"].iloc[start]) * (1.0 + center_offset_pct)
|
|
step = center * step_pct
|
|
if step <= 0:
|
|
return 0.0, 0, 0
|
|
buy_levels = sorted([center - n * step for n in range(1, levels + 1) if center - n * step > 0])
|
|
sell_levels = sorted([center + n * step for n in range(1, levels + 1)])
|
|
end = min(start + horizon, len(df))
|
|
pnl, n_roundtrips, n_wins = 0.0, 0, 0
|
|
pos_open = None
|
|
for i in range(start + 1, end):
|
|
hi, lo = float(df["high"].iloc[i]), float(df["low"].iloc[i])
|
|
if pos_open is None:
|
|
for bl in buy_levels:
|
|
if lo <= bl:
|
|
pnl -= bl * qty * fee_pct
|
|
pos_open = bl
|
|
break
|
|
else:
|
|
for sl in sell_levels:
|
|
if hi >= sl:
|
|
gross = (sl - pos_open) * qty
|
|
fee = sl * qty * fee_pct + pos_open * qty * fee_pct
|
|
net = gross - fee
|
|
pnl += net
|
|
if net > 0:
|
|
n_wins += 1
|
|
n_roundtrips += 1
|
|
pos_open = None
|
|
break
|
|
pnl_pct = pnl / (center * qty) if center > 0 else 0.0
|
|
return pnl_pct, n_roundtrips, n_wins
|
|
|
|
|
|
@dataclass
|
|
class BTResult:
|
|
name: str
|
|
total_pnl_pct: float
|
|
n_trades: int
|
|
winrate: float
|
|
max_dd_pct: float
|
|
sharpe: float
|
|
|
|
|
|
def run_one(df, tf_min, lookback, pred_len, static_step_pct, levels,
|
|
advisor, cfg: dict, stride=12, regime_window_hours=720):
|
|
n = len(df)
|
|
pnls, ns, ws, regimes = [], [], [], []
|
|
pause_hits = 0
|
|
peak, eq, max_dd = 1.0, 1.0, 0.0
|
|
for t in range(lookback, n - pred_len, stride):
|
|
horizon = min(pred_len, stride * 2)
|
|
# Regime
|
|
win_start = max(0, t - regime_window_hours)
|
|
win = df["close"].iloc[win_start:t + 1]
|
|
ret = (win.iloc[-1] - win.iloc[0]) / win.iloc[0] if len(win) > 1 and win.iloc[0] > 0 else 0.0
|
|
regime = "trending" if abs(ret) > 0.05 else "sideways"
|
|
regimes.append(regime)
|
|
# PAUSE check
|
|
advice = advisor.advise(df.iloc[:t + 1], lookback=lookback, pred_len=pred_len, tf_min=tf_min)
|
|
if advice.pause_grid and advice.confidence >= cfg["pause_min_conf"]:
|
|
if advice.expected_range_pct > cfg["pause_range_thr"] or (
|
|
abs(advice.center_offset_pct) / max(advice.expected_range_pct, 1e-4) > cfg["pause_bias_thr"]
|
|
):
|
|
pause_hits += 1
|
|
pnls.append(0.0); ns.append(0); ws.append(0)
|
|
continue
|
|
# Step
|
|
kronos_step = float(advice.step_percent)
|
|
eff_step = max(static_step_pct * cfg["step_floor"],
|
|
min(static_step_pct * cfg["step_ceil"], kronos_step))
|
|
# Center
|
|
eff_off = max(-cfg["center_clamp"], min(cfg["center_clamp"], advice.center_offset_pct))
|
|
pnl_pct, nt, nw = simulate_grid(df, t, horizon, eff_step, levels,
|
|
center_offset_pct=eff_off)
|
|
pnls.append(pnl_pct); ns.append(nt); ws.append(nw)
|
|
eq *= (1.0 + pnl_pct)
|
|
peak = max(peak, eq)
|
|
max_dd = min(max_dd, (eq - peak) / peak)
|
|
if not pnls:
|
|
return BTResult("?", 0.0, 0, 0.0, 0.0, 0.0), []
|
|
total = float(np.prod(1.0 + np.array(pnls)) - 1.0)
|
|
nt, nw = sum(ns), sum(ws)
|
|
wr = nw / nt if nt else 0.0
|
|
sharpe = float(np.mean(pnls) / (np.std(pnls) + 1e-9) * math.sqrt(252 / max(1, len(pnls) // 24))) if len(pnls) > 1 else 0.0
|
|
return BTResult(cfg["name"], total, nt, wr, max_dd, sharpe), regimes
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--csv", required=True)
|
|
ap.add_argument("--tf", type=int, default=60)
|
|
ap.add_argument("--lookback", type=int, default=400)
|
|
ap.add_argument("--pred-len", type=int, default=24)
|
|
ap.add_argument("--step", type=float, default=0.005)
|
|
ap.add_argument("--levels", type=int, default=10)
|
|
ap.add_argument("--stride", type=int, default=12)
|
|
ap.add_argument("--model", default="kronos-mini")
|
|
ap.add_argument("--device", default="cpu")
|
|
args = ap.parse_args()
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
|
log = logging.getLogger("compare")
|
|
|
|
df = pd.read_csv(args.csv)
|
|
df.columns = [c.strip().lower() for c in df.columns]
|
|
if "timestamps" in df.columns:
|
|
df["timestamps"] = pd.to_datetime(df["timestamps"])
|
|
df = df.set_index("timestamps")
|
|
elif "timestamp" in df.columns:
|
|
df["timestamps"] = pd.to_datetime(df["timestamp"])
|
|
df = df.set_index("timestamps")
|
|
df = df[["open", "high", "low", "close", "volume"]].astype(float).sort_index()
|
|
log.info(f"Loaded {len(df)} candles")
|
|
|
|
# Static baseline (no advisor)
|
|
log.info("=== STATIC baseline (no advisor) ===")
|
|
static_pnls, static_n, static_w = [], [], []
|
|
peak, eq, max_dd = 1.0, 1.0, 0.0
|
|
n = len(df)
|
|
for t in range(args.lookback, n - args.pred_len, args.stride):
|
|
horizon = min(args.pred_len, args.stride * 2)
|
|
pnl, nt, nw = simulate_grid(df, t, horizon, args.step, args.levels)
|
|
static_pnls.append(pnl); static_n.append(nt); static_w.append(nw)
|
|
eq *= (1.0 + pnl)
|
|
peak = max(peak, eq)
|
|
max_dd = min(max_dd, (eq - peak) / peak)
|
|
s_total = float(np.prod(1.0 + np.array(static_pnls)) - 1.0)
|
|
s_sharpe = float(np.mean(static_pnls) / (np.std(static_pnls) + 1e-9) * math.sqrt(252 / max(1, len(static_pnls) // 24)))
|
|
static_res = BTResult("static", s_total, sum(static_n),
|
|
sum(static_w) / sum(static_n) if sum(static_n) else 0.0,
|
|
max_dd, s_sharpe)
|
|
|
|
from kronos import KronosAdvisor
|
|
advisor = KronosAdvisor(model_name=args.model, device=args.device)
|
|
log.info("Loaded advisor")
|
|
|
|
cfgs = [
|
|
{"name": "conservative",
|
|
"pause_min_conf": 0.6, "pause_range_thr": 0.05, "pause_bias_thr": 0.6,
|
|
"step_floor": 0.7, "step_ceil": 1.5, "center_clamp": 0.002},
|
|
{"name": "adjusted",
|
|
"pause_min_conf": 0.7, "pause_range_thr": 0.08, "pause_bias_thr": 0.75,
|
|
"step_floor": 0.85, "step_ceil": 1.3, "center_clamp": 0.0015},
|
|
{"name": "minimal",
|
|
"pause_min_conf": 0.85, "pause_range_thr": 0.10, "pause_bias_thr": 0.9,
|
|
"step_floor": 0.9, "step_ceil": 1.15, "center_clamp": 0.001},
|
|
]
|
|
results = [static_res]
|
|
for cfg in cfgs:
|
|
log.info(f"=== {cfg['name']} ===")
|
|
t0 = time.time()
|
|
r, regimes = run_one(df, args.tf, args.lookback, args.pred_len,
|
|
args.step, args.levels, advisor, cfg, args.stride)
|
|
log.info(f" {cfg['name']} done in {time.time()-t0:.1f}s")
|
|
# Per-regime breakdown
|
|
results.append(r)
|
|
|
|
print()
|
|
print("=" * 88)
|
|
print(f"{'MODE':<15} {'PnL %':>10} {'#Trades':>10} {'WinRate':>10} {'MaxDD %':>10} {'Sharpe':>10}")
|
|
print("-" * 88)
|
|
for r in results:
|
|
print(f"{r.name:<15} {r.total_pnl_pct*100:>9.2f}% {r.n_trades:>10d} "
|
|
f"{r.winrate*100:>9.1f}% {r.max_dd_pct*100:>9.2f}% {r.sharpe:>9.2f}")
|
|
print("=" * 88)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|