Files
Chan/strategies/Wyckoff_BTC_GATED.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

198 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# --- Do not remove these libs ---
"""
Wyckoff BTC — Market-State Gated SpringDecision Layer
Spring = V1_BASELINEFROZEN
Gate v1.1 = LOCKED default Decision rule:
market_state in {accumulation, markup} -> allow Spring
else -> block
Soft-score 不进默认规则。勿改 Spring;勿全样本扫 Gate。
"""
from __future__ import annotations
import json
import logging
import sys
from pathlib import Path
from pandas import DataFrame
import pandas as pd
_CHAN = Path(__file__).resolve().parents[1]
if str(_CHAN) not in sys.path:
sys.path.insert(0, str(_CHAN))
from engine.market_state import apply_decision_gate, compute_market_state_8h # noqa: E402
from freqtrade.strategy import merge_informative_pair # noqa: E402
from Wyckoff_BTC_V1_BASELINE import Wyckoff_BTC_V1_BASELINE # noqa: E402
logger = logging.getLogger(__name__)
class Wyckoff_BTC_GATED(Wyckoff_BTC_V1_BASELINE):
"""Baseline Spring + causal Market State Gate。"""
STRATEGY_VERSION = "GATED_V1_1_LOCKED"
SETUP_FAMILY = "SPRING_GATED"
# LOCKED default — 研究脚本可临时改写,跑完必须恢复
gate_mode: str = "state_set"
gate_q_sum: float = 100.0
gate_q_bad: float = 55.0
decision_log_enabled: bool = True
decision_log_path: str = str(_CHAN / "logs" / "wyckoff_decision_events.jsonl")
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe = super().populate_indicators(dataframe, metadata)
pair = metadata["pair"]
btf = self.bias_timeframe or "8h"
raw8 = self.dp.get_pair_dataframe(pair=pair, timeframe=btf)
st8 = compute_market_state_8h(raw8)
# 覆盖默认门闩为当前 class 配置(可能已被脚本锁定)
st8 = apply_decision_gate(
st8,
mode=str(self.gate_mode),
q_sum=float(self.gate_q_sum),
q_bad=float(self.gate_q_bad),
)
keep = [
"date",
"accumulation_score",
"markup_score",
"distribution_score",
"markdown_score",
"range_score",
"market_state",
"allow_spring",
"allow_utad",
"ema_slope",
"dist_ema200",
]
st8 = st8[[c for c in keep if c in st8.columns]].copy()
dataframe = merge_informative_pair(dataframe, st8, self.timeframe, btf, ffill=True)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe = super().populate_entry_trend(dataframe, metadata)
bs = f"_{self.bias_timeframe or '8h'}"
allow_s = dataframe.get(f"allow_spring{bs}")
allow_u = dataframe.get(f"allow_utad{bs}")
if allow_s is None or allow_u is None:
return dataframe
allow_s = allow_s.fillna(False).astype(bool)
allow_u = allow_u.fillna(False).astype(bool)
block_long = (dataframe["enter_long"] == 1) & (~allow_s)
block_short = (dataframe["enter_short"] == 1) & (~allow_u)
self._log_decision_events(dataframe, metadata, allow_s, allow_u, bs)
dataframe.loc[block_long, ["enter_long", "enter_tag"]] = (0, "")
dataframe.loc[block_short, ["enter_short", "enter_tag"]] = (0, "")
return dataframe
def _decision_log_active(self) -> bool:
if not bool(getattr(self, "decision_log_enabled", True)):
return False
config = getattr(self, "config", {}) or {}
runmode = config.get("runmode")
runmode_value = getattr(runmode, "value", str(runmode) if runmode is not None else "")
if runmode_value:
return runmode_value == "dry_run"
return bool(config.get("dry_run", False))
def _log_decision_events(
self,
dataframe: DataFrame,
metadata: dict,
allow_s: pd.Series,
allow_u: pd.Series,
bias_suffix: str,
) -> None:
if not self._decision_log_active():
return
pair = metadata.get("pair", "")
long_candidates = dataframe["enter_long"] == 1
short_candidates = dataframe["enter_short"] == 1
if not bool(long_candidates.any() or short_candidates.any()):
return
seen = getattr(self, "_decision_log_seen", None)
if seen is None:
seen = set()
self._decision_log_seen = seen
events = []
for idx in dataframe.index[long_candidates]:
events.append(self._decision_event(dataframe.loc[idx], pair, "SPRING_LONG", bool(allow_s.loc[idx]), bias_suffix))
for idx in dataframe.index[short_candidates]:
events.append(self._decision_event(dataframe.loc[idx], pair, "UTAD_SHORT", bool(allow_u.loc[idx]), bias_suffix))
path = Path(str(getattr(self, "decision_log_path", ""))).expanduser()
try:
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("a", encoding="utf-8") as handle:
for event in events:
key = (
event["timestamp"],
event["pair"],
event["signal_type"],
event["gate_version"],
)
if key in seen:
continue
seen.add(key)
handle.write(json.dumps(event, ensure_ascii=False, sort_keys=True) + "\n")
except OSError as exc:
logger.warning("Decision log write failed: %s", exc)
def _decision_event(self, row: pd.Series, pair: str, signal_type: str, allow: bool, bias_suffix: str) -> dict:
state_col = f"market_state{bias_suffix}"
bias_time_col = f"date{bias_suffix}"
state = self._json_value(row.get(state_col))
bias_bar_time = self._json_value(row.get(bias_time_col))
state_missing = state in (None, "", "missing")
block_reason = "" if allow else ("state_missing" if state_missing else "not_in_allow_set")
event = {
"timestamp": self._json_value(row.get("date")),
"pair": pair,
"signal_type": signal_type,
"market_state": state if not state_missing else "missing",
"allow": bool(allow),
"gate_version": self.STRATEGY_VERSION,
"baseline_signal": signal_type,
"block_reason": block_reason,
"bias_bar_time": bias_bar_time,
"would_enter": True,
"order_sent": bool(allow),
}
for score in [
"accumulation_score",
"markup_score",
"distribution_score",
"markdown_score",
"range_score",
]:
event[score] = self._json_value(row.get(f"{score}{bias_suffix}"))
return event
@staticmethod
def _json_value(value):
if value is None:
return None
try:
if pd.isna(value):
return None
except (TypeError, ValueError):
pass
if hasattr(value, "isoformat"):
return value.isoformat()
if hasattr(value, "item"):
return value.item()
return value