News informational-only: gate sentiment bonus/penalty off by default
Add NewsConfig.sentiment_affects_signals (default False), gating the +5 aligned-sentiment confidence bonus and the 10-pt news_conflict penalty. With event_risk_caps_signals already False, live decisions are now pure technical, matching the backtester (use_news=False). Warnings and the sentiment display are unchanged. Adds a gating regression test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FMaaPdrrrQuh1VvGQH1icN
This commit is contained in:
@@ -22,7 +22,7 @@ This file is the authoritative developer/agent reference. The human-facing docs
|
|||||||
| `pair_signals/__init__.py` | 52 | `PAIR_PRODUCERS` registry + `produce_signal()` |
|
| `pair_signals/__init__.py` | 52 | `PAIR_PRODUCERS` registry + `produce_signal()` |
|
||||||
| `pair_signals/xauusd.py` | 71 | XAU config factory + per-timeframe overrides (docstring = validation record) |
|
| `pair_signals/xauusd.py` | 71 | XAU config factory + per-timeframe overrides (docstring = validation record) |
|
||||||
| `main.py` | ~348 | CLI: `python main.py --pair XAUUSD --timeframe H1 --detail/--json` |
|
| `main.py` | ~348 | CLI: `python main.py --pair XAUUSD --timeframe H1 --detail/--json` |
|
||||||
| `test_all.py` | ~2100 | QA suite: **162 tests in 10 modules**, no live API needed. Must stay green. |
|
| `test_all.py` | ~2100 | QA suite: **190 tests in 13 modules**, no live API needed. Must stay green. |
|
||||||
| `backtest/gpu_backtest.py` | ~2231 | Bar-replay backtester (CuPy→NumPy fallback) using the real `SignalAggregator` |
|
| `backtest/gpu_backtest.py` | ~2231 | Bar-replay backtester (CuPy→NumPy fallback) using the real `SignalAggregator` |
|
||||||
| `backtest/diagnose_filters.py` | 609 | Filter-diagnosis tooling |
|
| `backtest/diagnose_filters.py` | 609 | Filter-diagnosis tooling |
|
||||||
| `backtest/research/` | — | Calendar research infra (`ff_calendar_scraper.py`, `calendar_store.py`) + result CSVs |
|
| `backtest/research/` | — | Calendar research infra (`ff_calendar_scraper.py`, `calendar_store.py`) + result CSVs |
|
||||||
@@ -84,7 +84,7 @@ EMA 9/21/50/200 (`ema_mode`: `"4ema"` default or `"2ema"` = 50/200 only); RSI 14
|
|||||||
| `weights`, `min_confidence`, `cooldown_bars`, `default_risk_pct`, `default_rr_ratio`, `atr_sl_multiplier` | — | **LEGACY / UNUSED** outside tests. The engine has its own weights; backtest cooldown lives in `BacktestConfig` |
|
| `weights`, `min_confidence`, `cooldown_bars`, `default_risk_pct`, `default_rr_ratio`, `atr_sl_multiplier` | — | **LEGACY / UNUSED** outside tests. The engine has its own weights; backtest cooldown lives in `BacktestConfig` |
|
||||||
|
|
||||||
### NewsConfig
|
### NewsConfig
|
||||||
Tracked currencies `["USD","XAU"]`; pre-event buffer 30 min, post-event 15 min (45 min after a *surprise* print); `event_risk_caps_signals=False` — **XAU default: event risk warns but does NOT cap confidence** (2026-07-11 audit: the caps suppressed trades worth +4.6..+7.2R/yr; XAU trades near USD releases outperform). Context cache TTL 600 s (must stay < pre-event buffer). 4 RSS feeds (investinglive, FXStreet, Google-News gold query, CNBC economy).
|
Tracked currencies `["USD","XAU"]`; pre-event buffer 30 min, post-event 15 min (45 min after a *surprise* print); `event_risk_caps_signals=False` — **XAU default: event risk warns but does NOT cap confidence** (2026-07-11 audit: the caps suppressed trades worth +4.6..+7.2R/yr; XAU trades near USD releases outperform); `sentiment_affects_signals=False` — **headline sentiment is informational only** (warnings still emitted; the +5 alignment bonus and 10-pt `news_conflict` penalty are gated off, so live decisions are pure technical, matching backtests). Context cache TTL 600 s (must stay < pre-event buffer). 4 RSS feeds (investinglive, FXStreet, Google-News gold query, CNBC economy).
|
||||||
|
|
||||||
### XauStrategyConfig (all XAU gates; `enabled=True`)
|
### XauStrategyConfig (all XAU gates; `enabled=True`)
|
||||||
| Field | Default | Notes |
|
| Field | Default | Notes |
|
||||||
@@ -182,9 +182,9 @@ Two entry points:
|
|||||||
4. News (if `use_news`): event risk (`clear`/`caution`/`danger`) + pair sentiment (−1..+1). Backtests pass `use_news=False`.
|
4. News (if `use_news`): event risk (`clear`/`caution`/`danger`) + pair sentiment (−1..+1). Backtests pass `use_news=False`.
|
||||||
5. Spread estimate = median(High−Low) of primary df.
|
5. Spread estimate = median(High−Low) of primary df.
|
||||||
6. **`_compute_final_signal`** (bonuses → event cap → penalties → thresholds → grade):
|
6. **`_compute_final_signal`** (bonuses → event cap → penalties → thresholds → grade):
|
||||||
- Bonuses: MTF agreement → +10 conf and `score = 0.6×primary + 0.4×mean(all TF scores)`; news sentiment aligned (|sent|>0.2) → +5; in a *directional* trend, RSI in pullback zone → +5, BB position favorable → +5 (mixed `trending` regime gets NO directional bonus).
|
- Bonuses: MTF agreement → +10 conf and `score = 0.6×primary + 0.4×mean(all TF scores)`; news sentiment aligned (|sent|>0.2) → +5 (gated by `news.sentiment_affects_signals`, False by default); in a *directional* trend, RSI in pullback zone → +5, BB position favorable → +5 (mixed `trending` regime gets NO directional bonus).
|
||||||
- Event risk `danger`: warning always; confidence hard-capped at 30 ONLY if `news.event_risk_caps_signals` (False by default — XAU-tuned).
|
- Event risk `danger`: warning always; confidence hard-capped at 30 ONLY if `news.event_risk_caps_signals` (False by default — XAU-tuned).
|
||||||
- **Penalty system (diminishing, replaces old additive stacking):** collect, sort by size desc, apply with decay ×1.0, ×0.5, ×0.25…, total capped at 30. Penalties: `mtf_disagreement` 15; `news_conflict` 10; `event_caution` 15 (config-gated); `counter_trend` 15; `extreme_vol` 10; `ema200_near` 5 (price within 0.5% of EMA200 — env `FB2_EMA_LO` overrides); `ema200_far` 5 (>2.5%); `exhaustion` 8 (price >3.5 ATR beyond nearest swing); `no_pullback` 8 (>2.5 ATR from EMA21 in trend) / `weak_pullback` 5 (>2.0); `rsi_extended` 5 (RSI>65 with BUY / <35 with SELL in any strong-ADX regime, both directions in mixed `trending`); `bb_overextended` 8 (BB pos >0.85 BUY / <0.15 SELL).
|
- **Penalty system (diminishing, replaces old additive stacking):** collect, sort by size desc, apply with decay ×1.0, ×0.5, ×0.25…, total capped at 30. Penalties: `mtf_disagreement` 15; `news_conflict` 10 (gated by `sentiment_affects_signals`, off by default — warning still emitted); `event_caution` 15 (config-gated); `counter_trend` 15; `extreme_vol` 10; `ema200_near` 5 (price within 0.5% of EMA200 — env `FB2_EMA_LO` overrides); `ema200_far` 5 (>2.5%); `exhaustion` 8 (price >3.5 ATR beyond nearest swing); `no_pullback` 8 (>2.5 ATR from EMA21 in trend) / `weak_pullback` 5 (>2.0); `rsi_extended` 5 (RSI>65 with BUY / <35 with SELL in any strong-ADX regime, both directions in mixed `trending`); `bb_overextended` 8 (BB pos >0.85 BUY / <0.15 SELL).
|
||||||
- **Exhaustion hard block**: ≥2 exhaustion-tagged penalties ({exhaustion, no_pullback, rsi_extended, bb_overextended} → `report.exhaustion_count`) AND momentum fading (ROC sign against score, or MACD signal against score) → confidence = 0.
|
- **Exhaustion hard block**: ≥2 exhaustion-tagged penalties ({exhaustion, no_pullback, rsi_extended, bb_overextended} → `report.exhaustion_count`) AND momentum fading (ROC sign against score, or MACD signal against score) → confidence = 0.
|
||||||
- Confidence clamped [0, 95]. **Final signal**: score ≥35 & conf ≥55 STRONG_BUY; ≥25 & ≥50 BUY; mirrored sells; else NEUTRAL.
|
- Confidence clamped [0, 95]. **Final signal**: score ≥35 & conf ≥55 STRONG_BUY; ≥25 & ≥50 BUY; mirrored sells; else NEUTRAL.
|
||||||
- **Quality grade**: |score|≥40 & conf≥65 & MTF → **A+**; ≥33 & ≥55 & MTF → **A**; ≥28 & ≥50 → **B**; ≥25 & ≥45 → **C**; ≥15 → **D**; else **F**.
|
- **Quality grade**: |score|≥40 & conf≥65 & MTF → **A+**; ≥33 & ≥55 & MTF → **A**; ≥28 & ≥50 → **B**; ≥25 & ≥45 → **C**; ≥15 → **D**; else **F**.
|
||||||
@@ -314,7 +314,7 @@ Data: paginated TD fetches (5000-bar chunks) cached as parquet in `backtest/cach
|
|||||||
|
|
||||||
## test_all.py — QA suite
|
## test_all.py — QA suite
|
||||||
|
|
||||||
`python test_all.py` — **162 tests, 10 modules** (config, data_provider, engine, news_engine, signal_aggregator, main, telegram_bot, integration, pair_signals registry + engine-bug regressions, 2026-07-16 audit-fix regressions). No network needed (mocked). Uses a custom `@test("name")` decorator that runs immediately (NOT pytest — don't grep for `def test_`). Exit code non-zero on failure. Includes: the xauusd-factory ≡ inline-config equivalence test, engine bug-fix regression tests (Module 9b), and a live-vs-GPU engine parity sweep incl. the EMA-ribbon path (Module 11). **Any signal-logic change must keep this green and should add tests.**
|
`python test_all.py` — **190 tests, 13 modules** (config, data_provider, engine, news_engine, signal_aggregator, main, telegram_bot, integration, pair_signals registry + engine-bug regressions, 2026-07-16 audit-fix regressions, chart patterns, web_publisher). No network needed (mocked). Uses a custom `@test("name")` decorator that runs immediately (NOT pytest — don't grep for `def test_`). Exit code non-zero on failure. Includes: the xauusd-factory ≡ inline-config equivalence test, engine bug-fix regression tests (Module 9b), and a live-vs-GPU engine parity sweep incl. the EMA-ribbon path (Module 11). **Any signal-logic change must keep this green and should add tests.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,11 @@ class NewsConfig:
|
|||||||
# entered near USD releases OUTPERFORM (+0.475R vs +0.256R avg).
|
# entered near USD releases OUTPERFORM (+0.475R vs +0.256R avg).
|
||||||
# Audit 2026-07-11 via backtest/research/calendar_store.
|
# Audit 2026-07-11 via backtest/research/calendar_store.
|
||||||
event_risk_caps_signals: bool = False
|
event_risk_caps_signals: bool = False
|
||||||
|
# Whether headline sentiment moves the SIGNAL (aligned: +5 confidence;
|
||||||
|
# conflicting: 10-point penalty). Warnings are always emitted
|
||||||
|
# regardless. Default False = news is informational only; decisions
|
||||||
|
# are pure technical, matching the backtester (use_news=False).
|
||||||
|
sentiment_affects_signals: bool = False
|
||||||
# Wider stay-out window after a high-impact release that surprised
|
# Wider stay-out window after a high-impact release that surprised
|
||||||
# vs forecast (inline releases use post_event_buffer_minutes).
|
# vs forecast (inline releases use post_event_buffer_minutes).
|
||||||
post_event_surprise_buffer_minutes: int = 45
|
post_event_surprise_buffer_minutes: int = 45
|
||||||
|
|||||||
@@ -1079,7 +1079,7 @@ class SignalAggregator:
|
|||||||
mtf_avg = sum(all_scores) / len(all_scores)
|
mtf_avg = sum(all_scores) / len(all_scores)
|
||||||
score = score * 0.6 + mtf_avg * 0.4
|
score = score * 0.6 + mtf_avg * 0.4
|
||||||
|
|
||||||
if abs(report.news_sentiment) > 0.2:
|
if self.config.news.sentiment_affects_signals and abs(report.news_sentiment) > 0.2:
|
||||||
if (report.news_sentiment > 0 and score > 0) or \
|
if (report.news_sentiment > 0 and score > 0) or \
|
||||||
(report.news_sentiment < 0 and score < 0):
|
(report.news_sentiment < 0 and score < 0):
|
||||||
confidence += 5
|
confidence += 5
|
||||||
@@ -1129,6 +1129,7 @@ class SignalAggregator:
|
|||||||
if (report.news_sentiment > 0 and score < -10) or \
|
if (report.news_sentiment > 0 and score < -10) or \
|
||||||
(report.news_sentiment < 0 and score > 10):
|
(report.news_sentiment < 0 and score > 10):
|
||||||
report.warnings.append("News sentiment CONFLICTS with technical signal")
|
report.warnings.append("News sentiment CONFLICTS with technical signal")
|
||||||
|
if self.config.news.sentiment_affects_signals:
|
||||||
penalties.append(("news_conflict", 10))
|
penalties.append(("news_conflict", 10))
|
||||||
|
|
||||||
if report.event_risk == "caution":
|
if report.event_risk == "caution":
|
||||||
|
|||||||
+32
@@ -1240,6 +1240,38 @@ def _():
|
|||||||
assert PlatformConfig().news.event_risk_caps_signals is False
|
assert PlatformConfig().news.event_risk_caps_signals is False
|
||||||
|
|
||||||
|
|
||||||
|
@test("Sentiment signal gating obeys news.sentiment_affects_signals")
|
||||||
|
def _():
|
||||||
|
config = PlatformConfig()
|
||||||
|
agg = SignalAggregator(config)
|
||||||
|
dfs = {
|
||||||
|
Timeframe.H1: make_ohlcv(300, seed=42),
|
||||||
|
Timeframe.M15: make_ohlcv(300, seed=43),
|
||||||
|
Timeframe.H4: make_ohlcv(300, seed=44),
|
||||||
|
}
|
||||||
|
report = agg.analyze_from_dataframes("XAU/USD", dfs, use_news=False)
|
||||||
|
|
||||||
|
def recompute(sentiment, affects):
|
||||||
|
config.news.sentiment_affects_signals = affects
|
||||||
|
report.news_sentiment = sentiment
|
||||||
|
report.warnings = []
|
||||||
|
agg._compute_final_signal(report)
|
||||||
|
return report.final_confidence
|
||||||
|
|
||||||
|
score_sign = 1 if report.primary_score >= 0 else -1
|
||||||
|
aligned, conflicting = 0.5 * score_sign, -0.5 * score_sign
|
||||||
|
|
||||||
|
baseline = recompute(0.0, False)
|
||||||
|
# default: neither aligned sentiment nor conflict moves confidence
|
||||||
|
assert recompute(aligned, False) == baseline, "aligned sentiment must not add confidence"
|
||||||
|
assert recompute(conflicting, False) == baseline, "conflicting sentiment must not penalize"
|
||||||
|
assert any("CONFLICTS" in w for w in report.warnings), "conflict warning must persist"
|
||||||
|
# flag on: aligned sentiment adds confidence again
|
||||||
|
assert recompute(aligned, True) > baseline, "flag on must restore the alignment bonus"
|
||||||
|
# PlatformConfig default is off (news is informational only)
|
||||||
|
assert PlatformConfig().news.sentiment_affects_signals is False
|
||||||
|
|
||||||
|
|
||||||
@test("SignalAggregator._generate_trade_setup BUY direction")
|
@test("SignalAggregator._generate_trade_setup BUY direction")
|
||||||
def _():
|
def _():
|
||||||
config = PlatformConfig()
|
config = PlatformConfig()
|
||||||
|
|||||||
Reference in New Issue
Block a user