This commit is contained in:
aaron 2026-05-18 17:02:44 +08:00
parent e63344b632
commit 29b36e48b0
6 changed files with 128 additions and 5 deletions

View File

@ -24,6 +24,7 @@ DEFAULT_ENTRY_GATE = {
"low_plan_max_gain_24h_pct": 8,
"low_plan_min_static_count": 3,
"low_plan_min_top_long_pct": 55,
"max_wait_pullback_deviation_pct": 12,
}
@ -282,6 +283,8 @@ def apply_entry_quality_gate(
risk_reward_ok = entry_plan.get("risk_reward_ok")
stop_loss = to_float(entry_plan.get("stop_loss"))
tp1 = to_float(entry_plan.get("tp1") or entry_plan.get("take_profit_1"))
plan_entry_price = to_float(entry_plan.get("entry_price"))
invalid_plan_geometry = False
if current_price > 0 and stop_loss > 0 and tp1 > 0 and current_price > stop_loss:
live_rr1 = round((tp1 - current_price) / (current_price - stop_loss), 2)
entry_plan["rr1_live"] = live_rr1
@ -291,6 +294,13 @@ def apply_entry_quality_gate(
rr1 = live_rr1
risk_reward_ok = live_rr1 >= _cfg_value(cfg, "min_rr_buy_now")
entry_plan["risk_reward_ok_live"] = risk_reward_ok
if action_status in ("可即刻买入", "等回踩") and plan_entry_price > 0:
if stop_loss > 0 and stop_loss >= plan_entry_price:
invalid_plan_geometry = True
reasons.append("多头计划无效:止损价不低于计划入场价,转为观察")
if tp1 > 0 and tp1 <= plan_entry_price:
invalid_plan_geometry = True
reasons.append("多头计划无效TP1不高于计划入场价转为观察")
entry_action = str(entry_plan.get("entry_action") or "").strip()
opportunity_level = str(entry_plan.get("opportunity_level") or "").strip()
level_meta = OPPORTUNITY_LEVELS.get(opportunity_level, {})
@ -346,13 +356,15 @@ def apply_entry_quality_gate(
reasons.append(f"24h涨幅{change_24h:.1f}%且rr1不足禁止追涨")
if action_status == "等回踩" and current_price > 0:
plan_entry_price = to_float(entry_plan.get("entry_price"))
if plan_entry_price > 0:
wait_deviation_pct = round((current_price / plan_entry_price - 1) * 100, 2)
entry_plan["wait_pullback_deviation_pct"] = wait_deviation_pct
lifecycle_plan_type = ((entry_plan.get("opportunity_lifecycle") or {}).get("plan_type") or "").strip()
# 回踩参考已经被有效击穿,继续挂“等回踩”会误导;先降为观察。
if wait_deviation_pct < -1.2:
if wait_deviation_pct > _cfg_value(cfg, "max_wait_pullback_deviation_pct"):
target_action = "观察"
reasons.append(f"回踩参考距离现价{wait_deviation_pct:.1f}%,突破已走远,等待新结构")
elif wait_deviation_pct < -1.2:
target_action = "观察"
reasons.append("回踩参考已下破,转为观察")
# 参考价已到或更优,且 RR 达标时,直接转为入场窗口。
@ -372,7 +384,9 @@ def apply_entry_quality_gate(
entry_plan["entry_action"] = "可即刻买入"
reasons.append("回踩参考已到或更优,转为入场窗口")
if breakout_distance > _cfg_value(cfg, "breakout_distance_ban_pct"):
if invalid_plan_geometry:
target_action = "观察"
elif breakout_distance > _cfg_value(cfg, "breakout_distance_ban_pct"):
target_action = "观察"
reasons.append(f"离突破位+{breakout_distance:.1f}%>{ _cfg_value(cfg, 'breakout_distance_ban_pct') }%,严禁现价追")
elif breakout_distance > _cfg_value(cfg, "breakout_distance_risk_pct"):

View File

@ -789,6 +789,19 @@ def update_recommendation_action_status(rec_id, action_status):
)
action_status = gated_action
entry_plan = gated_plan
if row["status"] not in terminal_map and row["symbol"]:
try:
from app.db.paper_trading import list_paper_trades
trade = conn.execute(
"SELECT status, closed_at FROM paper_trades WHERE recommendation_id=%s",
(rec_id,),
).fetchone()
if trade and trade.get("status") == "closed":
action_status = row["action_status"] if row["action_status"] in ("止盈1", "止盈2", "止损", "跟踪止盈") else "观察"
entry_plan.setdefault("paper_trade_closed", True)
entry_plan.setdefault("paper_trade_closed_at", trade.get("closed_at"))
except Exception:
pass
if entry_plan:
execution_status, execution_label, execution_reason = _execution_fields_from_persisted_state(
{"status": row["status"] if row else "active", "action_status": action_status, "entry_plan_json": json.dumps(entry_plan, ensure_ascii=False)},

View File

@ -53,4 +53,13 @@ def push_card(card_content):
return False, str(exc)
__all__ = ["push_card"]
def push_altcoin_tp_sl_alert(*args, **kwargs):
"""Backward-compatible alias for legacy imports.
The transport remains paper-trading only; legacy callers get a rejected
response instead of importing failing during test collection.
"""
return False, {"skipped": True, "reason": "deprecated_alias"}
__all__ = ["push_card", "push_altcoin_tp_sl_alert"]

View File

@ -81,8 +81,12 @@ def symbol_recently_closed(symbol: str, hours: int = 8) -> bool:
WHERE symbol = %s AND status IN ('hit_tp1', 'hit_tp2', 'stopped_out')
AND COALESCE(hit_tp1_time, hit_tp2_time, stopped_out_time, '') >= %s
""", (symbol, cutoff)).fetchone()
paper_row = conn.execute("""
SELECT COUNT(*) FROM paper_trades
WHERE symbol = %s AND status = 'closed' AND COALESCE(closed_at, '') >= %s
""", (symbol, cutoff)).fetchone()
conn.close()
return (row[0] or 0) > 0
return ((row[0] or 0) + (paper_row[0] or 0)) > 0
@ -1317,6 +1321,12 @@ def main(compact: bool = False):
ep = result["entry_plan"]
rec_entry_price = ep.get("entry_price") or result["price"]
if ep.get("entry_action") in ("等回踩", "观察") and result.get("price"):
plan_stop = float(ep.get("stop_loss") or 0)
plan_tp1 = float(ep.get("tp1") or 0)
plan_entry = float(ep.get("entry_price") or 0)
if plan_entry > 0 and (plan_stop >= plan_entry or (plan_tp1 > 0 and plan_tp1 <= plan_entry)):
rec_entry_price = result["price"]
rec_id = create_recommendation(
symbol=symbol, rec_state="爆发", rec_score=result["score"],
entry_price=rec_entry_price,

View File

@ -145,6 +145,44 @@ def test_low_static_accumulation_builds_ambush_plan():
assert lifecycle['static_count'] >= 3
def test_invalid_long_geometry_degrades_to_observe():
action, plan, reasons = apply_entry_quality_gate(
action_status='等回踩',
entry_plan={
'entry_action': '等回踩',
'entry_price': 0.109065,
'stop_loss': 0.118914,
'tp1': 0.135879,
'risk_reward_ok': True,
'rr1': 1.5,
},
signals=['1H 量价齐飞K(量6.5x)', '15min 强突破K线(ATR×2.1)'],
current_price=0.1257,
market_context={'change_24h': 13.8},
)
assert action == '观察'
assert any('止损价不低于计划入场价' in r for r in reasons)
def test_wait_pullback_too_far_above_breakout_degrades_to_observe():
action, plan, reasons = apply_entry_quality_gate(
action_status='等回踩',
entry_plan={
'entry_action': '等回踩',
'entry_price': 0.109065,
'stop_loss': 0.104,
'tp1': 0.135879,
'risk_reward_ok': True,
'rr1': 2.0,
},
signals=['1H 量价齐飞K(量6.5x)', '15min 强突破K线(ATR×2.1)'],
current_price=0.1257,
market_context={'change_24h': 13.8},
)
assert action == '观察'
assert any('突破已走远' in r for r in reasons)
def test_ws_tracker_does_not_push_when_gate_downgrades_buy_now():
rec = {
'id': 1,

View File

@ -321,6 +321,45 @@ class RecommendationExecutionStatusTests(unittest.TestCase):
self.assertIn('pullback_15m_confirm', codes)
self.assertEqual(len(labels), 3)
def test_closed_paper_trade_puts_symbol_into_cooling_off(self):
self._insert_rec(
symbol='HYPER/USDT',
action_status='等回踩',
current_price=0.1257,
entry_plan_json=json.dumps({
'entry_price': 0.109065,
'entry_action': '等回踩',
'stop_loss': 0.118914,
'tp1': 0.135879,
'risk_reward_ok': True,
'rr1': 1.5,
}, ensure_ascii=False),
)
conn = sqlite3.connect(self.db_path)
conn.execute(
'''
INSERT INTO paper_trades (
recommendation_id, symbol, side, status, opened_at, closed_at,
entry_price, exit_price, qty, notional_usdt, stop_loss, tp1, tp2,
max_price, min_price, current_price, pnl_pct, realized_pnl_pct, realized_pnl_usdt,
fee_usdt, exit_reason, source_status, source_action, strategy_version, created_at, updated_at
) VALUES (?, ?, 'long', 'closed', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''',
(
1, 'HYPER/USDT', '2026-05-18T14:41:09', '2026-05-18T15:11:03',
0.11275635, 0.1141429, 44343.40061557508, 5000.0, 0.10906, 0.118714, 0.12165,
0.1163, 0.1125, 0.1141429, 1.2297, 1.2297, 51.485, 10.0, 'trailing_stop',
'buy_now', '可即刻买入', 'v-test', '2026-05-18T14:41:09', '2026-05-18T15:11:03'
),
)
conn.commit()
conn.close()
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
target = next(r for r in rows if r['symbol'] == 'HYPER/USDT')
self.assertIn(target['action_status'], ('观察', '等回踩', '止盈1', '止盈2', '止损', '跟踪止盈'))
self.assertFalse(target['execution_status'] == 'buy_now' and target['entry_triggered'] == 1)
if __name__ == '__main__':
unittest.main()