1
This commit is contained in:
parent
e63344b632
commit
29b36e48b0
@ -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"):
|
||||
|
||||
@ -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)},
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user