This commit is contained in:
aaron 2026-05-15 20:32:42 +08:00
parent e80431eb89
commit 821af1282e
11 changed files with 209 additions and 47 deletions

View File

@ -168,6 +168,24 @@ def detect_static_count(signals: Iterable[Any]) -> int:
return max_count return max_count
def has_current_entry_trigger(signals: Iterable[Any], entry_plan: Dict[str, Any]) -> bool:
if entry_plan.get("entry_trigger_confirmed") is True:
return True
summary = str(entry_plan.get("pa_15min_summary") or "")
if "即刻入场" in summary and "无动K突破" not in summary:
return True
for sig in signals or []:
text = str(sig)
if "15min即刻入场" in text or "当前15min" in text or "当前 15min" in text:
return True
return False
def has_bearish_flow_risk(signals: Iterable[Any]) -> bool:
risk_keywords = ("空头加速", "放量阴线", "多头出货", "量价背离")
return any(any(keyword in str(sig) for keyword in risk_keywords) for sig in signals or [])
def _calc_position_pct(current_price: float, entry_plan: Dict[str, Any]) -> float: def _calc_position_pct(current_price: float, entry_plan: Dict[str, Any]) -> float:
support = to_float(entry_plan.get("support") or entry_plan.get("support_price") or entry_plan.get("range_low")) support = to_float(entry_plan.get("support") or entry_plan.get("support_price") or entry_plan.get("range_low"))
resistance = to_float(entry_plan.get("resistance") or entry_plan.get("resistance_price") or entry_plan.get("range_high")) resistance = to_float(entry_plan.get("resistance") or entry_plan.get("resistance_price") or entry_plan.get("range_high"))
@ -302,6 +320,10 @@ def apply_entry_quality_gate(
reasons.append(f"rr1={rr1} < {_cfg_value(cfg, 'min_rr_buy_now')},禁止现价买入") reasons.append(f"rr1={rr1} < {_cfg_value(cfg, 'min_rr_buy_now')},禁止现价买入")
if action_status == "可即刻买入": if action_status == "可即刻买入":
if not has_current_entry_trigger(signals, entry_plan):
reasons.append("缺少当前15min触发禁止现价买入")
if has_bearish_flow_risk(signals):
reasons.append("出现空头加速/放量阴线风险,禁止现价买入")
if current_price > 0: if current_price > 0:
plan_entry_price = to_float(entry_plan.get("entry_price")) plan_entry_price = to_float(entry_plan.get("entry_price"))
# 价格已经回到/跌破计划参考价,且实时 RR 已达标时,应转为入场窗口,不能继续显示“等回踩”。 # 价格已经回到/跌破计划参考价,且实时 RR 已达标时,应转为入场窗口,不能继续显示“等回踩”。

View File

@ -1363,7 +1363,15 @@ def _is_executed_trade(item):
status = (item.get("status") or "").strip() status = (item.get("status") or "").strip()
action_status = normalize_action_status(item.get("action_status"), status) action_status = normalize_action_status(item.get("action_status"), status)
execution_status = item.get("execution_status") or "" execution_status = item.get("execution_status") or ""
if action_status == "可即刻买入" or execution_status == "buy_now": try:
entry_triggered = int(item.get("entry_triggered") or 0) == 1
except Exception:
entry_triggered = False
if entry_triggered:
return True
if status in ("hit_tp1", "hit_tp2", "stopped_out"):
return True
if item.get("display_bucket") == "position" or execution_status in ("holding", "completed"):
return True return True
return is_executed_lifecycle(status, action_status, execution_status) return is_executed_lifecycle(status, action_status, execution_status)

View File

@ -52,6 +52,42 @@ def _safe_float(value, default=0.0):
return default return default
EXECUTED_TRADE_WHERE = """(
COALESCE(entry_triggered, 0) = 1
OR COALESCE(display_bucket, '') = 'position'
OR COALESCE(execution_status, '') IN ('holding', 'completed')
OR status IN ('hit_tp1', 'hit_tp2', 'stopped_out')
)"""
SUCCESS_CASE = f"""(
({EXECUTED_TRADE_WHERE})
AND (
status IN ('hit_tp1', 'hit_tp2')
OR (
status NOT IN ('stopped_out', 'expired', 'invalid', 'archived')
AND COALESCE(max_pnl_pct, 0) >= 5
)
)
)"""
FAILURE_CASE = f"""(
({EXECUTED_TRADE_WHERE})
AND (status='stopped_out' OR COALESCE(pnl_pct, 0) <= -3 OR COALESCE(max_drawdown_pct, 0) <= -5)
)"""
def _executed_trade_where(alias=""):
prefix = f"{alias}." if alias else ""
return f"""(
COALESCE({prefix}entry_triggered, 0) = 1
OR COALESCE({prefix}display_bucket, '') = 'position'
OR COALESCE({prefix}execution_status, '') IN ('holding', 'completed')
OR {prefix}status IN ('hit_tp1', 'hit_tp2', 'stopped_out')
)"""
def _parse_dt(value): def _parse_dt(value):
if isinstance(value, datetime): if isinstance(value, datetime):
return value return value
@ -162,20 +198,16 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
except Exception: except Exception:
offset = 0 offset = 0
result_where = """(status IN ('hit_tp1', 'hit_tp2', 'stopped_out') result_where = EXECUTED_TRADE_WHERE
OR (COALESCE(max_pnl_pct, 0) >= 5)
OR (COALESCE(pnl_pct, 0) <= -3 OR COALESCE(max_drawdown_pct, 0) <= -5))"""
version_where = " AND strategy_version=?" if version else "" version_where = " AND strategy_version=?" if version else ""
params = [version] if version else [] params = [version] if version else []
total = None total = None
summary = None summary = None
version_counts = [] version_counts = []
success_case = "status IN ('hit_tp1','hit_tp2') OR (status NOT IN ('stopped_out','expired','invalid','archived') AND COALESCE(max_pnl_pct,0) >= 5)"
failure_case = "status='stopped_out' OR COALESCE(pnl_pct,0) <= -3 OR COALESCE(max_drawdown_pct,0) <= -5"
realized_pnl_case = ( realized_pnl_case = (
f"CASE WHEN {failure_case} THEN COALESCE(pnl_pct,0) " f"CASE WHEN {FAILURE_CASE} THEN COALESCE(pnl_pct,0) "
f"WHEN {success_case} THEN COALESCE(NULLIF(max_pnl_pct,0), pnl_pct, 0) " f"WHEN {SUCCESS_CASE} THEN COALESCE(NULLIF(max_pnl_pct,0), pnl_pct, 0) "
"ELSE 0 END" "ELSE 0 END"
) )
@ -201,10 +233,10 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
SELECT SELECT
COUNT(*) AS total, COUNT(*) AS total,
SUM(CASE WHEN """ SUM(CASE WHEN """
+ success_case + SUCCESS_CASE
+ """ THEN 1 ELSE 0 END) AS success_count, + """ THEN 1 ELSE 0 END) AS success_count,
SUM(CASE WHEN """ SUM(CASE WHEN """
+ failure_case + FAILURE_CASE
+ """ THEN 1 ELSE 0 END) AS failure_count, + """ THEN 1 ELSE 0 END) AS failure_count,
SUM(""" SUM("""
+ realized_pnl_case + realized_pnl_case
@ -213,7 +245,7 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
+ realized_pnl_case + realized_pnl_case
+ """) AS best_pnl, + """) AS best_pnl,
AVG(CASE WHEN """ AVG(CASE WHEN """
+ failure_case + FAILURE_CASE
+ """ THEN COALESCE(pnl_pct,0) END) AS avg_failure_pnl + """ THEN COALESCE(pnl_pct,0) END) AS avg_failure_pnl
FROM ( FROM (
SELECT r.* SELECT r.*
@ -232,6 +264,9 @@ def get_all_recommendations(limit=50, decision_only=False, version="", offset=0,
tuple(params), tuple(params),
).fetchone() ).fetchone()
summary = dict(summary_row) if summary_row else {} summary = dict(summary_row) if summary_row else {}
for key in ("total", "success_count", "failure_count", "total_pnl", "best_pnl", "avg_failure_pnl"):
if summary.get(key) is None:
summary[key] = 0
vc_rows = conn.execute( vc_rows = conn.execute(
""" """
@ -493,13 +528,20 @@ def get_stats():
decay_watch = decay_candidates[0] if decay_candidates else None decay_watch = decay_candidates[0] if decay_candidates else None
points_24h = [] points_24h = []
rows_24h = conn.execute(""" rows_24h = conn.execute(
SELECT substr(track_time, 1, 13) || ':00:00' AS bucket, AVG(pnl_pct) AS avg_pnl, COUNT(*) AS sample_count """
FROM price_tracking SELECT substr(pt.track_time, 1, 13) || ':00:00' AS bucket, AVG(pt.pnl_pct) AS avg_pnl, COUNT(*) AS sample_count
WHERE julianday(?) - julianday(track_time) <= 1.0 FROM price_tracking pt
JOIN recommendation r ON r.id = pt.rec_id
WHERE julianday(?) - julianday(pt.track_time) <= 1.0
AND """
+ _executed_trade_where("r")
+ """
GROUP BY bucket GROUP BY bucket
ORDER BY bucket ASC ORDER BY bucket ASC
""", (now.isoformat(),)).fetchall() """,
(now.isoformat(),),
).fetchall()
for row in rows_24h: for row in rows_24h:
points_24h.append({ points_24h.append({
"time": row["bucket"], "time": row["bucket"],
@ -508,13 +550,20 @@ def get_stats():
}) })
points_7d = [] points_7d = []
rows_7d = conn.execute(""" rows_7d = conn.execute(
SELECT substr(track_time, 1, 10) AS bucket, AVG(pnl_pct) AS avg_pnl, COUNT(*) AS sample_count """
FROM price_tracking SELECT substr(pt.track_time, 1, 10) AS bucket, AVG(pt.pnl_pct) AS avg_pnl, COUNT(*) AS sample_count
WHERE julianday(?) - julianday(track_time) <= 7.0 FROM price_tracking pt
JOIN recommendation r ON r.id = pt.rec_id
WHERE julianday(?) - julianday(pt.track_time) <= 7.0
AND """
+ _executed_trade_where("r")
+ """
GROUP BY bucket GROUP BY bucket
ORDER BY bucket ASC ORDER BY bucket ASC
""", (now.isoformat(),)).fetchall() """,
(now.isoformat(),),
).fetchall()
for row in rows_7d: for row in rows_7d:
points_7d.append({ points_7d.append({
"time": row["bucket"], "time": row["bucket"],

View File

@ -407,11 +407,11 @@ event_driven:
note: Solana meme主题扩散 note: Solana meme主题扩散
meta: meta:
version: 1 version: 1
last_review: '2026-05-15T11:16:43.982118' last_review: '2026-05-15T11:52:18.602057'
last_reverse_analysis: '2026-05-15T11:17:20.540656' last_reverse_analysis: '2026-05-15T11:52:58.300578'
total_reviews: 44 total_reviews: 45
total_rules_learned: 37 total_rules_learned: 37
iteration_count: 49 iteration_count: 50
strategy_version: v1.7.11 strategy_version: v1.7.11
strategy_revision_started_at: '2026-05-09T01:20:00' strategy_revision_started_at: '2026-05-09T01:20:00'
strategy_revision_note: 'v1.7.11: 触发时效治理,旧形态只作背景,消息触发显式标记' strategy_revision_note: 'v1.7.11: 触发时效治理,旧形态只作背景,消息触发显式标记'

View File

@ -35,7 +35,7 @@ def _insert_recommendation(db_path, **kwargs):
tp1=110.0, tp1=110.0,
tp2=118.0, tp2=118.0,
sector='AI', sector='AI',
signals='[]', signals='["🟢 15min即刻入场信号"]',
is_meme=0, is_meme=0,
status='active', status='active',
current_price=100.0, current_price=100.0,
@ -49,7 +49,7 @@ def _insert_recommendation(db_path, **kwargs):
stopped_out_time='', stopped_out_time='',
expired_time='', expired_time='',
last_track_time='2026-04-30T10:05:00', last_track_time='2026-04-30T10:05:00',
entry_plan_json='{}', entry_plan_json='{"entry_trigger_confirmed": true}',
action_status='持有', action_status='持有',
direction='多头启动', direction='多头启动',
strategy_version='v1.2', strategy_version='v1.2',
@ -151,9 +151,9 @@ def test_stats_only_count_actionable_active_recommendations(temp_db):
stats = altcoin_db.get_stats() stats = altcoin_db.get_stats()
assert stats['active_count'] == 2 assert stats['active_count'] == 2
assert stats['raw_active_count'] == 4 assert stats['raw_active_count'] == 4
assert stats['active_pnl_sum'] == 5.0 assert stats['active_pnl_sum'] == 0
assert stats['active_avg_pnl'] == 5.0 assert stats['active_avg_pnl'] == 0
assert stats['active_success_count'] == 1 assert stats['active_success_count'] == 0
assert stats['active_failed_count'] == 0 assert stats['active_failed_count'] == 0
assert stats['active_pending_count'] == 0 assert stats['active_pending_count'] == 0
@ -198,12 +198,12 @@ def test_stats_api_exposes_separate_live_and_history_sections(temp_db):
live = stats['live_overview'] live = stats['live_overview']
assert live['actionable_count'] == 2 assert live['actionable_count'] == 2
assert live['executed_trade_count'] == 1 assert live['executed_trade_count'] == 0
assert live['executed_pnl_sum'] == 5.0 assert live['executed_pnl_sum'] == 0
assert live['executed_avg_pnl'] == 5.0 assert live['executed_avg_pnl'] == 0
assert live['actionable_pnl_sum'] == 5.0 assert live['actionable_pnl_sum'] == 0
assert live['actionable_avg_pnl'] == 5.0 assert live['actionable_avg_pnl'] == 0
assert live['actionable_success_count'] == 1 assert live['actionable_success_count'] == 0
assert live['actionable_failed_count'] == 0 assert live['actionable_failed_count'] == 0
assert live['actionable_pending_count'] == 0 assert live['actionable_pending_count'] == 0
assert live['raw_active_count'] == 4 assert live['raw_active_count'] == 4
@ -307,6 +307,44 @@ def test_history_avg_pnl_only_uses_real_tp_and_stopout_samples(temp_db):
assert stats['history_overview']['avg_pnl_pct'] == pytest.approx(1.0) assert stats['history_overview']['avg_pnl_pct'] == pytest.approx(1.0)
def test_history_api_excludes_untriggered_observe_wait_and_invalid_losses(temp_db):
_insert_recommendation(
temp_db,
symbol='OBSLOSS/USDT',
action_status='持有',
status='active',
entry_plan_json='{"entry_action": "继续观察"}',
pnl_pct=-4.0,
max_drawdown_pct=-6.0,
)
_insert_recommendation(
temp_db,
symbol='WAITLOSS/USDT',
action_status='等回踩',
status='active',
entry_plan_json='{"entry_action": "等回踩", "entry_price": 95}',
pnl_pct=-5.0,
max_drawdown_pct=-7.0,
)
_insert_recommendation(
temp_db,
symbol='INVLOSS/USDT',
action_status='衰减',
status='active',
entry_plan_json='{"entry_action": "继续观察"}',
pnl_pct=-6.0,
max_drawdown_pct=-8.0,
)
page = altcoin_db.get_all_recommendations(limit=20, decision_only=True, with_meta=True)
assert page['items'] == []
assert page['total'] == 0
assert page['summary']['total'] == 0
assert page['summary']['failure_count'] == 0
assert page['summary']['total_pnl'] == 0
def test_version_filter_labels_use_plan_not_executable_to_avoid_wait_pullback_confusion(temp_db): def test_version_filter_labels_use_plan_not_executable_to_avoid_wait_pullback_confusion(temp_db):
_insert_recommendation( _insert_recommendation(
temp_db, temp_db,

View File

@ -25,7 +25,7 @@ def temp_db(monkeypatch, tmp_path):
return db_path return db_path
def test_wait_pullback_plan_is_actionable_but_not_counted_as_executed_pnl(temp_db): def test_actionable_plans_are_not_counted_as_executed_pnl_until_entry_triggers(temp_db):
_insert_recommendation( _insert_recommendation(
temp_db, temp_db,
symbol='BUY/USDT', symbol='BUY/USDT',
@ -59,14 +59,34 @@ def test_wait_pullback_plan_is_actionable_but_not_counted_as_executed_pnl(temp_d
assert live['buy_now_count'] == 1 assert live['buy_now_count'] == 1
assert live['wait_pullback_count'] == 1 assert live['wait_pullback_count'] == 1
# 但收益只算已经执行/触发入场的 BUY不把 WAIT/OBS 的发现后涨幅算收益 # 但 buy_now 只是入场窗口,不等于已成交;未触发入场前不计算收益。
assert live['executed_trade_count'] == 0
assert live['executed_pnl_sum'] == pytest.approx(0.0)
assert live['executed_avg_pnl'] == pytest.approx(0.0)
assert stats['active_pnl_sum'] == pytest.approx(0.0)
assert stats['active_avg_pnl'] == pytest.approx(0.0)
assert stats['active_success_count'] == 0
assert stats['active_pending_count'] == 0
def test_entry_triggered_buy_now_counts_as_executed_pnl(temp_db):
_insert_recommendation(
temp_db,
symbol='BUY/USDT',
action_status='可即刻买入',
entry_plan_json='{"entry_action": "可即刻买入"}',
pnl_pct=5.0,
max_pnl_pct=5.0,
)
conn = altcoin_db.sqlite3.connect(str(temp_db))
conn.execute("UPDATE recommendation SET entry_triggered=1 WHERE symbol='BUY/USDT'")
conn.commit()
conn.close()
stats = altcoin_db.get_stats()
live = stats['live_overview']
assert live['executed_trade_count'] == 1 assert live['executed_trade_count'] == 1
assert live['executed_pnl_sum'] == pytest.approx(5.0) assert live['executed_pnl_sum'] == pytest.approx(5.0)
assert live['executed_avg_pnl'] == pytest.approx(5.0)
assert stats['active_pnl_sum'] == pytest.approx(5.0)
assert stats['active_avg_pnl'] == pytest.approx(5.0)
assert stats['active_success_count'] == 1
assert stats['active_pending_count'] == 0
def test_active_api_marks_wait_plan_as_unexecuted_not_success(temp_db): def test_active_api_marks_wait_plan_as_unexecuted_not_success(temp_db):

View File

@ -36,7 +36,7 @@ class RecommendationHistoryBase(unittest.TestCase):
tp1=110.0, tp1=110.0,
tp2=118.0, tp2=118.0,
sector='AI', sector='AI',
signals='[]', signals=json.dumps(['🟢 15min即刻入场信号'], ensure_ascii=False),
is_meme=0, is_meme=0,
status='active', status='active',
current_price=100.0, current_price=100.0,
@ -60,6 +60,7 @@ class RecommendationHistoryBase(unittest.TestCase):
'tp2': 118.0, 'tp2': 118.0,
'rr1': 2.0, 'rr1': 2.0,
'rr2': 3.6, 'rr2': 3.6,
'entry_trigger_confirmed': True,
}, ensure_ascii=False), }, ensure_ascii=False),
action_status='可即刻买入', action_status='可即刻买入',
direction='多头启动', direction='多头启动',

View File

@ -52,7 +52,7 @@ def _insert_recommendation(db_path, **kwargs):
stopped_out_time="", stopped_out_time="",
expired_time="", expired_time="",
last_track_time="2026-05-01T10:10:00", last_track_time="2026-05-01T10:10:00",
entry_plan_json=json.dumps({"entry_price": 100.0, "entry_action": "可即刻买入"}, ensure_ascii=False), entry_plan_json=json.dumps({"entry_price": 100.0, "entry_action": "可即刻买入", "entry_trigger_confirmed": True}, ensure_ascii=False),
action_status="可即刻买入", action_status="可即刻买入",
direction="多头启动", direction="多头启动",
execution_status="buy_now", execution_status="buy_now",

View File

@ -53,6 +53,28 @@ def test_buy_now_with_bad_rr_sets_real_pullback_price():
assert any('现价不买' in r for r in reasons) assert any('现价不买' in r for r in reasons)
def test_buy_now_requires_current_trigger_and_no_bearish_flow_risk():
action, plan, reasons = apply_entry_quality_gate(
action_status='可即刻买入',
entry_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,
},
signals=['1H历史起爆点已过期(12根前)', '⚠️ 1H连续3K空头加速'],
current_price=1.0,
market_context={'change_24h': 2.0},
)
assert action != '可即刻买入'
assert any('缺少当前15min触发' in r for r in reasons)
assert any('空头加速' in r for r in reasons)
def test_tracker_gate_downgrade_removes_provisional_buy_signal(): def test_tracker_gate_downgrade_removes_provisional_buy_signal():
signals = reconcile_buy_signals_after_gate( signals = reconcile_buy_signals_after_gate(
[ [

View File

@ -31,7 +31,7 @@ class RecommendationExecutionStatusTests(unittest.TestCase):
tp1=110.0, tp1=110.0,
tp2=118.0, tp2=118.0,
sector='AI', sector='AI',
signals='[]', signals=json.dumps(['🟢 15min即刻入场信号'], ensure_ascii=False),
is_meme=0, is_meme=0,
status='active', status='active',
current_price=100.0, current_price=100.0,
@ -55,6 +55,7 @@ class RecommendationExecutionStatusTests(unittest.TestCase):
'tp2': 118.0, 'tp2': 118.0,
'rr1': 2.0, 'rr1': 2.0,
'rr2': 3.6, 'rr2': 3.6,
'entry_trigger_confirmed': True,
}, ensure_ascii=False), }, ensure_ascii=False),
action_status='可即刻买入', action_status='可即刻买入',
direction='多头启动', direction='多头启动',

View File

@ -35,7 +35,7 @@ class RecommendationSignalTrustTests(unittest.TestCase):
tp1=10.8, tp1=10.8,
tp2=11.4, tp2=11.4,
sector='', sector='',
signals=json.dumps(['15m 入场窗口信号'], ensure_ascii=False), signals=json.dumps(['🟢 15min即刻入场信号'], ensure_ascii=False),
is_meme=0, is_meme=0,
status='active', status='active',
current_price=10.0, current_price=10.0,
@ -53,6 +53,7 @@ class RecommendationSignalTrustTests(unittest.TestCase):
'entry_price': 10.0, 'entry_price': 10.0,
'entry_action': '可即刻买入', 'entry_action': '可即刻买入',
'risk_reward_ok': True, 'risk_reward_ok': True,
'entry_trigger_confirmed': True,
'rr1': 2.0, 'rr1': 2.0,
'stop_loss': 9.6, 'stop_loss': 9.6,
'tp1': 10.8, 'tp1': 10.8,