Files
Chan/scripts/wyckoff_tf_grid.py
T
jackyu66gitandCursor 8ee11317d3 fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新
自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-25 22:57:43 +08:00

227 lines
6.5 KiB
Python

#!/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()