自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
6.5 KiB
Python
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()
|