TF-IDF baseline (news_sentiment.py, models/xauusd_sentiment.joblib) and the two-head DeBERTa-v3-small transformer (transformer/, news_transformer.py) with its Google News crawler and DeepSeek labeller (data_collect/), the golden-set and archive evals (eval/), and the 2026-09-22 and 2026-09-27 results and progress notes. Transformer weights (model.pt, model.onnx) and the crawl database are not tracked; rebuild with transformer/train.py and transformer/export_onnx.py. The DeepSeek label cache in data/ is tracked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
8.6 KiB
ML-NEWS scikit model vs DeepSeek — first evaluation, 2026-09-17
Model under test: models/xauusd_sentiment.joblib (TF-IDF word + char,
logistic regression, trained 2026-09-17 on 606 DeepSeek-labelled
gold headlines from Sep 8–17). Teacher: DeepSeek deepseek-v4-flash
through ForexBot2's live prompt (sha1 d32e04c6). Scripts and raw
outputs: this folder (results/*_20260917_22*). DeepSeek spend for the
whole exercise: 11 calls.
Verdict
The model is a partial copy of DeepSeek, not a replacement. It has learned what gold-specific headline words mean and nothing else: perfect on direct gold news and safe-haven stories, near random on macro doctrine (hawkish/dovish Fed, dollar, yields, risk-on), zero on negation, and it assigns a direction to irrelevant headlines. DeepSeek is still 78/78 on the independent set today.
| Test | Truth | Model | DeepSeek | Keyword fallback |
|---|---|---|---|---|
| Golden set, 78 reviewer-labelled headlines, 3-class | human | 39/78 = 50.0% | 78/78 = 100% | 24/78 = 30.8% |
| Golden set, directional rows only (70), right sign | human | 38/70 = 54.3% | 70/70 | 16/70 |
| Archive, expanding daily folds, pooled OOF (567 rows), 3-class | DeepSeek | 64.4% | (is the label) | 44.4% |
| Archive OOF, sign agreement when both lean | DeepSeek | 74.8% | 86.8% (on 152 rows only) | |
| Live feed, last 24 h, 66 headlines not in training, 3-class | DeepSeek | 57.6% (66.7% headline-only) | 39.4% | |
| Live feed, gold-mentioning and not in training (37) | DeepSeek | 70.3% (75.7% headline-only) | 35.1% | |
| DeepSeek re-scored 100 archived headlines (teacher self-agreement) | DeepSeek (earlier) | 90.0% 3-class, 98.6% sign |
Paired McNemar on the golden set: model vs DeepSeek 0 vs 39 discordant (p < 0.0001); model vs keyword 28 vs 13 (p = 0.028). The model beats the keyword fallback and loses to DeepSeek by a wide margin.
Golden set by category (hits / n): model | DeepSeek | keyword
distractors 1/8 | 8/8 | 8/8 irrelevant news gets a direction
dovish_fed 6/8 | 8/8 | 7/8
gold_direct 8/8 | 8/8 | 4/8 learned
hawkish_fed 3/8 | 8/8 | 2/8 "Hot CPI fuels Fed tightening" -> bullish
inverted_metrics 3/6 | 6/6 | 0/6
negation_traps 0/8 | 8/8 | 2/8 "signals no rate hike" -> bearish 0.85 conf
real_yields 3/6 | 6/6 | 1/6
risk_on 1/4 | 4/4 | 0/4
safe_haven 8/8 | 8/8 | 0/8 learned
strong_usd 2/7 | 7/7 | 0/7 "Greenback rallies" -> bullish
weak_usd 4/7 | 7/7 | 0/7
The model's confidence does not flag its errors on this set: the negation-trap miss above carries 0.85 confidence and the "consumer confidence surges" miss 0.82.
Archive time-split (retrain on days < d, score day d)
day train test | full acc F1 | head acc F1 | keyword
09-10 49 87 | 62.1% 0.626 | 51.7% 0.521 | 47.1%
09-11 136 91 | 52.7% 0.524 | 53.8% 0.525 | 39.6%
09-14 227 130 | 76.9% 0.714 | 72.3% 0.656 | 58.5%
09-15 357 84 | 69.0% 0.656 | 61.9% 0.605 | 44.0%
09-16 441 115 | 55.7% 0.545 | 54.8% 0.531 | 33.0%
09-17 556 60 | 68.3% 0.643 | 75.0% 0.703 | 40.0%
pooled — 567 | 64.4% 0.636 | 61.4% 0.607 | 44.4%
"full" = headline + description (the shipped recipe), "head" = headline only. Agreement does not rise with more training days yet; the day-to-day swing (53–77%) is larger than any trend. In-sample the shipped bundle scores 97.4%, so the gap to 64% is generalisation, not capacity. The description helps a little on the archive folds and hurts on the live sample (60.5% vs 68.4%); both differences are inside the noise of these sample sizes.
Ceiling: DeepSeek disagrees with itself
Re-sending 100 archived headlines today (same prompt, same model, temperature 0, different batch neighbours): 90% same class, 93% directional, 98.6% same sign, but only 42% the identical number, mean |difference| 0.09. A student cannot exceed about 90% class agreement on this label. Where DeepSeek changed its class (9 rows) the OOF model matched one of its two answers on 7.
Confidence routing (pooled OOF, shipped recipe)
conf >= kept share kept agreement hybrid agreement (rest to DeepSeek)
0.60 44% 80.9% 91.5%
0.70 28% 87.4% 96.5%
0.80 17% 95.8% 99.3%
0.90 6.5% 100.0% 100.0%
The WORKFLOW doc's 0.70 / 0.20 starting point keeps 28% of headlines local at 87% agreement with DeepSeek. That saves roughly a quarter of the calls, which are 56 ms and a fraction of a cent per headline in batches of 25 anyway. The golden set shows the caveat: high confidence on a macro or negated headline is not a reliable signal there.
Structural findings
- Training covers gold-mentioning headlines only. The archive
stores DeepSeek's XAU score only when the headline matches a gold
keyword; for USD-only headlines only the overall score is kept and
DeepSeek's xau is discarded (
_apply_llm_sentiment). So the model never saw a Fed, dollar or yields headline that did not also say "gold", which is exactly where it fails. Live, 29 of the 76 headlines were USD-only; the model was at 60% on them versus 72–77% on gold-mentioning ones. Archiving DeepSeek's xau for every headline would fix the training set at no extra cost. - Nine days of data spanning one regime (a Fed hiking cycle in September 2026). The 2026-09-08 rule stands: do not deploy before about 4,000 labelled headlines over three or more weeks.
- Negation and doctrine are not learnable from 606 bag-of-words examples; the regex domain markers catch "dollar ... strengthen" but not "greenback rallies" or "dollar index hits high".
- Latency: model 1 ms per headline in-process; DeepSeek 56 ms per headline (1.4 s per batch of 25). Latency is not the constraint.
Addendum: backfilling the missing gold labels (same day)
backfill_labels.py sent the 961 archive headlines without a gold
label (930 DeepSeek-scored without a gold key, 31 never scored) through
the live DeepSeek path: 38 calls, all labelled, kept in
results/backfill_labels.jsonl. Macro labels at ±0.05: 350 bearish,
228 bullish, 383 neutral (40% neutral versus 22% on gold headlines).
| Training set | Gold-headline OOF | Macro-headline OOF | Golden set |
|---|---|---|---|
| gold rows only (616, shipped data) | 64.4% | 38.4% | 39/78 |
| gold + macro rows (1,577) | 61.6% | 51.8% | 42/78 |
Adding the macro headlines lifts macro agreement by 13 points but costs 3 points on gold headlines, and the golden-set change is a reshuffle, not a fix: distractors 1→6, hawkish 3→5, strong-USD 2→4, weak-USD 4→6, but dovish 6→3, safe-haven 8→4, risk-on 1→0. On nine days of one regime the linear model cannot hold both worlds at once. The missing-label problem was real, but repairing it on the existing corpus does not produce a usable model; it makes the corpus complete so that weeks of further collection can.
Side finding for the live system: on the 930 macro headlines, the gold
reading the platform displays for a non-gold headline (the inverted
overall score, _gold_direction_label) matches DeepSeek's own gold
score only 61% of the time (sign 77%). Display-only, but the macro
persona and the summary read that label.
experiment_recipe.py (same day): stripping the publisher suffix,
marking negated words and weighting confident labels moved OOF
agreement by less than a point and the golden set to 40–42/78;
negation traps went from 0/8 to 2/8. Cleaning is not the bottleneck.
After the archive patch (drafted the same day in ForexBot2)
Once the ForexBot2 patch is live, logs/newsdata.db gains a
xau_score column holding DeepSeek's gold score for every LLM-scored
row, and headlines naming no tracked currency arrive with
currencies_json = []. news_sentiment.py still reads its label from
score_by_currency_json["XAU"], so it will keep seeing only the
gold-keyword rows until its loader is switched to xau_score
(fall back to the JSON key for rows older than the column). Skip rows
whose source is a test feed (feed.example, good.example): the
ForexBot2 test suite used to write fixture headlines into the real
archive; they are keyword-scored, so the score_source = 'llm' filter
already excludes them.
How to keep measuring
eval_live.py appends every fresh headline to
results/live_shadow.jsonl; run it daily and the out-of-sample sample
grows for free (cached DeepSeek answers are never re-bought). Re-run
eval_golden.py after any retrain; it is the only independent truth.