This commit is contained in:
aaron 2026-05-28 20:30:38 +08:00
parent 205eb43aa1
commit 15abdc3286
11 changed files with 146 additions and 16 deletions

View File

@ -104,6 +104,10 @@ AlphaX 是一个以 `Python + FastAPI + PostgreSQL + Docker + 静态 HTML` 组
- 核心认知:因子不等于策略。一个因子可以是先决条件、触发、确认、入场、风控或归因,但不能因为单个因子表现好就直接升级成完整策略。
- 完整策略必须至少包含:适用市场环境、交易宇宙、先决条件、核心触发、辅助确认、入场规则、止盈止损、失效条件、仓位/杠杆约束和独立复盘口径。
- 多策略链路必须保持独立:`main_composite_v1`、`box_retest_1h_v1`、`box_retest_4h_v1` 等策略可以共享行情、账户级风控和执行框架但不能共享同一套入场门槛、RR、挂单距离和 paper trading 执行门禁。
- 策略级配置入口在 `app/core/strategy_registry.py``StrategyDefinition.entry_gate_config` 控制确认/跟踪/展示派生的买点质量闸门,`StrategyDefinition.paper_config` 控制该策略进入 paper trading 的入场、挂单和动态杠杆门槛。
- 新增策略时必须注册稳定 `strategy_code`,并明确自己的 `entry_gate_config` / `paper_config`。不要把新策略的特殊门槛写进全局 `paper_trading_config()``DEFAULT_ENTRY_GATE`,否则会影响其他策略的信号生成和成交样本。
- `apply_entry_quality_gate()` 必须传入或从 `entry_plan.strategy_code` 派生策略身份;`paper_trader.py` 中开仓、挂单、挂单成交和挂单维护应通过策略级配置合并后的参数执行。
- `app/core/factor_scoring.py` 是确认层因子评分中心。新增确认加减分不要继续散落写死 `score += N`,应优先通过 `FactorScorer.delta(factor_code, base_delta, evidence=...)` 计算。
- 稳定因子代码来自 `app/core/signal_taxonomy.py`,例如 `vp_fly_1h_current`、`volume_consecutive_1h`、`ignition_d1_current`、`sector_rotation`、`sentiment_resonance`、`top_trader_long`、`risk_reward_bad`。
- `signal_performance` 是复盘后动态权重来源;`review_engine.py` 更新信号绩效后,`config_loader.get_signal_weights()` 会让下一轮筛选/确认读取生效权重。

View File

