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.
16 KiB
Two-head transformer trained on history + archive, 2026-09-27
Outcome: the transformer passes all four promotion criteria and is exported to models/xau_transformer/.
The TF-IDF bundle models/xauusd_sentiment.joblib is untouched, and nothing in ForexBot2 imports either
model, so nothing changed live.
DeepSeek spend today: 560 calls in four runs (20 pilot, 200 full run, 140 precise top-up, 200 growth
set; 15 batches failed and were skipped), labelling 13,625 crawled headlines. Every eval ran with
--no-network.
The shipped bundle is from round 3 (see the last section): 17,245 training rows, golden 72/78, with the two weak categories fixed. Round 1 (8,870 rows, 65/78) and round 2 (12,270 rows, 67/78) are kept below because round 1's val numbers are the cleanest comparison against TF-IDF.
Data
Labelling the crawl
The precise crawl (intitle:<term> site:<publisher>) was resumed and ran in the background all session.
Two DeepSeek runs labelled its pool, plus a 30% share of the broad pool for the not-relevant side:
| Pool | Labelled | Gold-relevant (|xau| ≥ 0.25) | Bull / bear among relevant |
|---|---|---|---|
| precise (intitle:) | 2,979 | 1,390 (47%) | 870 / 520 |
| broad (keyword anywhere) | 3,271 | 320 (10%) | 253 / 67 |
| history total | 6,250 | 1,710 | |
- The 20-call pilot on the precise pool alone gave 52% relevant against 7% for the broad pilot on 2026-09-23. Spot checks looked right ("Dollar slumps as risk appetite rebounds" +0.50, "This is what a $52 million Hamptons beach house looks like" 0.00).
- Relevant share by query term: "rate cut" 87%, "safe haven" 88%, payrolls 80%, "rate hike" 72%, yields 65%, Fed 61%, gold 49%, dollar 44%, inflation 38%, Powell 31%.
- Hindsight check: mean xau per year is +0.02 to +0.06 for 2019–2024, +0.12 for 2025 and 0.00 for 2026. 2025 was the big gold rally, so there may be some hindsight in DeepSeek's 2025 labels, but the effect is small and the direction head is trained on sign only.
- About 1,900 precise headlines are still unlabelled because the crawl kept adding them; it needs roughly 80 more calls.
Training set (transformer/train.py)
| Rows | |
|---|---|
| history labels | 6,250 |
| newsdata Sep 8–22 + backfill | 2,637 |
| unique after dedupe, golden removed | 8,870 |
| Split | n | relevant | irrelevant | grey | bull | bear |
|---|---|---|---|---|---|---|
| train | 7,460 | 2,332 | 4,492 | 636 | 1,290 | 1,042 |
| dev (random 5%, picks epoch + threshold) | 392 | 119 | 245 | 28 | 72 | 47 |
| val (newsdata ≥ 2026-09-18, never used to choose) | 1,018 | 378 | 501 | 139 | 171 | 207 |
Sweep on the enlarged set (microsoft/deberta-v3-small, lr 5e-5 unless noted)
| Run | Dev 3-class | Best epoch / thr | Val 3-class | Val macro-F1 | Val rel F1 | Val dir acc |
|---|---|---|---|---|---|---|
| 4 ep, bs 32 | 0.824 | 4 / 0.25 | 0.708 | 0.704 | 0.830 | 0.907 |
| 4 ep, bs 64 (chosen) | 0.824 | 3 / 0.60 | 0.720 | 0.715 | 0.841 | 0.942 |
| 6 ep, bs 32 | 0.814 | 4 / 0.20 | 0.713 | 0.708 | 0.842 | 0.897 |
| 8 ep, bs 64 | 0.821 | 8 / 0.40 | 0.709 | 0.704 | 0.836 | 0.905 |
| 6 ep, bs 32, lr 3e-5 | 0.821 | 6 / 0.20 | 0.714 | 0.711 | 0.834 | 0.926 |
The two 4-epoch runs tie on dev; bs 64 has the higher dev relevance F1 (0.869 vs 0.865) and is the script default, so it was chosen. All five runs land within 1.2 points on val: the data did the work, not the hyperparameters.
For reference, the same sweep on the 3,620-row set before labelling (earlier today) gave dev 0.72–0.78 and val 0.62–0.68, and the chosen model then scored 49/78 golden. deberta-v3-base was also tried on the small set: half a dev point better, but 38.6 ms per headline on 1 thread, over the 20 ms limit.
Same unseen rows: Sep 18–22 (DeepSeek label at ±0.05, non-final model)
| Model | All (n=1,022) | Gold-key (313) | Macro (709) | Rel P / R / F1 | Dir acc on relevant |
|---|---|---|---|---|---|
TF-IDF all 09-22 (promoted) |
62.8% | 71.9% | 58.8% | ||
| Transformer on 3,620 rows | 65.7% (F1 0.656) | 75.1% | 61.5% | 0.685 / 0.895 / 0.776 | 89.8% |
| Transformer on 8,870 rows | 71.9% (F1 0.714) | 83.7% | 66.7% | 0.802 / 0.885 / 0.841 | 94.2% |
The DeepSeek self-agreement ceiling is about 90%, so 72% is 18 points short of "as good as DeepSeek".
Golden set (78 reviewer-labelled headlines, argmax label)
| Model | 3-class | McNemar vs keyword | Spearman vs DeepSeek |
|---|---|---|---|
TF-IDF all 09-22 |
47/78 (60.3%) | 31 vs 8, p = 0.0003 | +0.423 |
| Transformer on 3,620 rows | 49/78 (62.8%) | 30 vs 5 | +0.556 |
| Transformer on 8,870 rows, non-final | 62/78 (79.5%) | 40 vs 2 | +0.676 |
Transformer, --final (shipped) |
65/78 (83.3%) | 42 vs 1 | +0.775 |
| DeepSeek | 78/78 |
The final model also trained on dev and val, but never on the golden set, so its golden score is a fair number. It is 3 headlines above the non-final model, which is within noise for 78 rows.
Hits by category:
| Category | TF-IDF | 3,620 rows | 8,870 rows | final |
|---|---|---|---|---|
| distractors | 5/8 | 7/8 | 8/8 | 8/8 |
| safe_haven | 7/8 | 8/8 | 7/8 | 8/8 |
| dovish_fed | 4/8 | 5/8 | 7/8 | 8/8 |
| real_yields | 3/6 | 4/6 | 5/6 | 6/6 |
| negation_traps | 1/8 | 3/8 | 6/8 | 7/8 |
| gold_direct | 7/8 | 7/8 | 7/8 | 7/8 |
| hawkish_fed | 8/8 | 7/8 | 7/8 | 7/8 |
| weak_usd | 4/7 | 5/7 | 6/7 | 6/7 |
| inverted_metrics | 3/6 | 2/6 | 6/6 | 4/6 |
| strong_usd | 5/7 | 1/7 | 3/7 | 3/7 |
| risk_on | 0/4 | 0/4 | 0/4 | 1/4 |
Remaining weak spots of the shipped model:
- Risk-on is 1/4. "Stocks roar to record highs as trade deal signed" and "Global equities rally on strong earnings and easing tensions" are bullish at 0.7–0.8. The model reads "rally" as gold rallying.
- Strong US data is 3/7. "Consumer confidence surges to post-pandemic high" → bullish 0.83, "ISM services index jumps" → bullish 0.52. Good-news-for-the-economy still doesn't map to bad-for-gold reliably. The precise crawl has no query for growth data (GDP, ISM, retail sales, confidence); adding one is the obvious next crawl.
- "FOMC minutes reveal growing support for policy easing" is still bearish at 0.63.
Export (transformer/export_onnx.py, from torch_final)
models/xau_transformer/model.onnx, 271 MB, int8 embedding table, fp32 compute.tokenizer.json,meta.json(final: true, threshold 0.6,export_checksinside).- Parity over 500 val headlines: tokenizer 500/500, mean |diff| 0.003–0.004, 0 decision flips, max
|diff| 0.075 on
p_relevantand 0.059 onp_bullish. The script printsPARITY FAILEDbecause the max is over its 0.05 tolerance. With zero flips the tolerance is the problem, not the export; it should be loosened to about 0.15, or the check should be flips only. - CPU latency: 18.9 ms on 1 thread, 9.6 ms on 16 threads, about 8.5 ms in the evals (4 threads).
Promotion criteria (fixed on 2026-09-22)
| Criterion | Target | Non-final | Final | |
|---|---|---|---|---|
| Golden set | ≥ 55/78 | 62/78 | 65/78 | pass |
| Distractors | ≥ 6/8 | 8/8 | 8/8 | pass |
| Sep 18–22 three-class agreement | ≥ 62.8% | 71.9% | (in-sample) | pass |
| CPU latency, 1 thread | ≤ 20 ms | 19.0 ms | 18.9 ms | pass |
All four pass. train.py --final was run (8,870 rows, 3 epochs) and the shipped ONNX comes from
torch_final. The non-final weights (the ones the val numbers above belong to) are in
models/xau_transformer/torch/.
What this does and does not mean:
- It is a better DeepSeek imitation than TF-IDF: +18 golden headlines, +9 points on unseen days, +35 points of Spearman.
- It still learns DeepSeek's opinion, not price moves, and DeepSeek itself only agrees with itself 90% of the time.
- The "about 4,000 labels over 3+ weeks" deployment rule is met on count (8,870) but the live-feed part is still one fortnight; the history labels are a different distribution (Reuters/CNBC/MarketWatch titles, not the bot's feeds). Shadow mode first.
Next
- Crawl growth data: add
intitle:queries for GDP, "retail sales", ISM, "consumer confidence", "jobless claims" andstocks OR equities(for risk-on), then label them (about 100–200 calls). - Finish labelling the precise pool: about 1,900 headlines left, roughly 80 calls, plus whatever the crawl adds. The crawl is still running; it has about 3,300 windows to go.
- Retrain weekly as the live archive grows; the whole train → export → eval loop takes under 5 minutes.
- Loosen the export tolerance or make it flips-only.
- Wire into ForexBot2 in shadow mode with
threads=4, DeepSeek still the answer. Decide the promotion rule for that separately.
Round 2: labelling the rest of the precise pool (same day)
The crawl kept running, so after round 1 there were about 3,500 unlabelled precise headlines. A 140-call
run with --broad-share 0 labelled 3,400 of them (4 batches failed). History labels: 9,650, of which
6,185 precise (47% relevant, 1,955 bull / 941 bear) and 3,465 broad (12% relevant). About 570 precise
headlines were still unlabelled at the end because the crawl was still adding them.
Training set: 12,270 unique headlines. Train 10,690 / dev 562 / val 1,018 (val unchanged).
Default config only (lr 5e-5, 4 epochs, batch 64), no sweep: round 1 showed the hyperparameters don't matter at this size. Dev picked epoch 4, threshold 0.2.
| Round 1 (8,870 rows) | Round 2 (12,270 rows) | |
|---|---|---|
| Val 3-class (unseen Sep 18–22) | 72.0% | 71.7% |
| Val macro-F1 | 0.715 | 0.715 |
| Val relevance P / R / F1 | 0.803 / 0.884 / 0.841 | 0.759 / 0.926 / 0.834 |
| Val direction acc on relevant | 94.2% | 93.4% |
| Archive eval, Sep 18–22 all / gold / macro | 71.9 / 83.7 / 66.7% | 71.8 / 83.1 / 66.9% |
| Golden, non-final | 62/78 | 64/78 |
| Golden, final (shipped) | 65/78 | 67/78 |
| Spearman vs DeepSeek, final | +0.775 | +0.765 |
| Export flips / max diff, final | 0 / 0.075 | 4 (0.4%) / 0.104 |
| CPU latency, 1 thread | 18.9 ms | 18.9 ms |
- The live-feed metric has saturated at about 72% with this kind of data. The extra 3,400 history labels changed nothing there. More gains on the live feed will have to come from live-feed labels.
- The golden set still moved, 65 → 67 final. Hawkish Fed went 7 → 8, inverted metrics 4 → 6.
- Per category, final: distractors 8/8, dovish_fed 8/8, hawkish_fed 8/8, safe_haven 8/8, inverted_metrics 6/6, negation_traps 7/8, gold_direct 7/8, weak_usd 6/7, real_yields 5/6, strong_usd 3/7, risk_on 1/4.
- Strong USD (3/7) and risk-on (1/4) did not move, as expected: the crawl has no queries for growth data or equities. That is the next crawl, not the next labelling run.
Promotion criteria, round 2 non-final: golden 64/78, distractors 8/8, Sep 18–22 71.8%, latency 18.9 ms.
All pass. train.py --final ran on all 12,270 rows for 4 epochs; the shipped model.onnx is from that
torch_final. Round 1's weights are in the session scratchpad (sweep2/small_e4_bs64_8870rows,
sweep2/torch_final_8870rows).
Round 3: teaching the two weak spots (same day)
Strong-US-data (3/7) and risk-on (1/4) had not moved through rounds 1 and 2 because no crawl query covered growth data or equities. Fix:
- New crawler query set
growth(crawl_google_news.py --query-set growth --window-days 30): 20intitle:queries on reuters.com and cnbc.com for GDP, retail sales, ISM, PMI, consumer confidence, jobless claims, consumer spending, manufacturing, "record high", "risk appetite", "Wall Street", "S&P 500", ceasefire, "trade deal", equities. 1,880 requests, about 70 minutes, 6,183 new headlines (the precise crawl was paused meanwhile so Google saw one crawler, then resumed). - Labeller changes (
label_history.py): a--query-setoption restricts labelling to headlines a given crawler set found (the growth queries had also re-tagged thousands of old broad headlines), andis_latin()skips Reuters' Chinese and Japanese wire items thatintitle:GDPandintitle:PMImatch. - 200 calls, growth set only, 4,975 headlines labelled, 1 failed. DeepSeek called 19% relevant, 734
bullish vs 216 bearish: "Wall Street falls" and "ceasefire talks collapse" are risk-off, and
"record high"also catches gold's own records. The bearish side is the pattern the model lacked: "Yields rise on improving risk appetite" −0.50, "US dollar rises after jobless claims data" −0.50.
| Query | Labelled | Relevant | Bear / bull |
|---|---|---|---|
| "S&P 500" | 692 | 16% | 36 / 74 |
| GDP | 690 | 12% | 17 / 67 |
| "Wall Street" | 651 | 18% | 12 / 105 |
| "record high" | 624 | 14% | 23 / 67 |
| manufacturing | 457 | 6% | 6 / 20 |
| ceasefire | 379 | 58% | 11 / 208 |
| "trade deal" | 307 | 4% | 4 / 9 |
| PMI | 304 | 6% | 3 / 15 |
| "retail sales" | 293 | 18% | 23 / 29 |
| equities | 205 | 28% | 14 / 44 |
| "jobless claims" | 126 | 57% | 35 / 37 |
| "risk appetite" | 72 | 72% | 23 / 29 |
| consumer spending / confidence, ISM | 175 | 15–56% | 9 / 30 |
Training set: 17,245 unique headlines. Train 15,416 / dev 811 / val 1,018 (val unchanged). Default config, dev picked epoch 4, threshold 0.2.
| Round 2 (12,270) | Round 3 (17,245) | |
|---|---|---|
| Val 3-class (unseen Sep 18–22) | 71.7% | 71.2% |
| Archive eval, Sep 18–22 all / gold / macro | 71.8 / 83.1 / 66.9% | 71.5 / 78.6 / 68.4% |
| Val relevance P / R / F1 | 0.759 / 0.926 / 0.834 | 0.724 / 0.929 / 0.813 |
| Val direction acc on relevant | 93.4% | 89.4% |
| Golden, non-final | 64/78 | 71/78 |
| Golden, final (shipped) | 67/78 | 72/78 |
| Spearman vs DeepSeek, final | +0.765 | +0.860 |
| McNemar vs DeepSeek, final | DS-only-right 11, p = 0.001 | DS-only-right 6, p = 0.031 |
| Export flips / max diff, final | 4 / 0.104 | 2 / 0.077 |
| CPU latency, 1 thread | 18.9 ms | 18.8 ms |
Golden by category, final: strong_usd 3/7 → 7/7, risk_on 1/4 → 4/4, inverted_metrics 6/6, distractors 8/8, dovish_fed 8/8, safe_haven 8/8, negation_traps 7/8, hawkish_fed 7/8, weak_usd 6/7, real_yields 5/6, gold_direct 6/8.
The six remaining misses:
- "Powell says restrictive policy needed for longer as inflation proves sticky" → bullish 0.83 (hawkish read as bullish; the only hawkish miss).
- "Rising real interest rates dim appeal of non-yielding assets" → bullish 0.95 (no "gold" in the text; "non-yielding assets" is unseen vocabulary).
- "Bullion demand weakens as Indian jewellery buying stalls" → neutral (relevance 0.0; physical demand is absent from the training feeds).
- "Miners struggle to meet gold demand as supply deficit widens" → bearish 0.32 ("miners struggle" reads as bad news).
- "Greenback weakens as US retail sales unexpectedly contract" → neutral (relevance just under 0.2).
- "No safe-haven bid for gold despite equity wobble" → bullish 0.15, low confidence.
Trade-offs to note honestly:
- The live-feed numbers slipped a little (gold-key 83.1 → 78.6%, direction on relevant 93.4 → 89.4%) while macro rose (66.9 → 68.4%). The all-rows figure is flat at 71.5%. The history labels now outnumber the live-feed labels 9 to 1, and they are a different distribution (wire headlines, no descriptions). If the live feed is what matters, weight it or add live labels; the 4-day val set is too small to say whether this is real.
- Golden differences of 1–2 headlines are noise; 67 → 72 with the two target categories going 4/11 → 11/11 is not.
Promotion criteria, round 3 non-final: golden 71/78, distractors 8/8, Sep 18–22 71.5%, latency 18.8 ms.
All pass. Shipped model.onnx is from torch_final (17,245 rows, 4 epochs). Round 2 weights are in
the session scratchpad (sweep2/small_e4_bs64_12270rows, sweep2/torch_final_12270rows).
Next, revised
- Shadow mode in ForexBot2 with
threads=4, DeepSeek still the answer. Compare per headline for a few weeks. This is now the step that produces new information; more history labels will not. - Live-feed labels. The bot produces about 300 a day. Retrain weekly; consider up-weighting newsdata rows or a second fine-tuning pass on them if the gold-key live number keeps slipping.
- The precise crawl is still running in the background (free). Label its remainder only if a specific gap shows up in shadow mode.
- Loosen the export
--toleranceor make it flips-only; it has printed FAILED on every export today with 0–11 flips of 1,000.