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.
11 KiB
XAUUSD Sentiment Workflow and Usage Guide
Purpose
This model answers one question:
Does a news headline imply bullish, neutral, or bearish sentiment for XAUUSD?
- Bullish means the news supports a higher XAUUSD price.
- Bearish means the news supports a lower XAUUSD price.
- Neutral means the direction is unclear, weak, or conflicting.
The model runs locally with scikit-learn. It does not require an API call and can be placed before an existing LLM news-analysis step to reduce latency and LLM usage.
Files
| Path | Purpose |
|---|---|
news_sentiment.py |
Training, scoring, and Python API |
newsdata.db |
SQLite training data |
models/xauusd_sentiment.joblib |
Trained model artifact |
models/xauusd_sentiment.metrics.json |
Training and evaluation metrics |
requirements.txt |
Runtime dependencies |
Recommended Workflow
flowchart LR
A[New article] --> B[Validate headline and description]
B --> C[XAUUSD local model]
C --> D{Prediction confident?}
D -->|Yes| E[Use local prediction]
D -->|No| F[Call existing LLM]
E --> G[Store result and source]
F --> G
G --> H[Retain reviewed or LLM labels]
H --> I[Periodic model retraining]
Start in shadow mode: run the local model and LLM together, but continue using the LLM result. Log both outputs for comparison. After validating the model on live news, enable the confidence-based LLM fallback workflow.
Environment Setup
From this repository:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
The saved model was built with the exact versions in requirements.txt.
If the existing Python project is in another directory, make this repository importable when starting it:
export PYTHONPATH="/home/harwinder/Development/ML-NEWS:$PYTHONPATH"
export XAUUSD_MODEL_PATH="/home/harwinder/Development/ML-NEWS/models/xauusd_sentiment.joblib"
python /path/to/existing/project/main.py
Add the same environment variables to the service manager, Docker container, or deployment configuration used in production.
Command-Line Usage
Score one headline:
.venv/bin/python news_sentiment.py score \
--headline "Gold gains as the dollar weakens"
Score a headline and description:
.venv/bin/python news_sentiment.py score \
--headline "Gold slides as Treasury yields rise" \
--description "A stronger US dollar adds pressure to bullion."
The output has this structure:
{
"target": "XAUUSD",
"label": "bearish",
"score": -0.82,
"confidence": 0.87,
"probabilities": {
"bearish": 0.87,
"neutral": 0.08,
"bullish": 0.05
}
}
The score is calculated as:
P(XAUUSD bullish) - P(XAUUSD bearish)
Use label as the primary classification. A neutral prediction can still have
a positive or negative score when the model sees a secondary directional
lean.
Batch JSONL Usage
Input records must be newline-delimited JSON. Each record can contain
headline, title, or text, plus an optional description:
{"id": 1, "headline": "Gold rises as the dollar falls"}
{"id": 2, "headline": "Gold holds steady ahead of CPI"}
{"id": 3, "title": "Gold falls as Treasury yields climb"}
Score the file:
.venv/bin/python news_sentiment.py score --input articles.jsonl > scored.jsonl
Each output record preserves the input fields and adds a sentiment object.
Python Integration
Load the model once when the application process starts. Do not load it for every article.
import os
from pathlib import Path
from news_sentiment import load_model, score_news
MODEL_PATH = Path(
os.environ.get(
"XAUUSD_MODEL_PATH",
"/home/harwinder/Development/ML-NEWS/models/xauusd_sentiment.joblib",
)
)
XAUUSD_MODEL = load_model(MODEL_PATH)
def analyze_xauusd_news(news: dict) -> dict:
prediction = score_news(
XAUUSD_MODEL,
headline=news["headline"],
description=news.get("description"),
)
return {
"target": prediction["target"],
"sentiment": prediction["label"],
"score": prediction["score"],
"confidence": prediction["confidence"],
"probabilities": prediction["probabilities"],
"score_source": "xauusd_sklearn_v3",
}
Example:
news = {
"headline": "Gold rallies as the US dollar weakens",
"description": "Falling Treasury yields provided additional support.",
}
result = analyze_xauusd_news(news)
print(result)
Connecting to the Existing LLM Workflow
Assume the existing project currently calls:
result = analyze_news_with_llm(headline, description)
For a full local replacement:
result = analyze_xauusd_news(
{
"headline": headline,
"description": description,
}
)
For the recommended hybrid workflow, call the LLM only when the local result is uncertain:
def analyze_news(news: dict) -> dict:
try:
ml_result = analyze_xauusd_news(news)
except Exception:
llm_result = analyze_news_with_llm(
news["headline"], news.get("description")
)
llm_result["score_source"] = "llm_model_error_fallback"
return llm_result
ordered_probabilities = sorted(
ml_result["probabilities"].values(), reverse=True
)
probability_margin = ordered_probabilities[0] - ordered_probabilities[1]
uncertain = (
ml_result["confidence"] < 0.70
or probability_margin < 0.20
)
if not uncertain:
return ml_result
llm_result = analyze_news_with_llm(
news["headline"], news.get("description")
)
llm_result["score_source"] = "llm_low_confidence_fallback"
return llm_result
The 0.70 confidence and 0.20 margin are initial operating thresholds, not
universal constants. Tune them using results from the project's live news
stream. Higher thresholds make more LLM calls but reduce reliance on uncertain
local predictions.
Input Guidance
Use English RSS-style input matching the training data:
- Provide the original headline.
- Provide a short description or summary when available.
- Avoid adding an LLM-generated interpretation to the input.
- Avoid passing an entire long article as the description.
- Preserve terms such as
XAUUSD,gold,USD,Fed, andTreasury yields. - Reject records where both headline and description are empty.
Persisting Predictions
Always record how a score was produced. Do not silently mix model and LLM scores without a source field.
Recommended fields are:
article_hash
target
label
score
confidence
probabilities_json
score_source
model_version
predicted_at
If the existing headlines table must be used, a model result can be mapped as
follows:
import json
import sqlite3
prediction = analyze_xauusd_news(news)
with sqlite3.connect("newsdata.db") as database:
database.execute(
"""
UPDATE headlines
SET score = ?,
score_by_currency_json = ?,
score_source = ?
WHERE hash = ? AND score IS NULL
""",
(
prediction["score"],
json.dumps({"XAU": prediction["score"]}),
prediction["score_source"],
news["hash"],
),
)
The score IS NULL condition prevents overwriting existing LLM training
labels. A separate predictions table is preferable when confidence,
probabilities, prediction history, or multiple model versions must be retained.
Retraining Workflow
The trainer opens the SQLite database read-only and uses rows with:
score_source = "llm"
label = xau_score, else score_by_currency_json["XAU"], else --extra-labels
source is not a test feed (feed.example, good.example)
Retrain after adding a meaningful amount of new labeled data:
.venv/bin/python news_sentiment.py train --db newsdata.db \
--extra-labels eval/results/backfill_labels.jsonl
Use --scope gold to train only on headlines that mention gold (the pre-2026-09-22
data), and --model models/candidates/<name>.joblib to train a candidate without
replacing the deployed model. Grade candidates with
eval/eval_golden.py --model ... and
eval/eval_archive.py --db ../newsdata.db --extra-labels results/backfill_labels.jsonl.
Training performs the following steps:
- Reads XAU sentiment labels (
xau_score, JSONXAU, extra labels). - Normalizes and deduplicates article text.
- Maps negative, zero, and positive XAU scores to classes.
- Evaluates a stratified random holdout.
- Evaluates a chronological holdout on the newest articles.
- Fits the final model on all eligible examples.
- Writes the model and metrics report.
Before deploying a retrained model, inspect:
python -m json.tool models/xauusd_sentiment.metrics.json
Prioritize the chronological holdout result because it better represents future news. Do not deploy solely because random holdout accuracy improved.
Production Rollout
- Run the model in shadow mode alongside the LLM.
- Log disagreements, confidence, latency, and final accepted output.
- Review obvious directional cases and low-confidence predictions.
- Enable local predictions only above conservative thresholds.
- Keep the LLM as a fallback for uncertain or failed predictions.
- Add reviewed LLM results to the training set.
- Retrain and compare chronological metrics periodically.
Load one model per application process. A process restart is the simplest way
to load a newly deployed model artifact. Only load trusted .joblib files,
because joblib model files can execute code during deserialization.
Monitoring
Track at least:
- Number of articles scored
- Bullish, neutral, and bearish distribution
- Average confidence and probability margin
- Percentage routed to the LLM
- ML versus LLM agreement rate
- Errors and model-loading failures
- Results by publisher and news source
- Current model version
Sudden changes in class distribution or confidence can indicate data drift, changed feed formatting, or a new market regime.
Current Limitations
- Training contains 2,620 deduplicated XAU-labeled examples, all DeepSeek labels.
- The data covers only 15 days (2026-09-08 to 09-22) of one regime.
- The model estimates news sentiment, not future returns or trade profitability.
- Confidence values are useful for routing but are not guaranteed to be fully calibrated probabilities.
- Unusual wording and economic conditions absent from training can reduce accuracy.
The current chronological holdout accuracy is 66.6% (macro-F1 0.659). Continue using the LLM fallback until live shadow-mode results justify broader local-model usage.
Troubleshooting
Model not found
Use an absolute model path or set XAUUSD_MODEL_PATH:
export XAUUSD_MODEL_PATH="/home/harwinder/Development/ML-NEWS/models/xauusd_sentiment.joblib"
Cannot import news_sentiment
Add the repository to PYTHONPATH before starting the existing project:
export PYTHONPATH="/home/harwinder/Development/ML-NEWS:$PYTHONPATH"
scikit-learn version warning
Install the pinned dependencies from this repository:
python -m pip install -r /home/harwinder/Development/ML-NEWS/requirements.txt
Empty article error
At least one of headline or description must contain text.
Predictions look unrelated to XAUUSD
Confirm that the loaded artifact is models/xauusd_sentiment.joblib and that
the result contains "target": "XAUUSD".