@ -10,6 +10,7 @@ import re
from typing import Any, Dict, Iterable, Tuple
from app.core.opportunity_level import OPPORTUNITY_LEVELS
from app.core.strategy_registry import normalize_strategy_code, strategy_entry_gate_config
DEFAULT_ENTRY_GATE = {
"enabled": True,
@ -274,13 +275,16 @@ def apply_entry_quality_gate(
derivatives_context: Dict[str, Any] = None,
sector_context: Dict[str, Any] = None,
cfg: Dict[str, Any] = None,
strategy_code: str = "",
) -> Tuple[str, Dict[str, Any], list]:
"""返回修正后的 action_status、增强后的 entry_plan、拦截原因。"""
cfg = {**DEFAULT_ENTRY_GATE, **(cfg or {})}
strategy_code = normalize_strategy_code(strategy_code or (entry_plan or {}).get("strategy_code"))
cfg = {**DEFAULT_ENTRY_GATE, **strategy_entry_gate_config(strategy_code), **(cfg or {})}
if not cfg.get("enabled", True):
return action_status, entry_plan or {}, []
entry_plan = dict(entry_plan or {})
entry_plan.setdefault("strategy_code", strategy_code)
signals = normalize_signals(signals)
market_context = market_context or {}
derivatives_context = derivatives_context or {}
@ -468,6 +472,7 @@ def apply_entry_quality_gate(
"risk_reward_ok": risk_reward_ok,
"breakout_distance_pct": breakout_distance,
"change_24h": change_24h,
"strategy_code": strategy_code,
}
if has_entry_score:
entry_plan["entry_quality_gate"]["entry_score"] = entry_score

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
MAIN_COMPOSITE_STRATEGY = "main_composite_v1"
@ -17,6 +17,8 @@ class StrategyDefinition:
description: str = ""
mode: str = "paper_only"
status: str = "active"
entry_gate_config: dict = field(default_factory=dict)
paper_config: dict = field(default_factory=dict)
STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
@ -31,12 +33,44 @@ STRATEGY_DEFINITIONS: dict[str, StrategyDefinition] = {
strategy_name="1H箱体突破回踩",
description="小时级底部箱体突破后回踩箱体上沿或EMA承接的早期结构策略。",
mode="paper_only",
entry_gate_config={
"min_entry_score_buy_now": 1,
"min_entry_score_wait_pullback": 0,
"min_rr_buy_now": 1.2,
"max_wait_pullback_deviation_pct": 20,
"breakout_distance_wait_pct": 25,
"gain_24h_wait_pct": 12,
},
paper_config={
"entry_min_rr": 1.5,
"order_min_rr": 1.5,
"order_min_distance_to_entry_pct": 0,
"order_require_current_trigger": False,
"dynamic_leverage_enabled": True,
"dynamic_leverage_min": 3,
},
),
BOX_RETEST_4H_STRATEGY: StrategyDefinition(
strategy_code=BOX_RETEST_4H_STRATEGY,
strategy_name="4H箱体突破回踩",
description="底部箱体突破后回踩箱体上沿或EMA承接的结构策略。",
mode="paper_only",
entry_gate_config={
"min_entry_score_buy_now": 1,
"min_entry_score_wait_pullback": 0,
"min_rr_buy_now": 1.2,
"max_wait_pullback_deviation_pct": 20,
"breakout_distance_wait_pct": 25,
"gain_24h_wait_pct": 12,
},
paper_config={
"entry_min_rr": 1.5,
"order_min_rr": 1.5,
"order_min_distance_to_entry_pct": 0,
"order_require_current_trigger": False,
"dynamic_leverage_enabled": True,
"dynamic_leverage_min": 3,
},
),
}
@ -63,6 +97,14 @@ def strategy_label(strategy_code: str | None) -> str:
return strategy_definition(strategy_code).strategy_name
def strategy_entry_gate_config(strategy_code: str | None) -> dict:
return dict(strategy_definition(strategy_code).entry_gate_config or {})
def strategy_paper_config(strategy_code: str | None) -> dict:
return dict(strategy_definition(strategy_code).paper_config or {})
def registered_strategy_codes() -> list[str]:
return list(STRATEGY_DEFINITIONS.keys())

View File

