#!/usr/bin/env python3 """离线网格:对比 Wyckoff 多周期组合(不依赖 Binance API)。""" from __future__ import annotations import json import logging import re import sys from pathlib import Path from typing import Any, Optional ROOT = Path(__file__).resolve().parents[3] sys.path.insert(0, str(ROOT)) STRAT_PATH = ROOT / "user_data/Chan/strategies/Wyckoff_BTC.py" CONFIG_PATH = ROOT / "user_data/Chan/config/Wyckoff_BTC.json" COMBOS = [ ("1h_4h_noBias", "1h", "4h", None), ("1h_4h_8h", "1h", "4h", "8h"), ("1h_8h_noBias", "1h", "8h", None), ("30m_4h_8h", "30m", "4h", "8h"), ("30m_4h_noBias", "30m", "4h", None), ("15m_1h_4h", "15m", "1h", "4h"), ("15m_4h_8h", "15m", "4h", "8h"), ("4h_8h_noBias", "4h", "8h", None), ] def stub_market(symbol: str = "BTC/USDT:USDT") -> dict[str, Any]: base = symbol.split("/")[0] return { "id": symbol, "symbol": symbol, "base": base, "quote": "USDT", "settle": "USDT", "baseId": base, "quoteId": "USDT", "settleId": "USDT", "type": "swap", "spot": False, "swap": True, "future": False, "option": False, "active": True, "contract": True, "linear": True, "inverse": False, "contractSize": 1.0, "precision": {"amount": 0.001, "price": 0.1}, "limits": { "amount": {"min": 0.001, "max": 1000.0}, "price": {"min": 0.1, "max": None}, "cost": {"min": 5.0, "max": None}, "leverage": {"min": 1.0, "max": 125.0}, }, "percentage": True, "taker": 0.0005, "maker": 0.0002, "info": {}, } def install_offline_markets(pairs: Optional[list[str]] = None) -> None: import ccxt import freqtrade.exchange.exchange as exmod from freqtrade.util import dt_ts if pairs is None: pairs = ["BTC/USDT:USDT"] markets = {p: stub_market(p) for p in pairs} tiers = { p: [ { "minNotional": 0, "maxNotional": 1e12, "maintenanceMarginRate": 0.005, "maxLeverage": 125, "info": {}, } ] for p in pairs } def fake_reload(self, force: bool = False, *, load_leverage_tiers: bool = True) -> None: self._markets = markets try: self._api.precisionMode = ccxt.TICK_SIZE self._api_async.precisionMode = ccxt.TICK_SIZE except Exception: pass try: self._api.set_markets(markets) except Exception: pass try: self._api_async.set_markets(markets) except Exception: pass self._last_markets_refresh = dt_ts() self._leverage_tiers = tiers self._trading_fees = {} exmod.Exchange.reload_markets = fake_reload # type: ignore exmod.Exchange.fills_leverage_tiers = lambda self: setattr(self, "_leverage_tiers", tiers) # type: ignore def patch_strategy(exec_tf: str, structure_tf: str, bias_tf: Optional[str]) -> None: text = STRAT_PATH.read_text() bias_repr = "None" if bias_tf is None else f'"{bias_tf}"' text = re.sub(r'^(\ttimeframe = ).*$', rf'\g<1>"{exec_tf}"', text, count=1, flags=re.M) text = re.sub( r'^(\tstructure_timeframe = ).*$', rf'\g<1>"{structure_tf}"', text, count=1, flags=re.M ) text = re.sub( r'^(\tbias_timeframe: Optional\[str\] = ).*$', rf'\g<1>{bias_repr}', text, count=1, flags=re.M, ) startup = 220 if exec_tf in ("1h", "4h", "8h") else 400 text = re.sub( r'^(\tstartup_candle_count = ).*$', rf'\g<1>{startup}', text, count=1, flags=re.M ) STRAT_PATH.write_text(text) def run_one(exec_tf: str, timerange: str) -> dict[str, Any]: from freqtrade.configuration import Configuration from freqtrade.enums import RunMode from freqtrade.optimize.backtesting import Backtesting import freqtrade.optimize.optimize_reports.bt_output as bt_output # 静默打印 bt_output.show_backtest_results = lambda *a, **k: None # type: ignore bt_output.show_backtest_result = lambda *a, **k: None # type: ignore for mod in list(sys.modules): if "Wyckoff_BTC" in mod or mod.endswith("Wyckoff_BTC"): del sys.modules[mod] config = Configuration.from_files([str(CONFIG_PATH)]) config["strategy"] = "Wyckoff_BTC" config["strategy_path"] = str(ROOT / "user_data/Chan/strategies") config["timerange"] = timerange config["timeframe"] = exec_tf config["export"] = "none" config["runmode"] = RunMode.BACKTEST config["datadir"] = ROOT / "user_data/data/binance" config["user_data_dir"] = ROOT / "user_data" config["enable_protections"] = False bt = Backtesting(config) bt.start() stats = bt.results strat_stats = stats["strategy"].get("Wyckoff_BTC") or list(stats["strategy"].values())[0] trades = int(strat_stats.get("total_trades") or 0) profit_pct = strat_stats.get("profit_total_pct") if profit_pct is None: profit_pct = float(strat_stats.get("profit_total") or 0) * 100 dd = float(strat_stats.get("max_drawdown_account") or 0) * 100 wr = float(strat_stats.get("winrate") or 0) * 100 return { "ok": True, "profit_pct": float(profit_pct), "trades": trades, "dd_pct": dd, "pf": float(strat_stats.get("profit_factor") or 0), "winrate": wr, "rejected": int(strat_stats.get("rejected_signals") or 0), "timeframe_used": config.get("timeframe"), } def main() -> None: logging.getLogger("freqtrade").setLevel(logging.ERROR) timerange = sys.argv[1] if len(sys.argv) > 1 else "20240101-" install_offline_markets() orig = STRAT_PATH.read_text() rows: list[dict[str, Any]] = [] try: for label, exec_tf, stf, btf in COMBOS: print(f"=== {label} ===", flush=True) patch_strategy(exec_tf, stf, btf) try: res = run_one(exec_tf, timerange) except Exception as e: res = {"ok": False, "error": f"{type(e).__name__}: {e}"} res["label"] = label res["exec"] = exec_tf res["struct"] = stf res["bias"] = btf or "-" rows.append(res) if res.get("ok"): print( f" profit={res['profit_pct']:.2f}% trades={res['trades']} " f"dd={res['dd_pct']:.2f}% pf={res['pf']:.2f} wr={res['winrate']:.1f}% " f"rej={res['rejected']}", flush=True, ) else: print(f" FAILED: {res.get('error')}", flush=True) finally: STRAT_PATH.write_text(orig) ok = [r for r in rows if r.get("ok")] ok.sort(key=lambda r: (r["profit_pct"], r["pf"]), reverse=True) print("\n========== RANKING ==========") print(f"{'label':<16} {'E':<5} {'S':<5} {'B':<5} {'profit%':>8} {'trades':>7} {'dd%':>7} {'pf':>6} {'wr%':>6}") for r in ok: print( f"{r['label']:<16} {r['exec']:<5} {r['struct']:<5} {r['bias']:<5} " f"{r['profit_pct']:>8.2f} {r['trades']:>7} {r['dd_pct']:>7.2f} {r['pf']:>6.2f} {r['winrate']:>6.1f}" ) out = ROOT / "user_data/Chan/scripts/wyckoff_tf_grid_result.txt" out.write_text(json.dumps({"timerange": timerange, "rows": rows}, indent=2)) print(f"\nSaved {out}") if ok: best = ok[0] print(f"BEST: {best['label']} -> 将写入策略默认周期") if __name__ == "__main__": main()