Quant review 2026-09-26: alert cooldown, plain-language alerts, M15 grade-A SELL block, 1R printed targets, panel/breach fixes
Live signals.db review (2026-09-04 → 09-25, 100 unique setups replayed on the scans' stored candles): the stop-hunt claim is refuted (71% of stop-outs ran ≥ 1R past the stop, the same as the books' 70–72%); −25.1R of the −37.8R raw live book were consecutive-bar re-entries the backtester's 5-bar cooldown never takes; the cooldown view sits at the 3rd–5th percentile of the books' three-week windows. Delivery (book-neutral): - telegram_bot: 5-bar alert cooldown per timeframe (_ALERT_COOLDOWN_BARS = backtest.cooldown_bars); setup block prints the trail first as the tested exit, TP1/TP2 as guides with their R, "Planned TP1 RR", and one validated-book line per setup class (agent_panel_base_rates.json); the informational 100-pip TP line is removed (owner request). - AI block rewritten for a new trader: "AI check: the odds look better than / worse than / about usual", the chance "in 100" against the usual odds, one reason for and one against, no verdict word or confidence figure; rendered whenever the verdict carries a probability. agent_panel: persona prompts ban trading jargon (PANEL_PROMPT_VERSION v2.1-2026-09-26); the macro-source verdict now carries its persona's p_win (every live v2 row had panel_probability NULL). Dashboard and guides mirror the wording. - telegram_bot: level-breach rescans skip the settlement-pause window (the 20:45 M15 bar was re-logged at 22:01/22:06/22:11 every evening). Signal (pre-registered rule-1 campaign, CUDA twin paired with gpu_backtest.py): - M15 grade-A SELLs blocked (sell_min_quality ["A+","B"], M15 override 6): W2 813/+154.15R PF 1.53 vs 931/+146.14R PF 1.43, OOS +68.5 vs +60.67, W3 max DD 20.9 vs 19.3R. PASSED a–f. - Printed target 1 floored at 1R in every generator (signals.tp_print_floor_rr), applied AFTER the min-RR and spread gates so no trade changes; mirrored in cuda_signal_kernels.py K2. Label-only re-pin of all four timeframes (only tp1/rr_planned columns move). A floor applied before the gate cost −24.6R and was rejected. - FAILED: M15 min_rr_ratio 1.0 (trade-identical, −2.0R). Owner's call: M5 tp_floor_rr 1.0 (+48.9R W2, max DD 10.1 → 15.9R), not shipped. New pins: M5 W2 e599c8a8…, M15 W2 5b65ecf6…, M15 OOS 09b8a8e7…, H1 W3 a44fd19b…, H4 W3 f3587bae…; twin and reference MD5s equal on every window. Base-rate table rebuilt from the new pins. Suite 558 passed. Evidence: backtest/research/rr_and_sell_campaign_2026-09-26.md (plus the 2026-09-18 loss-anatomy and wrong-direction reports it builds on).
This commit is contained in:
+256
-35
@@ -1475,6 +1475,56 @@ def _():
|
||||
# MODULE 6: main.py
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
@test("Printed-target floor: generic setup prints TP1 at >= 1R after the gates judged the raw target")
|
||||
def _():
|
||||
from engine import TechnicalAnalysis
|
||||
from signal_aggregator import SignalReport
|
||||
from pair_signals.xauusd import make_config
|
||||
|
||||
def build(print_floor):
|
||||
cfg = make_config(Timeframe.M5) # tp_floor_rr 0.5 on M5
|
||||
cfg.signals.tp_print_floor_rr = print_floor
|
||||
agg = SignalAggregator(cfg)
|
||||
r = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="2026-09-15 08:00:00+00:00")
|
||||
r.final_signal = SignalStrength.BUY
|
||||
r.signal_quality = "B"
|
||||
r.current_price = 3300.0
|
||||
r.final_score = 30.0
|
||||
# ATR 8: stop = swing low 3290 - 1 ATR = 3282 (18 = 2.25 ATR, above the
|
||||
# 2 ATR / $8 floors); the swing high at 3304 fails the 0.5R floor, so
|
||||
# TP1 = max(1.5 ATR, 0.5 x 18) = 12 -> 3312 (passes the $9 spread gate).
|
||||
r.primary_analysis = TechnicalAnalysis(
|
||||
symbol="XAU/USD", timeframe="5min", timestamp="t",
|
||||
key_levels={"atr": 8.0, "swing_low_1": 3290.0, "swing_high_1": 3304.0})
|
||||
return agg._generate_trade_setup(r, 0.10)
|
||||
|
||||
raw = build(0.0) # floor off: the historical geometry
|
||||
assert raw is not None and raw.strategy in (None, "GENERIC")
|
||||
assert abs(raw.stop_loss - 3282.0) < 1e-9 and abs(raw.take_profit_1 - 3312.0) < 1e-9
|
||||
assert raw.risk_reward == 0.67
|
||||
floored = build(1.0) # the shipped default
|
||||
assert floored is not None, "the floor must never veto a setup the gates accepted"
|
||||
assert abs(floored.stop_loss - raw.stop_loss) < 1e-9, "the stop (the trade) is unchanged"
|
||||
assert abs(floored.entry_price - raw.entry_price) < 1e-9
|
||||
assert abs(floored.take_profit_1 - 3318.0) < 1e-9, "TP1 printed at exactly 1R"
|
||||
assert floored.risk_reward == 1.0
|
||||
assert floored.take_profit_2 >= floored.take_profit_1 + 8.0 - 1e-9, "TP2 stays >= 1 ATR beyond TP1"
|
||||
assert abs(floored.reward_pips - 180.0) < 1e-9 and abs(floored.risk_pips - 180.0) < 1e-9
|
||||
# A target the gates reject stays rejected: ATR 4 -> TP1 6 < $9 spread gate,
|
||||
# with or without the printed floor (the floor runs after the gate).
|
||||
cfg = make_config(Timeframe.M5); agg = SignalAggregator(cfg)
|
||||
r = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="t")
|
||||
r.final_signal = SignalStrength.BUY; r.signal_quality = "B"; r.current_price = 3300.0; r.final_score = 30.0
|
||||
r.primary_analysis = TechnicalAnalysis(symbol="XAU/USD", timeframe="5min", timestamp="t",
|
||||
key_levels={"atr": 4.0, "swing_low_1": 3296.0})
|
||||
assert agg._generate_trade_setup(r, 0.10) is None
|
||||
assert any("SPREAD GATE" in w for w in r.warnings)
|
||||
# The effective floor is max(tp_floor_rr, tp_print_floor_rr).
|
||||
assert SignalAggregator(make_config(Timeframe.M15))._printed_target_floor() == 1.0
|
||||
m5 = make_config(Timeframe.M5); m5.signals.tp_print_floor_rr = 0.0
|
||||
assert SignalAggregator(m5)._printed_target_floor() == 0.5
|
||||
|
||||
|
||||
print("\n═══ Module 6: main.py ═══")
|
||||
|
||||
from main import (
|
||||
@@ -1716,34 +1766,19 @@ def _():
|
||||
assert "$7.50" in msg
|
||||
|
||||
|
||||
@test("format_signal_message renders 100-pip informational TP")
|
||||
@test("format_signal_message has no fixed-pip target line (removed 2026-09-26 by owner request)")
|
||||
def _():
|
||||
def _setup(direction="BUY", **extra):
|
||||
return TradeSetup(
|
||||
pair="XAU/USD", display_name="XAU/USD", direction=direction,
|
||||
entry_price=3300.0, stop_loss=3290.0, take_profit_1=3315.0,
|
||||
take_profit_2=3325.0, risk_reward=1.5, risk_pips=100,
|
||||
reward_pips=150, pip_value=0.10, **extra,
|
||||
)
|
||||
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
|
||||
report.final_signal = SignalStrength.BUY
|
||||
report.signal_quality = "A"
|
||||
# BUY: 100 pips × $0.10 = $10 above entry; risk $10 → exactly 1.0R.
|
||||
report.trade_setup = _setup()
|
||||
report.trade_setup = TradeSetup(
|
||||
pair="XAU/USD", display_name="XAU/USD", direction="BUY",
|
||||
entry_price=3300.0, stop_loss=3290.0, take_profit_1=3315.0,
|
||||
take_profit_2=3325.0, risk_reward=1.5, risk_pips=100,
|
||||
reward_pips=150, pip_value=0.10)
|
||||
msg = format_signal_message(report)
|
||||
assert "TP 100p" in msg
|
||||
assert "3310.00" in msg, "BUY 100p TP must be entry + $10.00"
|
||||
assert "≈1.0R" in msg
|
||||
# SELL mirror: $10 below entry.
|
||||
report.final_signal = SignalStrength.SELL
|
||||
report.trade_setup = _setup(direction="SELL")
|
||||
msg = format_signal_message(report)
|
||||
assert "TP 100p" in msg
|
||||
assert "3290.00" in msg, "SELL 100p TP must be entry - $10.00"
|
||||
# No setup → no informational TP line.
|
||||
report.trade_setup = None
|
||||
msg = format_signal_message(report)
|
||||
assert "TP 100p" not in msg
|
||||
assert "TP 100p" not in msg and "TP 100" not in msg
|
||||
assert not hasattr(__import__("telegram_bot"), "_INFO_TP_PIPS")
|
||||
|
||||
|
||||
@test("_indicator_summary condenses indicators to one arrow line")
|
||||
@@ -2117,6 +2152,8 @@ def _():
|
||||
"candlestick": 1.0, "divergence": 2.0, "adx": 1.0,
|
||||
},
|
||||
}
|
||||
# Grade-A SELLs blocked on M15 (2026-09-26 campaign).
|
||||
inline_cfg.xau_strategy.sell_min_quality = ["A+", "B"]
|
||||
if tf == Timeframe.M5:
|
||||
# Documented per-TF overrides (see xauusd.py docstring):
|
||||
# hour 9 UTC blocked, hour 21 UNBLOCKED (2026-07-30
|
||||
@@ -2618,6 +2655,120 @@ def _():
|
||||
d.pop(key, None)
|
||||
|
||||
|
||||
@test("Alert cooldown: no second alert within 5 bars of the last alerted entry (backtester rule)")
|
||||
def _():
|
||||
import telegram_bot as tb
|
||||
from signal_aggregator import SignalReport, TradeSetup
|
||||
|
||||
def rpt(bar, entry, quality="A"):
|
||||
r = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp=bar)
|
||||
r.final_signal = SignalStrength.SELL
|
||||
r.signal_quality = quality
|
||||
r.trade_setup = TradeSetup(
|
||||
pair="XAU/USD", display_name="XAU/USD", direction="SELL",
|
||||
entry_price=entry, stop_loss=entry + 30.0, take_profit_1=entry - 30.0,
|
||||
take_profit_2=entry - 45.0, risk_reward=1.0, risk_pips=300.0,
|
||||
reward_pips=300.0, pip_value=0.10, strategy="GENERIC")
|
||||
return r
|
||||
|
||||
key = "XAU/USD_15min" # the live key shape: symbol + tf.value
|
||||
dicts = (tb._last_alert_fp, tb._last_alert_ts, tb._last_alert_setup,
|
||||
tb._last_alert_quality, tb._last_alert_bar)
|
||||
saved = [dict(d) for d in dicts]
|
||||
for d in dicts:
|
||||
d.pop(key, None)
|
||||
try:
|
||||
assert tb._ALERT_COOLDOWN_BARS == 5, "must equal backtest.cooldown_bars"
|
||||
assert tb._key_bar_seconds(key) == 900
|
||||
assert tb._key_bar_seconds("XAU/USD_5min") == 300
|
||||
assert tb._key_bar_seconds("TEST_ALERT_FP") is None, "unknown key → no cooldown"
|
||||
# 2026-09-15: four consecutive M15 SELL setups as price fell (the
|
||||
# live log alerted them all; the backtester takes one).
|
||||
first = rpt("2026-09-15 08:00:00+00:00", 4300.0)
|
||||
assert tb._should_alert(key, first)
|
||||
assert not tb._inside_alert_cooldown(key, first), "nothing alerted yet"
|
||||
# A failed send does not commit → the next bar is still eligible.
|
||||
assert tb._should_alert(key, rpt("2026-09-15 08:15:00+00:00", 4280.0))
|
||||
tb._commit_alert(key, first)
|
||||
for bar, px in (("2026-09-15 08:15:00+00:00", 4280.0), # entry moved 0.67R → old rule re-alerted
|
||||
("2026-09-15 08:30:00+00:00", 4260.0),
|
||||
("2026-09-15 08:45:00+00:00", 4240.0),
|
||||
("2026-09-15 09:00:00+00:00", 4220.0)): # 4 bars later: still inside
|
||||
r = rpt(bar, px)
|
||||
assert tb._inside_alert_cooldown(key, r), bar
|
||||
assert not tb._should_alert(key, r), f"{bar} must be inside the 5-bar cooldown"
|
||||
# A grade upgrade does not break the cooldown either.
|
||||
assert not tb._should_alert(key, rpt("2026-09-15 08:30:00+00:00", 4260.0, "A+"))
|
||||
# 5 bars after the alerted entry the cooldown has lapsed; the
|
||||
# entry has moved far more than half the risk → re-alert.
|
||||
later = rpt("2026-09-15 09:15:00+00:00", 4200.0)
|
||||
assert not tb._inside_alert_cooldown(key, later)
|
||||
assert tb._should_alert(key, later)
|
||||
# A report without a setup is never held by the cooldown (it is
|
||||
# not an alert candidate anyway) and an unparsable bar label
|
||||
# disables the rule rather than blocking alerts.
|
||||
no_setup = rpt("2026-09-15 08:15:00+00:00", 4280.0); no_setup.trade_setup = None
|
||||
assert not tb._inside_alert_cooldown(key, no_setup)
|
||||
assert not tb._inside_alert_cooldown(key, rpt("not-a-time", 4280.0))
|
||||
# The cooldown is per key: the M5 loop is untouched by an M15 alert.
|
||||
assert not tb._inside_alert_cooldown("XAU/USD_5min", rpt("2026-09-15 08:05:00+00:00", 4280.0))
|
||||
# The bar label is recorded at commit time from the report.
|
||||
assert tb._last_alert_bar[key] == pd.Timestamp("2026-09-15 08:00:00", tz="UTC")
|
||||
finally:
|
||||
for d, old in zip(dicts, saved):
|
||||
d.clear(); d.update(old)
|
||||
|
||||
|
||||
@test("format_signal_message: trail first as the tested exit, TP1 a guide with its R, book line")
|
||||
def _():
|
||||
import telegram_bot as tb
|
||||
import agent_panel
|
||||
from signal_aggregator import SignalReport, TradeSetup
|
||||
from engine import TechnicalAnalysis
|
||||
|
||||
report = SignalReport(symbol="XAU/USD", display_name="XAU/USD", timestamp="now")
|
||||
report.final_signal = SignalStrength.BUY
|
||||
report.signal_quality = "A"
|
||||
report.primary_analysis = TechnicalAnalysis(symbol="XAU/USD", timeframe="15min", timestamp="now")
|
||||
# Grade-A gold ATR geometry: stop 2 ATR, TP1 1.5 ATR → planned RR 0.75.
|
||||
report.trade_setup = TradeSetup(
|
||||
pair="XAU/USD", display_name="XAU/USD", direction="BUY",
|
||||
entry_price=3300.0, stop_loss=3280.0, take_profit_1=3315.0,
|
||||
take_profit_2=3322.5, risk_reward=0.75, risk_pips=200.0,
|
||||
reward_pips=150.0, pip_value=0.10, strategy="XAU_ATR",
|
||||
trail_arm_r=0.70, trail_dist_r=0.75)
|
||||
saved = agent_panel._base_rates_cache
|
||||
agent_panel._base_rates_cache = {"classes": {
|
||||
"15min|XAU_ATR|A|BUY": {"n": 120, "win_rate": 55.0, "mean_r": 0.04},
|
||||
"15min|*|*|BUY": {"n": 635, "win_rate": 63.31, "mean_r": 0.2309},
|
||||
}}
|
||||
try:
|
||||
msg = tb.format_signal_message(report)
|
||||
assert "Exit rule (tested)" in msg
|
||||
assert "TP1 guide" in msg and "= 0.8R" in msg, "TP1 must carry its R (1.5 ATR / 2 ATR)"
|
||||
assert "TP2 guide" in msg and "(1.1R)" in msg
|
||||
assert "= 1R" in msg, "the SL line names 1R"
|
||||
assert "Planned TP1 RR 1:0.8" in msg and "the trail is the exit" in msg
|
||||
assert msg.index("Exit rule") < msg.index("TP1 guide") < msg.index("Planned TP1 RR"), \
|
||||
"the tested exit must be printed before the guides"
|
||||
assert "📚 Book (this setup class): 120 trades, 55% winners, avg +0.04R per trade" in msg
|
||||
assert "TP1 is a guide, let it run" not in msg, "old wording replaced"
|
||||
# Under 30 trades in the exact class → the wider direction class.
|
||||
agent_panel._base_rates_cache["classes"]["15min|XAU_ATR|A|BUY"]["n"] = 12
|
||||
msg = tb.format_signal_message(report)
|
||||
assert "Book (BUY setups on this timeframe): 635 trades, 63% winners, avg +0.23R" in msg
|
||||
# No table → no book line, and the message still renders.
|
||||
agent_panel._base_rates_cache = {}
|
||||
msg = tb.format_signal_message(report)
|
||||
assert "📚 Book" not in msg and "Exit rule (tested)" in msg
|
||||
# No primary analysis (hand-built report) → no book line, no error.
|
||||
agent_panel._base_rates_cache = {"classes": {"15min|*|*|*": {"n": 9, "win_rate": 1, "mean_r": 1}}}
|
||||
report.primary_analysis = None
|
||||
assert "📚 Book" not in tb.format_signal_message(report)
|
||||
finally:
|
||||
agent_panel._base_rates_cache = saved
|
||||
|
||||
|
||||
@test("Alert candidates: a quality-passing signal without a trade setup is skipped and logged")
|
||||
def _():
|
||||
import logging
|
||||
@@ -2917,11 +3068,25 @@ def _():
|
||||
assert ts.strategy == "XAU_CP" and ts.direction == "BUY"
|
||||
assert ts.setup_pattern == "double_bottom"
|
||||
assert ts.stop_loss < ts.entry_price
|
||||
assert abs((ts.take_profit_1 - ts.entry_price) - 19.0) < 1e-6, \
|
||||
"TP1 must be the measured move"
|
||||
# The measured move (19) is the gated target; the PRINTED TP1 is at
|
||||
# least 1R since 2026-09-26 (tp_print_floor_rr), never less than the
|
||||
# measured move.
|
||||
sl_dist = ts.entry_price - ts.stop_loss
|
||||
assert abs((ts.take_profit_1 - ts.entry_price) - max(19.0, sl_dist)) < 1e-6, \
|
||||
"printed TP1 must be max(measured move, 1R)"
|
||||
assert ts.risk_reward >= 1.0
|
||||
assert rep.signal_quality == "B"
|
||||
assert rep.bullish_reasons[0].startswith("XAU CHART PATTERN"), \
|
||||
"tag marker must sit at reasons[0]"
|
||||
# With the printed floor off, TP1 is exactly the measured move (the
|
||||
# historical geometry) and the stop is identical: the floor never
|
||||
# changes the trade.
|
||||
cfg2 = _cp_config(); cfg2.signals.tp_print_floor_rr = 0.0
|
||||
rep2 = _cp_report(df, cfg2)
|
||||
SignalAggregator(cfg2)._try_chart_pattern(rep2, df, 0.10)
|
||||
assert abs((rep2.trade_setup.take_profit_1 - rep2.trade_setup.entry_price) - 19.0) < 1e-6, \
|
||||
"TP1 must be the measured move when the printed floor is off"
|
||||
assert abs(rep2.trade_setup.stop_loss - ts.stop_loss) < 1e-9
|
||||
|
||||
|
||||
@test("CP gate: accepted setup removes stale vetoes and deduplicates extension context")
|
||||
@@ -5743,8 +5908,12 @@ def _():
|
||||
cfg = _mk(tf)
|
||||
assert cfg.xau_strategy.sell_blocked_regimes == [], \
|
||||
f"{tf}: sell_blocked_regimes must stay at the default off-M5"
|
||||
assert cfg.xau_strategy.sell_min_quality == ["A+", "A", "B"], \
|
||||
f"{tf}: sell_min_quality must stay at the default off-H1"
|
||||
# 2026-09-26 campaign: M15 blocks grade-A shorts (A+ and B still
|
||||
# trade); H4 keeps the default allow-list.
|
||||
assert _mk(Timeframe.M15).xau_strategy.sell_min_quality == ["A+", "B"], \
|
||||
"M15 must block grade-A SELLs (M15 override 6)"
|
||||
assert _mk(Timeframe.H4).xau_strategy.sell_min_quality == ["A+", "A", "B"], \
|
||||
"H4: sell_min_quality must stay at the default"
|
||||
|
||||
|
||||
@test("GPU parity: cache miss falls back LOUDLY (counter + warning)")
|
||||
@@ -6107,6 +6276,37 @@ def _():
|
||||
_aio21.run(main())
|
||||
|
||||
|
||||
@test("Breach rescan is skipped while the M15 bar to read sits in the settlement pause")
|
||||
def _():
|
||||
async def main():
|
||||
_m21c_reset()
|
||||
_tb21._autoscan_chats.add(911)
|
||||
calls = {"n": 0}
|
||||
good = _m21c_report()
|
||||
|
||||
async def working(*a, **k):
|
||||
calls["n"] += 1
|
||||
return good
|
||||
|
||||
bot = _M21cBot()
|
||||
with patch.object(_tb21, "run_signal", working):
|
||||
# 2026-09-22 22:01 UTC: the bar "just closed" is labelled 21:46,
|
||||
# inside the pause — the live log re-logged the 20:45 bar here.
|
||||
with patch.object(_tb21, "_closed_bar_in_settlement", lambda *a, **k: True):
|
||||
await _tb21._handle_breach(bot, "XAU/USD", 3301.0, "swing_high_1")
|
||||
assert calls["n"] == 0, "no rescan during the settlement pause"
|
||||
assert not bot.sent, "and no alert"
|
||||
assert "XAU/USD" not in _tb21._pending_breach, "nothing parked either"
|
||||
assert "XAU/USD" not in _tb21._scan_cooldowns, "the cooldown is not consumed"
|
||||
# Outside the pause the same crossing rescans as before.
|
||||
with patch.object(_tb21, "_closed_bar_in_settlement", lambda *a, **k: False):
|
||||
await _tb21._handle_breach(bot, "XAU/USD", 3301.0, "swing_high_1")
|
||||
assert calls["n"] == 1
|
||||
_m21c_reset()
|
||||
|
||||
_aio21.run(main())
|
||||
|
||||
|
||||
@test("H33c: breach reuses this bar's M15 scan; a new bar rescans once")
|
||||
def _():
|
||||
async def main():
|
||||
@@ -7049,6 +7249,15 @@ def _():
|
||||
assert v.source == "macro"
|
||||
assert [t.persona for t in v.takes] == \
|
||||
["macro", "technical", "risk", "contrarian"]
|
||||
# The macro take's p_win is the verdict's probability and reaches
|
||||
# the report (2026-09-26: every live v2 row under this source had
|
||||
# panel_probability NULL because the verdict carried none).
|
||||
macro_take = next(t for t in v.takes if t.persona == "macro")
|
||||
assert macro_take.p_win is not None # v1 reply -> pseudo-probability
|
||||
assert v.probability == round(float(macro_take.p_win), 1)
|
||||
rep = _panel_report()
|
||||
ap._write_fields(rep, v, "MISS")
|
||||
assert rep.panel_probability == v.probability and rep.panel_probability > 0
|
||||
finally:
|
||||
lc._client = None
|
||||
ap.reset_panel_state()
|
||||
@@ -13336,13 +13545,20 @@ def _():
|
||||
"p_win": 30.0, "reason_for": "f", "reason_against": "The stop is huge"},
|
||||
]
|
||||
msg = tb.format_signal_message(report)
|
||||
assert "AI estimate" in msg and "45%" in msg and "60%" in msg
|
||||
assert "Still being tested" in msg
|
||||
assert "Biggest worry: The stop is huge" in msg, "lowest p_win's reason_against"
|
||||
# 2026-09-26 owner rule: simple and clear for a new trader.
|
||||
assert "AI check: the odds look worse than usual." in msg, "45 vs usual 60"
|
||||
assert "about *45 in 100*" in msg and "usual for this kind of setup: 60 in 100" in msg
|
||||
assert "The move already ran far." in msg
|
||||
assert "Against: The stop is huge" in msg, "the take furthest from 50 gives the reasons"
|
||||
assert "macro 45%" in msg and "technical 65%" in msg and "risk 30%" in msg
|
||||
tail = msg.split("AI estimate", 1)[1]
|
||||
assert "Do not trade on it alone" in msg
|
||||
tail = msg.split("AI check", 1)[1]
|
||||
assert "NEUTRAL" not in tail and "AGREE" not in tail and "OPPOSE" not in tail, \
|
||||
"the v2 line shows a probability, never a verdict word"
|
||||
"the line shows a probability, never a verdict word"
|
||||
assert "Still being tested" not in msg and "Biggest worry" not in msg and "%* chance" not in msg
|
||||
assert tb._panel_odds_word(66.0, 60.0) == "the odds look about usual"
|
||||
assert tb._panel_odds_word(68.0, 60.0) == "the odds look better than usual"
|
||||
assert tb._panel_odds_word(45.0, None) == "the odds look about even"
|
||||
d = report_to_dict(report)
|
||||
assert d["panel_probability"] == 45.0 and d["panel_verdict_source"] == "mean"
|
||||
assert d["panel_prompt_version"] == ap.PANEL_PROMPT_VERSION
|
||||
@@ -13353,13 +13569,18 @@ def _():
|
||||
print_signal_report(report)
|
||||
out = buf.getvalue()
|
||||
assert "AI estimate" in out and "45%" in out and "against: The stop is huge" in out
|
||||
# legacy (macro) rendering is byte-for-byte the old format
|
||||
# a macro-source verdict WITH a probability (since 2026-09-26) renders
|
||||
# the same plain block; the legacy verdict line is only for a verdict
|
||||
# that carries no probability (v1 rows, the chair).
|
||||
report.panel_verdict_source = "macro"
|
||||
report.panel_verdict = "OPPOSE"
|
||||
report.panel_confidence = 64.0
|
||||
macro = tb.format_signal_message(report)
|
||||
assert "AI check" in macro and "OPPOSE" not in macro.split("AI check", 1)[1]
|
||||
report.panel_probability = 0.0
|
||||
legacy = tb.format_signal_message(report)
|
||||
assert "AI macro view" in legacy and "OPPOSE" in legacy and "(64%)" in legacy
|
||||
assert "AI estimate" not in legacy
|
||||
assert "AI check" not in legacy
|
||||
|
||||
|
||||
@test("app_config: agent_panel.verdict_source accepts mean, rejects others")
|
||||
@@ -13409,7 +13630,7 @@ def _():
|
||||
# single-persona rendering: no votes line, the headline carries the number
|
||||
import telegram_bot as tb
|
||||
msg = tb.format_signal_message(report)
|
||||
assert "AI estimate" in msg and "58%" in msg
|
||||
assert "AI check" in msg and "58 in 100" in msg
|
||||
assert "macro 58%" not in msg, "one persona: no separate votes line"
|
||||
assert "macro 58%" in tb.format_signal_message(report2)
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user