@ -8,7 +8,7 @@ from datetime import datetime, timedelta
from app.config.system_config import paper_trading_config
from app.core.global_risk import evaluate_global_risk
from app.core.strategy_registry import normalize_strategy_code, strategy_label
from app.core.strategy_registry import normalize_strategy_code, strategy_label, strategy_paper_config
from app.db.schema import get_conn
from app.db.system_logs import record_system_error
from app.integrations.feishu_push import push_card
@ -328,8 +328,19 @@ def _entry_plan(rec: dict) -> dict:
return _loads_json(rec.get("entry_plan_json"), {})
def _strategy_code_from_rec(rec: dict) -> str:
plan = _entry_plan(rec)
return normalize_strategy_code(rec.get("strategy_code") or plan.get("strategy_code"))
def _paper_cfg_for_rec(rec: dict, config: dict | None = None) -> dict:
cfg = dict(_paper_cfg(config) or {})
cfg.update(strategy_paper_config(_strategy_code_from_rec(rec)))
return cfg
def _strategy_lineage_from_rec(rec: dict) -> dict:
code = normalize_strategy_code(rec.get("strategy_code"))
code = _strategy_code_from_rec(rec)
signal_id = _safe_int(rec.get("strategy_signal_id"))
snapshot = _loads_json(rec.get("strategy_snapshot_json"), {})
roles = _loads_json(rec.get("factor_roles_json"), {})
@ -690,7 +701,7 @@ def _push_order_filled_card(order: dict, result: dict, event_time: str = "") ->
def _open_trade(conn, rec: dict, current_price: float, event_time: str, config: dict | None = None, push_open_card: bool = True) -> dict:
cfg = _paper_cfg(config)
cfg = _paper_cfg_for_rec(rec, config)
rec_id = _safe_int(rec.get("id"))
symbol = str(rec.get("symbol") or "").strip().upper()
plan = _entry_plan(rec)
@ -942,7 +953,7 @@ def _paper_order_distance_pct(side: str, current_price: float, target: float) ->
def _paper_order_gate(rec: dict, current_price: float, config: dict | None = None, conn=None) -> tuple[bool, list[str], dict]:
cfg = _paper_cfg(config)
cfg = _paper_cfg_for_rec(rec, config)
if not bool(cfg.get("order_gate_enabled", True)):
return True, [], {"gate_enabled": False}
@ -1074,7 +1085,7 @@ def _order_recommendation_cancel_reason(conn, rec: dict, order: dict) -> str:
def _order_payload_from_rec(rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict:
cfg = _paper_cfg(config)
cfg = _paper_cfg_for_rec(rec, config)
plan = _entry_plan(rec)
lineage = _strategy_lineage_from_rec(rec)
return {
@ -1106,7 +1117,7 @@ def _order_payload_from_rec(rec: dict, current_price: float, event_time: str, co
def _fill_paper_order(conn, order: dict, rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict:
fill_price = _safe_float(order.get("target_price")) or current_price
cfg = _paper_cfg(config)
cfg = _paper_cfg_for_rec(rec, config)
stop_loss = _safe_float(order.get("stop_loss") or _entry_plan(rec).get("stop_loss") or rec.get("stop_loss"))
base_notional = _safe_float(order.get("notional_usdt"), default_notional_usdt(cfg))
global_ok, global_detail = _global_risk_entry_check(conn, rec, base_notional, cfg)
@ -1174,7 +1185,7 @@ def _fill_paper_order(conn, order: dict, rec: dict, current_price: float, event_
def _sync_wait_pullback_order(conn, rec: dict, current_price: float, event_time: str, config: dict | None = None) -> dict:
cfg = _paper_cfg(config)
cfg = _paper_cfg_for_rec(rec, config)
rec_id = _safe_int(rec.get("id"))
order = conn.execute("SELECT * FROM paper_orders WHERE recommendation_id=%s", (rec_id,)).fetchone()
if order:

View File

@ -368,6 +368,7 @@ def apply_recommendation_state_transition(rec_id, requested_action, current_pric
market_context=normalize_json_object(item.get("market_context_json")),
derivatives_context=normalize_json_object(item.get("derivatives_context_json")),
sector_context=normalize_json_object(item.get("sector_context_json")),
strategy_code=item.get("strategy_code") or entry_plan.get("strategy_code"),
)
else:
gate_reasons = []
@ -547,6 +548,7 @@ def update_recommendation_action_status(rec_id, action_status):
market_context=normalize_json_object(row["market_context_json"]),
derivatives_context=normalize_json_object(row["derivatives_context_json"]),
sector_context=normalize_json_object(row["sector_context_json"]),
strategy_code=row["strategy_code"] or entry_plan.get("strategy_code"),
)
action_status = gated_action
entry_plan = gated_plan

View File

@ -302,6 +302,7 @@ def derive_execution_fields(item):
market_context=market_context,
derivatives_context=derivatives_context,
sector_context=sector_context,
strategy_code=item.get("strategy_code") or entry_plan.get("strategy_code"),
)
try:
rec_score_for_gate = float(item.get("rec_score") or 0)

View File

@ -41,6 +41,7 @@ from app.config.config_loader import (
get_strategy_params,
)
from app.core.opportunity_lifecycle import apply_entry_quality_gate
from app.core.strategy_registry import BOX_RETEST_1H_STRATEGY, BOX_RETEST_4H_STRATEGY, MAIN_COMPOSITE_STRATEGY
from app.core.opportunity_level import (
attach_opportunity_level,
classify_opportunity_level,
@ -1180,6 +1181,16 @@ def confirm_burst(symbol, cand):
atr_1h = calc_atr(h1_df, 14)
confirm_cfg = _get_cfg_section("confirm")
pa_recency_cfg = confirm_cfg.get("pa_recency", {})
# Defensive defaults: confirm runs under scheduler on many symbols with
# different data availability. Keep optional higher-timeframe signals from
# crashing the whole batch when a branch is skipped or an API returns less
# data than expected.
stale_vp_count = 0
stale_1h_ignitions = []
stale_d1_ignitions = []
bp_1h = {"detected": False}
bp_4h = {"detected": False}
bp_daily = {"detected": False}
upstream_sector_context = cand_detail.get("sector_context") or {}
if upstream_sector_context.get("hot_sectors") or upstream_sector_context.get("leader_symbol"):
@ -1250,7 +1261,6 @@ def confirm_burst(symbol, cand):
score += factor_scorer.delta("volume_divergence_1h", 1, evidence="1H放量但价格行为未确认", value=round(vol_ratio, 2))
# ---- 1H箱体突破回踩比4H更早的结构候选仍需买点/风控过滤 ----
bp_1h = {"detected": False}
try:
if h1_df is not None and len(h1_df) >= 60:
bp_1h = detect_box_breakout_pullback_1h(h1_df)
@ -1279,7 +1289,6 @@ def confirm_burst(symbol, cand):
resistance = high_q_supply[0]["top"]
# ---- 4H箱体突破回踩完整结构模型优先作为“可执行形态”记录和复盘 ----
bp_4h = {"detected": False}
try:
if h4_df is not None and len(h4_df) >= 40:
bp_4h = detect_box_breakout_pullback_4h(h4_df)
@ -1485,7 +1494,6 @@ def confirm_burst(symbol, cand):
# ---- 日线底部突破回踩检测(提前到门控前,供高位过滤器复用)----
# 复用已拉取的 d1_df零额外API调用
bp_daily = {"detected": False}
try:
if d1_df is not None and len(d1_df) >= 50:
bp_daily = detect_breakout_pullback(d1_df, "日线")
@ -1516,7 +1524,14 @@ def confirm_burst(symbol, cand):
current_trigger_ok = bool(current_trigger_times)
recent_candidate_ok = (fresh_reason == "fresh_candidate_state")
if score >= structure_gate_score and entry_action in ("即刻买入", "可即刻买入") and (current_trigger_ok or recent_candidate_ok):
if fresh_reason != "stale_structure_background_only" and (stale_vp_count > 0 or stale_1h_ignitions or stale_d1_ignitions or bp_daily.get("detected") or bp_1h.get("detected") or bp_4h.get("detected")):
if fresh_reason != "stale_structure_background_only" and (
stale_vp_count > 0
or stale_1h_ignitions
or stale_d1_ignitions
or (bp_daily or {}).get("detected")
or (bp_1h or {}).get("detected")
or (bp_4h or {}).get("detected")
):
signals.append(f"🟡 历史强背景+当前结构确认(score≥{structure_gate_score})")
confirmed = True
@ -1760,6 +1775,13 @@ def confirm_burst(symbol, cand):
}
entry_plan = attach_opportunity_level(entry_plan, level_meta)
gate_strategy_code = (
BOX_RETEST_1H_STRATEGY if bp_1h.get("detected")
else BOX_RETEST_4H_STRATEGY if bp_4h.get("detected")
else MAIN_COMPOSITE_STRATEGY
)
entry_plan.setdefault("strategy_code", gate_strategy_code)
# v1.7.5 买点质量闸门:确认强势 ≠ 允许现价追买。
gated_action, gated_plan, gate_reasons = apply_entry_quality_gate(
action_status="可即刻买入" if entry_action in ("即刻买入", "可即刻买入") else "等回踩",
@ -1769,6 +1791,7 @@ def confirm_burst(symbol, cand):
market_context=compute_market_context(h1_df, price),
derivatives_context=cand_detail.get("derivatives_context", {}),
sector_context=cand_detail.get("sector_context", {}),
strategy_code=gate_strategy_code,
)
if bypass_confirmed and vp_fly_count == 0 and not current_trigger_times and gated_action == "可即刻买入":
gated_action = "等回踩"
@ -2086,6 +2109,8 @@ def main(compact: bool = False, verbose: bool = False, limit: int | None = None,
rec_entry_price = result["price"]
previous_rec_id = _active_recommendation_id(symbol)
strategy_ctx = _strategy_context_for_recommendation(symbol, result, ep)
if strategy_ctx.get("strategy_code"):
ep["strategy_code"] = strategy_ctx["strategy_code"]
rec_id = create_recommendation(
symbol=symbol, rec_state="爆发", rec_score=result["score"],
entry_price=rec_entry_price,
@ -2142,6 +2167,8 @@ def main(compact: bool = False, verbose: bool = False, limit: int | None = None,
if _should_publish_watch_candidate(cand, result):
watch_plan = _watch_candidate_plan(symbol, result, cand_detail)
strategy_ctx = _strategy_context_for_recommendation(symbol, result, watch_plan)
if strategy_ctx.get("strategy_code"):
watch_plan["strategy_code"] = strategy_ctx["strategy_code"]
rec_id = create_recommendation(
symbol=symbol,
rec_state="观察",

View File

@ -250,6 +250,7 @@ def analyze_tracking_signals(symbol, rec, current_price):
market_context=rec.get("market_context") or {},
derivatives_context=rec.get("derivatives_context") or {},
sector_context=rec.get("sector_context") or {},
strategy_code=rec.get("strategy_code") or entry_plan.get("strategy_code"),
)
if gate_reasons:
buy_signals = reconcile_buy_signals_after_gate(

View File

@ -890,7 +890,7 @@ function renderRecCard(r) {
return '<span class="sig '+cls+'">'+displaySignalText(s)+'</span>';
}).join('');
var score = r.rec_score||0, st = scoreTier(score), ver = r.strategy_version||'';
var strategyLabel = r.strategy_name || ({main_composite_v1:'综合确认主链路',box_retest_4h_v1:'4H箱体突破回踩'}[r.strategy_code||''] || r.strategy_code || '');
var strategyLabel = r.strategy_name || ({main_composite_v1:'综合确认主链路',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩'}[r.strategy_code||''] || r.strategy_code || '');
var hasQualityGate = ep.entry_quality_gate && Array.isArray(ep.entry_quality_gate.reasons) && ep.entry_quality_gate.reasons.length;
var entryLabel = isWait ? '回踩参考' : (hasQualityGate ? '失效参考' : '参考价位');
var entryRef = (isWait || hasQualityGate) ? (ep.entry_price || r.entry_price || 0) : (r.entry_price || ep.entry_price || 0);

View File

@ -168,7 +168,7 @@ function renderOrders(items){if(!items.length){$('orderRows').innerHTML='<tr><td
'<td><button class="row-action" type="button" onclick="deleteOrder('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,'&#39;')+'\')">删除</button></td>'+
'</tr>'}).join('')}
function protectionCell(x){var trail=Number(x.trailing_stop||0);var trailHtml=trail>0?'<span class="trail-line">移动止盈 $'+fmt(trail,6)+'</span>':'<span class="trail-line off">移动止盈未启动</span>';return '<div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span>'+trailHtml+'</div>'}
function strategyName(x){return (x&&x.strategy_name)||({main_composite_v1:'综合确认主链路',box_retest_4h_v1:'4H箱体突破回踩'}[(x&&x.strategy_code)||'']||((x&&x.strategy_code)||'--'))}
function strategyName(x){return (x&&x.strategy_name)||({main_composite_v1:'综合确认主链路',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩'}[(x&&x.strategy_code)||'']||((x&&x.strategy_code)||'--'))}
async function loadOpenTrades(nextOffset){openOffset=Math.max(0,nextOffset||0);$('openRows').innerHTML='<tr><td colspan="14" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit='+LIMIT+'&offset='+openOffset+'&status=open');openTotal=d.total||0;renderTradeRows('openRows',d.items||[],'暂无持仓中的策略交易');renderOpenPager()}catch(e){$('openRows').innerHTML='<tr><td colspan="14" class="empty">'+esc(e.message)+'</td></tr>'}}
async function loadCompleted(){await Promise.all([loadCompletedTrades(),loadCompletedOrders()])}
async function loadCompletedTrades(){$('completedTradeRows').innerHTML='<tr><td colspan="14" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit=80&offset=0&status=closed');renderTradeRows('completedTradeRows',d.items||[],'暂无已平仓策略交易')}catch(e){$('completedTradeRows').innerHTML='<tr><td colspan="14" class="empty">'+esc(e.message)+'</td></tr>'}}
@ -199,10 +199,11 @@ function renderCompletedOrders(items){if(!items.length){$('completedOrderRows').
'<td><div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span></div></td>'+
'<td>'+time(x.created_at)+'</td>'+
'<td>'+time(ended)+'</td>'+
'<td><div>'+esc(strategyName(x))+'</div><div class="muted">'+esc(x.cancel_reason||x.source_status||'--')+' · '+esc(x.source_action||'')+'</div></td>'+
'<td><div>'+esc(strategyName(x))+'</div><div class="muted">'+esc(cancelReasonLabel(x.cancel_reason||x.status))+'</div><div class="muted">'+esc(x.source_status||'--')+' · '+esc(x.source_action||'')+'</div></td>'+
'<td><button class="row-action" type="button" onclick="deleteOrder('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,'&#39;')+'\')">删除</button></td>'+
'</tr>'}).join('')}
function orderStatus(x){return {filled:'已成交',expired:'已过期',canceled:'已取消',rejected:'已拒绝'}[x.status]||x.status||'--'}
function cancelReasonLabel(r){return {global_risk_rejected:'全局风控拒绝:市场/账户风险过高,未转持仓',stop_loss_leverage_risk_exceeded:'止损杠杆风险超限:按当前止损和杠杆亏损过大',portfolio_drawdown_pause:'账户回撤保护:暂停新增仓位',weak_entries_pause:'近期弱入场过多:暂停新增仓位',recommendation_invalid:'原推荐已失效:机会过期/归档后撤单',too_far_from_entry:'价格远离计划价:继续等待意义不大',expired:'挂单超时:超过有效期未成交',upgraded_to_buy_now:'信号升级为入场窗口:旧挂单已撤销',canceled:'已取消',filled:'已成交',rejected:'已拒绝'}[r]||r||'--'}
function renderOpenPager(){var page=Math.floor(openOffset/LIMIT)+1,totalPages=Math.max(1,Math.ceil(openTotal/LIMIT));$('openPageInfo').textContent='第 '+page+' / '+totalPages+' 页 · 共 '+openTotal+' 条';$('openPager').innerHTML='<button '+(openOffset===0?'disabled':'')+' onclick="loadOpenTrades('+(openOffset-LIMIT)+')">上一页</button><span>第 '+page+' / '+totalPages+' 页</span><button '+((openOffset+LIMIT>=openTotal)?'disabled':'')+' onclick="loadOpenTrades('+(openOffset+LIMIT)+')">下一页</button>'}
async function loadEvents(nextOffset){eventOffset=Math.max(0,nextOffset||0);$('eventRows').innerHTML='<div class="loading">加载中...</div>';try{var sym=$('eventSymbol').value||'';var typ=$('eventType').value||'';var d=await api('/api/paper-trading/events?limit='+EVENT_LIMIT+'&offset='+eventOffset+'&symbol='+encodeURIComponent(sym)+'&event_type='+encodeURIComponent(typ));eventTotal=d.total||0;renderEvents(d.items||[]);renderEventPager()}catch(e){$('eventRows').innerHTML='<div class="empty">'+esc(e.message)+'</div>'}}
function eventLabel(t){return {open:'开仓',close:'平仓',trailing_activate:'移动止盈激活',trailing_move:'移动止盈上移'}[t]||t||'动作'}

View File

@ -5,6 +5,7 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app.core.opportunity_lifecycle import apply_entry_quality_gate
from app.core.strategy_registry import BOX_RETEST_1H_STRATEGY, MAIN_COMPOSITE_STRATEGY
from app.services.price_tracker import reconcile_buy_signals_after_gate
@ -77,6 +78,41 @@ def test_low_entry_score_blocks_buy_now_and_weak_pullback():
assert plan['entry_quality_gate']['entry_score'] == 0
def test_entry_gate_uses_strategy_specific_thresholds():
base_plan = {
'entry_action': '可即刻买入',
'entry_price': 1.0,
'current_price': 1.0,
'stop_loss': 0.95,
'tp1': 1.12,
'risk_reward_ok': True,
'rr1': 2.4,
'entry_trigger_confirmed': True,
'score_components': {'opportunity_score': 12, 'entry_score': 1, 'risk_score': 1},
}
main_action, main_plan, _ = apply_entry_quality_gate(
action_status='可即刻买入',
entry_plan=dict(base_plan),
signals=['当前15min即刻入场信号'],
current_price=1.0,
market_context={'change_24h': 2.0},
strategy_code=MAIN_COMPOSITE_STRATEGY,
)
box_action, box_plan, _ = apply_entry_quality_gate(
action_status='可即刻买入',
entry_plan=dict(base_plan),
signals=['当前15min即刻入场信号'],
current_price=1.0,
market_context={'change_24h': 2.0},
strategy_code=BOX_RETEST_1H_STRATEGY,
)
assert main_action != '可即刻买入'
assert main_plan['entry_quality_gate']['strategy_code'] == MAIN_COMPOSITE_STRATEGY
assert box_action == '可即刻买入'
assert box_plan['strategy_code'] == BOX_RETEST_1H_STRATEGY
def test_buy_now_requires_current_trigger_and_no_bearish_flow_risk():
action, plan, reasons = apply_entry_quality_gate(
action_status='可即刻买入',