fix(web): 自动刷新保留 K 线视窗;威科夫与图表增量更新
自动刷新改用 tail update 与 scrollToPosition 恢复视窗,避免 setData 后跳到最右;拆分 chart_tv 模块并扩展 analyze/recent API。同步威科夫分析、pipeline 增量构建及相关策略与配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user