fix bug
This commit is contained in:
parent
e80431eb89
commit
821af1282e
@ -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 已达标时,应转为入场窗口,不能继续显示“等回踩”。
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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: 触发时效治理,旧形态只作背景,消息触发显式标记'
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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='多头启动',
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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(
|
||||||
[
|
[
|
||||||
|
|||||||
@ -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='多头启动',
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user