finrix 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. finrix-1.0.0/LICENSE +21 -0
  2. finrix-1.0.0/PKG-INFO +21 -0
  3. finrix-1.0.0/README.md +192 -0
  4. finrix-1.0.0/finrix.egg-info/PKG-INFO +21 -0
  5. finrix-1.0.0/finrix.egg-info/SOURCES.txt +28 -0
  6. finrix-1.0.0/finrix.egg-info/dependency_links.txt +1 -0
  7. finrix-1.0.0/finrix.egg-info/entry_points.txt +2 -0
  8. finrix-1.0.0/finrix.egg-info/requires.txt +14 -0
  9. finrix-1.0.0/finrix.egg-info/top_level.txt +1 -0
  10. finrix-1.0.0/macro_forecast_engine/__init__.py +2 -0
  11. finrix-1.0.0/macro_forecast_engine/config.py +28 -0
  12. finrix-1.0.0/macro_forecast_engine/data_fetcher.py +9 -0
  13. finrix-1.0.0/macro_forecast_engine/data_loader.py +300 -0
  14. finrix-1.0.0/macro_forecast_engine/data_processor.py +40 -0
  15. finrix-1.0.0/macro_forecast_engine/finrix_config.py +46 -0
  16. finrix-1.0.0/macro_forecast_engine/indicator_params.py +77 -0
  17. finrix-1.0.0/macro_forecast_engine/json_utils.py +27 -0
  18. finrix-1.0.0/macro_forecast_engine/modal_engine.py +606 -0
  19. finrix-1.0.0/macro_forecast_engine/run_engine.py +68 -0
  20. finrix-1.0.0/macro_forecast_engine/trader_execution.py +84 -0
  21. finrix-1.0.0/macro_forecast_engine/trader_risk.py +103 -0
  22. finrix-1.0.0/macro_forecast_engine/trader_timemachine.py +413 -0
  23. finrix-1.0.0/pyproject.toml +38 -0
  24. finrix-1.0.0/setup.cfg +4 -0
  25. finrix-1.0.0/tests/test_backtest.py +235 -0
  26. finrix-1.0.0/tests/test_diffusion.py +50 -0
  27. finrix-1.0.0/tests/test_indicators.py +75 -0
  28. finrix-1.0.0/tests/test_integration.py +128 -0
  29. finrix-1.0.0/tests/test_risk.py +76 -0
  30. finrix-1.0.0/tests/test_security.py +110 -0
finrix-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 carnoba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
finrix-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: finrix
3
+ Version: 1.0.0
4
+ Summary: FINRIX Quantitative Trading Terminal — XAUUSD macro forecast engine with Rich CLI
5
+ Requires-Python: <3.13,>=3.11
6
+ License-File: LICENSE
7
+ Requires-Dist: modal==0.73.0
8
+ Requires-Dist: neuralforecast==1.7.5
9
+ Requires-Dist: pyfredapi==0.10.2
10
+ Requires-Dist: transformers==4.46.3
11
+ Requires-Dist: torch==2.5.1
12
+ Requires-Dist: pandas==2.2.3
13
+ Requires-Dist: numpy==2.1.3
14
+ Requires-Dist: scikit-learn==1.5.2
15
+ Requires-Dist: yfinance==0.2.48
16
+ Requires-Dist: requests==2.32.3
17
+ Requires-Dist: python-dotenv==1.0.1
18
+ Requires-Dist: rich==13.9.4
19
+ Requires-Dist: questionary==2.0.1
20
+ Requires-Dist: InquirerPy==0.3.4
21
+ Dynamic: license-file
finrix-1.0.0/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # FINRIX XAUUSD Quant Trading Terminal
2
+
3
+ > **Status: Research Prototype / Quantitative Framework.** FINRIX supports
4
+ > reproducible market research, forecasting, and backtesting. It is not
5
+ > investment advice and does not execute live orders without a separately
6
+ > installed and explicitly authorized broker bridge.
7
+
8
+ [![CI Status](https://img.shields.io/github/actions/workflow/status/carnoba/trading-prediction/.github/workflows/ci.yml?label=CI&logo=github&color=blue)](https://github.com/carnoba/trading-prediction/actions/workflows/ci.yml)
9
+ [![Pytest Pass](https://img.shields.io/badge/pytest-207%20passed-brightgreen.svg)](https://github.com/carnoba/trading-prediction/actions/workflows/ci.yml)
10
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-3100-rc1/)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+ [![Execution: Local CPU | Modal A10G GPU](https://img.shields.io/badge/Execution-Local%20CPU%20%7C%20Modal%20A10G%20GPU-orange.svg)](macro_forecast_engine/)
13
+ [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
14
+ [![Zero-Trust Audit PASSED](https://img.shields.io/badge/Audit-PASSED-brightgreen.svg)](FINRIX_SECURITY_AND_QA_AUDIT_REPORT.md)
15
+ [![14-Strategy Engine](https://img.shields.io/badge/Strategies-14-brightgreen.svg)](macro_forecast_engine/)
16
+
17
+
18
+
19
+
20
+
21
+ https://github.com/user-attachments/assets/b600ad35-9752-46c7-a912-12deb9714708
22
+
23
+
24
+
25
+
26
+
27
+
28
+ ## One-Line Execution
29
+
30
+ Run the full FINRIX terminal directly from command line:
31
+
32
+ ```bash
33
+ finrix
34
+ ```
35
+
36
+ Or launch the Rich/questionary terminal:
37
+
38
+ ```bash
39
+ python app.py
40
+ ```
41
+
42
+ The System Settings menu controls Modal A10G vs local CPU, default equity, and risk per trade -- all forecast actions use saved values automatically.
43
+
44
+ ## Core Architecture & Key Features
45
+
46
+ ### Dual-Path Execution Engine
47
+
48
+ - **Local CPU** -- Lightweight drift & baseline execution, runs instantly on any machine
49
+ - **Modal A10G Cloud GPU** -- NeuralForecast TFT deep learning, 100,000-path CUDA Monte Carlo, CUDA-accelerated quantile regression
50
+
51
+ ### Quantile Forecasting Envelope
52
+
53
+ Monotonic price corridors $q_{10} \le q_{50} \le q_{90}$ with dynamic volatility decay across daily (30 business days), monthly (22 business days), and yearly (252 business days) horizons.
54
+
55
+ ### Dynamic Equity-Based Position Sizing
56
+
57
+ Real-time dollar risk math with standardized Gold pip sizing, pip-based
58
+ stop-loss/take-profit calculation, equity compounding, transaction-cost-aware
59
+ trade sizing, and fractional-Kelly allocation across 14 strategies.
60
+
61
+ ### Genuine Trade-Based Backtesting
62
+
63
+ The timemachine engine simulates signal entry, open-position tracking,
64
+ stop-loss/take-profit exits, signal reversal, 1.5-pip spread, and slippage.
65
+ Metrics use closed trades and the dollar equity curve rather than static
66
+ per-bar percentage returns.
67
+
68
+ ### 14-Strategy Backtesting & Parameter Engine
69
+
70
+ All strategies persist to `data/finrix_indicator_params.json`:
71
+
72
+ | Strategy | Indicators |
73
+ |----------|------------|
74
+ | SMC Fair Value Gap (FVG) | FVG detection, order block breakout |
75
+ | SMC Liquidity Order Block Breakout | SSL/BSL liquidity pool detection |
76
+ | EMA Golden Cross / Death Cross | Fast/slow EMA crossover, trend period |
77
+ | Triple EMA Ribbon | Fast/medium/slow EMA ribbon dynamics |
78
+ | RSI Mean Reversion | RSI period, overbought, oversold |
79
+ | MACD Zero-Line & Signal Line Crossover | Fast/slow/signal MACD components |
80
+ | Bollinger Bands Squeeze & Reversal | Period, standard deviation |
81
+ | Supertrend ATR Trend Follower | ATR period, multiplier |
82
+ | Stochastic Oscillator | K period, D period, slowing |
83
+ | ADX + DI Trend Classifier | ADX period, DI threshold |
84
+ | Ichimoku Cloud Kumo Breakout | Tenkan, Kijun, Senkou B periods |
85
+ | ATR Volatility Expansion Channel | ATR period |
86
+ | TFT Log-Return Trend | Temporal Fusion Transformer ML |
87
+ | Volatility Parity & Risk-Adjusted Allocation | Volatility-normalized allocation |
88
+
89
+ ### Macro Sentiment Analysis
90
+
91
+ Live RSS headlines processed via **FinBERT** NLP model with dovish/hawkish net score cancellation math. Fallback regex-based scoring when transformer models are unavailable.
92
+
93
+ ## Terminal Menu Workflow Guide (Options 1-8)
94
+
95
+ | Option | Description |
96
+ |--------|-------------|
97
+ | **1** | Instant Execution Card -- Kelly Position Sizing, dynamic Entry bounds, Dynamic SL (exact pip distance), Risk:Reward ratios |
98
+ | **2** | Daily Price Path & Signals -- 30-day forward forecast with quantile monotonicity ($q_{10} \le q_{50} \le q_{90}$) |
99
+ | **3** | Monthly Macro Horizon -- 22-day outlook & weekly pivots with dynamic bias calculation |
100
+ | **4** | Yearly Regime Trend Shift -- 252-day forecast with volatility envelopes, $q_{90}$ capped at 6500 |
101
+ | **5** | Macro News & FinBERT Sentiment -- Live RSS headline aggregation, Dovish/Hawkish score cancellation |
102
+ | **6** | Risk & Position Sizing Calculator -- Custom market price overrides, pip distance math, zero static hardcoded outputs |
103
+ | **7** | Historical Strategy Backtester -- All 14 strategies across 15m, 1h, 4h, 1D timeframes, dynamic lot sizing |
104
+ | **8** | Multi-Strategy Ensemble Analyzer -- Top 5 leaderboard, dynamic header title (Local CPU vs Modal A10G), all strategies |
105
+
106
+ ### System Settings
107
+
108
+ - Toggle execution environment (local/modal)
109
+ - Set default account equity
110
+ - Set default risk per trade %
111
+ - Customize indicator parameters (all 10 groups with limits)
112
+ - Save & apply settings persistence via `data/finrix_config.json`
113
+
114
+ ## Quality Assurance
115
+
116
+ The repository includes a real pytest suite covering dynamic risk sizing,
117
+ lookahead-safe trade lifecycle execution, all 14 indicator strategies,
118
+ timeframe-specific caching, non-finite JSON coercion, and configuration
119
+ sanitization.
120
+
121
+ ```text
122
+ 207 passed
123
+ ```
124
+
125
+ Run the checks with:
126
+
127
+ ```bash
128
+ python -m pytest -q
129
+ python -m compileall -q app.py macro_forecast_engine
130
+ ```
131
+
132
+ Finrix Screenshots:
133
+
134
+ <img width="283" height="304" alt="Screenshot 2026-11-08 105821" src="https://github.com/user-attachments/assets/919fb0d2-2d04-4172-a401-9f4caab112c7" />
135
+ <img width="282" height="281" alt="Screenshot 2026-11-08 105740" src="https://github.com/user-attachments/assets/d5558b17-58df-434c-9fda-0e8376d08090" />
136
+ <img width="275" height="259" alt="Screenshot 2026-11-08 105707" src="https://github.com/user-attachments/assets/c11f30cd-bc4c-45fa-9ffa-6ab9fde55a88" />
137
+ <img width="276" height="128" alt="Screenshot 2026-11-07 225107" src="https://github.com/user-attachments/assets/b0487285-9d31-4dd7-8191-e7b53a166eeb" />
138
+ <img width="285" height="529" alt="Screenshot 2026-11-07 225031" src="https://github.com/user-attachments/assets/894637f0-90d2-4c23-9465-83352218779e" />
139
+
140
+
141
+
142
+
143
+ ## Future Roadmap
144
+
145
+ - **Multi-asset expansion** -- Forex pairs, Crypto indices, Equity indices
146
+ - **Direct MetaTrader 5 (MT5) execution bridge** & Webhook integration
147
+ - **Automated portfolio rebalancing** with dynamic risk limits
148
+ - **Walking-forward analysis** & rolling-origin backtesting
149
+ - **Cluster deployment** across multiple Modal GPU workers
150
+
151
+ ## Repository Layout
152
+
153
+ ```text
154
+ macro_forecast_engine/
155
+ ├── modal_engine.py # Primary Modal A10G pipeline
156
+ ├── run_engine.py # Forecast orchestration utilities
157
+ ├── data_loader.py # Market, FRED, RSS, and sentiment ingestion
158
+ ├── execution_layer.py # Macro-bias signal filtering utilities
159
+ ├── config.py # Environment-backed configuration
160
+ ├── indicator_params.py # Persistent indicator parameter JSON
161
+ ├── finrix_config.py # Terminal settings persistence
162
+ ├── trader_execution.py # SMC + TFT execution levels
163
+ ├── trader_risk.py # Kelly sizing & volatility parity
164
+ ├── trader_timemachine.py # Strategy replay & confidence scoring
165
+ ├── config.py
166
+ ├── __init__.py
167
+ .github/
168
+ ├── workflows/
169
+ │ └── ci.yml # GitHub Actions CI pipeline
170
+ └── ISSUE_TEMPLATE/
171
+ ├── bug_report.md # Bug report template
172
+ └── feature_request.md # Feature request template
173
+ .github/PULL_REQUEST_TEMPLATE.md # PR checklist template
174
+ CONTRIBUTING.md # Open-source guidelines
175
+ FINRIX_Institutional_Project_Report.docx # Project Report
176
+ requirements.txt
177
+ .env.example
178
+ README.md (this file)
179
+ FINRIX_SECURITY_AND_QA_AUDIT_REPORT.md
180
+ data/
181
+ ├── finrix_config.json
182
+ ├── finrix_indicator_params.json
183
+ ├── macro_daily_forecast.json
184
+ ├── macro_month_forecast.json
185
+ └── macro_year_forecast.json
186
+ cache/
187
+ gold_market_cache.csv
188
+ ```
189
+
190
+ ---
191
+
192
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: finrix
3
+ Version: 1.0.0
4
+ Summary: FINRIX Quantitative Trading Terminal — XAUUSD macro forecast engine with Rich CLI
5
+ Requires-Python: <3.13,>=3.11
6
+ License-File: LICENSE
7
+ Requires-Dist: modal==0.73.0
8
+ Requires-Dist: neuralforecast==1.7.5
9
+ Requires-Dist: pyfredapi==0.10.2
10
+ Requires-Dist: transformers==4.46.3
11
+ Requires-Dist: torch==2.5.1
12
+ Requires-Dist: pandas==2.2.3
13
+ Requires-Dist: numpy==2.1.3
14
+ Requires-Dist: scikit-learn==1.5.2
15
+ Requires-Dist: yfinance==0.2.48
16
+ Requires-Dist: requests==2.32.3
17
+ Requires-Dist: python-dotenv==1.0.1
18
+ Requires-Dist: rich==13.9.4
19
+ Requires-Dist: questionary==2.0.1
20
+ Requires-Dist: InquirerPy==0.3.4
21
+ Dynamic: license-file
@@ -0,0 +1,28 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ finrix.egg-info/PKG-INFO
5
+ finrix.egg-info/SOURCES.txt
6
+ finrix.egg-info/dependency_links.txt
7
+ finrix.egg-info/entry_points.txt
8
+ finrix.egg-info/requires.txt
9
+ finrix.egg-info/top_level.txt
10
+ macro_forecast_engine/__init__.py
11
+ macro_forecast_engine/config.py
12
+ macro_forecast_engine/data_fetcher.py
13
+ macro_forecast_engine/data_loader.py
14
+ macro_forecast_engine/data_processor.py
15
+ macro_forecast_engine/finrix_config.py
16
+ macro_forecast_engine/indicator_params.py
17
+ macro_forecast_engine/json_utils.py
18
+ macro_forecast_engine/modal_engine.py
19
+ macro_forecast_engine/run_engine.py
20
+ macro_forecast_engine/trader_execution.py
21
+ macro_forecast_engine/trader_risk.py
22
+ macro_forecast_engine/trader_timemachine.py
23
+ tests/test_backtest.py
24
+ tests/test_diffusion.py
25
+ tests/test_indicators.py
26
+ tests/test_integration.py
27
+ tests/test_risk.py
28
+ tests/test_security.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ finrix = app:main
@@ -0,0 +1,14 @@
1
+ modal==0.73.0
2
+ neuralforecast==1.7.5
3
+ pyfredapi==0.10.2
4
+ transformers==4.46.3
5
+ torch==2.5.1
6
+ pandas==2.2.3
7
+ numpy==2.1.3
8
+ scikit-learn==1.5.2
9
+ yfinance==0.2.48
10
+ requests==2.32.3
11
+ python-dotenv==1.0.1
12
+ rich==13.9.4
13
+ questionary==2.0.1
14
+ InquirerPy==0.3.4
@@ -0,0 +1 @@
1
+ macro_forecast_engine
@@ -0,0 +1,2 @@
1
+ """Macro-quant XAUUSD forecasting package."""
2
+
@@ -0,0 +1,28 @@
1
+ """Environment-backed configuration for the Modal forecast engine."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ try:
9
+ from dotenv import load_dotenv
10
+ except ImportError: # pragma: no cover
11
+ load_dotenv = None
12
+
13
+ BASE_DIR = Path(__file__).resolve().parent
14
+ PROJECT_ROOT = BASE_DIR.parent
15
+ DATA_DIR = PROJECT_ROOT / "data"
16
+
17
+ if load_dotenv is not None:
18
+ load_dotenv(PROJECT_ROOT / ".env")
19
+
20
+ FRED_API_KEY = os.getenv("FRED_API_KEY", "").strip()
21
+ FORECAST_MODE = os.getenv("FORECAST_MODE", os.getenv("FORECAST_HORIZON", "monthly")).lower()
22
+ FINBERT_MODEL = os.getenv("FINBERT_MODEL", "ProsusAI/finbert")
23
+ FRED_SERIES_TIPS = "DFII10"
24
+ FRED_SERIES_2Y = "DGS2"
25
+
26
+ HORIZONS = {"daily": 30, "monthly": 22, "yearly": 252}
27
+ if FORECAST_MODE not in HORIZONS:
28
+ raise ValueError(f"FORECAST_MODE must be one of {', '.join(HORIZONS)}")
@@ -0,0 +1,9 @@
1
+ """Compatibility exports for timeframe-aware market data ingestion."""
2
+
3
+ from .data_loader import (
4
+ GOLD_TICKERS,
5
+ INTRADAY_PERIOD_LIMITS,
6
+ fetch_market_data,
7
+ )
8
+
9
+ __all__ = ["GOLD_TICKERS", "INTRADAY_PERIOD_LIMITS", "fetch_market_data"]
@@ -0,0 +1,300 @@
1
+ """Institutional cross-asset and macro data ingestion helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import warnings
8
+ from pathlib import Path
9
+ from io import StringIO
10
+ import xml.etree.ElementTree as ET
11
+ from typing import Any, Iterable, Mapping, Optional
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+ import requests
16
+ from sklearn.preprocessing import RobustScaler
17
+
18
+ try:
19
+ from .data_processor import add_sentiment_and_flow
20
+ except ImportError: # Modal may load this file as a top-level script.
21
+ from macro_forecast_engine.data_processor import add_sentiment_and_flow
22
+
23
+ try:
24
+ import yfinance as yf
25
+ except ImportError: # pragma: no cover
26
+ yf = None
27
+
28
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
29
+ GOLD_TICKERS = ("GC=F", "XAUUSD=X")
30
+ INTRADAY_PERIOD_LIMITS = {
31
+ "15m": ("60d", "15m"),
32
+ "1h": ("60d", "1h"),
33
+ "4h": ("60d", "1h"),
34
+ "1d": ("730d", "1d"),
35
+ }
36
+
37
+
38
+ def _download(ticker: str, period: str = "10y", interval: str = "1d") -> pd.DataFrame:
39
+ if yf is None:
40
+ raise RuntimeError("yfinance is required for market data ingestion")
41
+ frame = yf.download(ticker, period=period, interval=interval, auto_adjust=False,
42
+ progress=False, threads=False)
43
+ if isinstance(frame.columns, pd.MultiIndex):
44
+ frame.columns = frame.columns.get_level_values(0)
45
+ if frame.empty:
46
+ raise ValueError(f"No data returned for {ticker}")
47
+ frame.index = pd.to_datetime(frame.index, utc=True).tz_localize(None)
48
+ frame.columns = [str(column).lower() for column in frame.columns]
49
+ return frame
50
+
51
+
52
+ def _normalized_request(period: str, interval: str) -> tuple[str, str]:
53
+ configured = INTRADAY_PERIOD_LIMITS.get(interval.lower())
54
+ return configured if configured else (period, interval)
55
+
56
+
57
+ def _cache_path(interval: str) -> Path:
58
+ normalized = interval.lower()
59
+ suffix = "1d" if normalized in {"1d", "d"} else normalized
60
+ safe_suffix = re.sub(r"[^a-z0-9_-]", "", suffix) or "1d"
61
+ return PROJECT_ROOT / "cache" / f"gold_cache_{safe_suffix}.csv"
62
+
63
+
64
+ def _load_cached_gold(interval: str) -> pd.DataFrame:
65
+ cache_path = _cache_path(interval)
66
+ if not cache_path.exists():
67
+ return pd.DataFrame()
68
+ try:
69
+ cached = pd.read_csv(cache_path, index_col=0, parse_dates=True)
70
+ cached.index = pd.to_datetime(cached.index, utc=True).tz_localize(None)
71
+ if "gold_close" not in cached.columns:
72
+ return pd.DataFrame()
73
+ return cached.sort_index().dropna(subset=["gold_close"])
74
+ except (OSError, ValueError, TypeError):
75
+ return pd.DataFrame()
76
+
77
+
78
+ def _fetch_gold(period: str, interval: str, cache_key: str | None = None) -> pd.DataFrame:
79
+ request_period, request_interval = _normalized_request(period, interval)
80
+ for ticker in GOLD_TICKERS:
81
+ try:
82
+ data = _download(ticker, request_period, request_interval)
83
+ if "close" not in data.columns or data["close"].dropna().empty:
84
+ continue
85
+ result = data.rename(columns={
86
+ "open": "gold_open", "high": "gold_high", "low": "gold_low",
87
+ "close": "gold_close", "volume": "gold_volume",
88
+ })
89
+ cache_path = _cache_path(cache_key or request_interval)
90
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
91
+ result.to_csv(cache_path)
92
+ return result
93
+ except (RuntimeError, ValueError, requests.RequestException, OSError):
94
+ continue
95
+ cached = _load_cached_gold(cache_key or request_interval)
96
+ if not cached.empty:
97
+ return cached
98
+ raise ValueError("Gold market data is unavailable from GC=F, XAUUSD=X, and local cache")
99
+
100
+
101
+ def fetch_market_data(period: str = "10y", interval: str = "1d") -> pd.DataFrame:
102
+ """Fetch gold, DXY, GVZ, ETF holdings/volume, and align daily factors."""
103
+ gold = _fetch_gold(period, interval, cache_key=interval)
104
+ frames: list[pd.DataFrame] = [gold]
105
+ request_period, request_interval = _normalized_request(period, interval)
106
+ for ticker, prefix in (("DX-Y.NYB", "dxy"),
107
+ ("^GVZ", "gvz"), ("GLD", "gld"), ("IAU", "iau")):
108
+ try:
109
+ data = _download(ticker, request_period, request_interval)
110
+ except (RuntimeError, ValueError, requests.RequestException, OSError):
111
+ continue
112
+ columns = {
113
+ "open": f"{prefix}_open", "high": f"{prefix}_high",
114
+ "low": f"{prefix}_low", "close": f"{prefix}_close",
115
+ "volume": f"{prefix}_volume",
116
+ }
117
+ available = {source: target for source, target in columns.items() if source in data.columns}
118
+ frames.append(data.rename(columns=available)[list(available.values())])
119
+ frame = frames[0]
120
+ for other in frames[1:]:
121
+ frame = frame.join(other, how="left")
122
+ return frame.sort_index().ffill().dropna(subset=["gold_close"])
123
+
124
+
125
+ def _fred_series(series_id: str, start_date: Optional[str], end_date: Optional[str],
126
+ api_key: Optional[str] = None) -> pd.Series:
127
+ params = {"id": series_id, "cosd": start_date or "", "coed": end_date or ""}
128
+ url = "https://fred.stlouisfed.org/graph/fredgraph.csv"
129
+ api_key = (api_key or os.getenv("FRED_API_KEY", "")).strip()
130
+ if api_key:
131
+ url = "https://api.stlouisfed.org/fred/series/observations"
132
+ params = {"series_id": series_id, "api_key": api_key, "file_type": "json",
133
+ "observation_start": start_date or "", "observation_end": end_date or ""}
134
+ response = requests.get(url, params=params, timeout=30)
135
+ response.raise_for_status()
136
+ data = pd.DataFrame(response.json().get("observations", []))
137
+ series = data.set_index("date")["value"]
138
+ else:
139
+ response = requests.get(url, params=params, timeout=30)
140
+ response.raise_for_status()
141
+ data = pd.read_csv(StringIO(response.text))
142
+ date_column = "DATE" if "DATE" in data.columns else "observation_date"
143
+ series = data.set_index(date_column)[series_id].replace(".", np.nan)
144
+ series.index = pd.to_datetime(series.index, utc=True).tz_localize(None)
145
+ return pd.to_numeric(series, errors="coerce").rename(series_id).dropna()
146
+
147
+
148
+ def fetch_macro_data(start_date: str, end_date: str,
149
+ fred_api_key: Optional[str] = None) -> pd.DataFrame:
150
+ """Fetch TIPS real yields and the 10Y-2Y Treasury curve spread."""
151
+ result = pd.DataFrame(index=pd.date_range(start_date, end_date, freq="D"))
152
+ for series_id, name in (("DFII10", "tips10y"), ("DGS10", "ust10y"), ("DGS2", "ust2y")):
153
+ try:
154
+ result = result.join(
155
+ _fred_series(series_id, start_date, end_date, api_key=fred_api_key),
156
+ how="left",
157
+ )
158
+ except (requests.RequestException, ValueError, KeyError):
159
+ result[name] = np.nan
160
+ if "DGS10" in result and "DGS2" in result:
161
+ result["curve_spread_10y2y"] = result["DGS10"] - result["DGS2"]
162
+ else:
163
+ result["curve_spread_10y2y"] = np.nan
164
+ return result.rename(columns={"DFII10": "tips10y"}).drop(columns=["DGS10", "DGS2"], errors="ignore")
165
+
166
+
167
+ def fetch_cot_data() -> pd.DataFrame:
168
+ """Fetch current CFTC disaggregated gold positioning when available."""
169
+ url = "https://publicreporting.cftc.gov/resource/jun7-fc8e.json"
170
+ try:
171
+ response = requests.get(url, params={"$limit": 2000, "$q": "GOLD"}, timeout=30)
172
+ response.raise_for_status()
173
+ rows = response.json()
174
+ data = pd.DataFrame(rows)
175
+ date_col = next((c for c in ("report_date_as_yyyy_mm_dd", "report_date") if c in data), None)
176
+ if date_col is None:
177
+ return pd.DataFrame()
178
+ data.index = pd.to_datetime(data[date_col], errors="coerce")
179
+ result = pd.DataFrame(index=data.index)
180
+ for prefix, target in (("comm_positions", "commercial_positions"),
181
+ ("noncomm_positions", "noncommercial_positions")):
182
+ long = next((c for c in data.columns if c.startswith(f"{prefix}_long")), None)
183
+ short = next((c for c in data.columns if c.startswith(f"{prefix}_short")), None)
184
+ if long and short:
185
+ result[target] = (
186
+ pd.to_numeric(data[long], errors="coerce")
187
+ - pd.to_numeric(data[short], errors="coerce")
188
+ )
189
+ return result.groupby(level=0).last().sort_index()
190
+ except (requests.RequestException, ValueError, KeyError):
191
+ return pd.DataFrame()
192
+
193
+
194
+ GOLD_NEWS_FEEDS: tuple[tuple[str, str], ...] = (
195
+ ("forexlive_gold", "https://www.forexlive.com/feed/news"),
196
+ ("investing_commodities", "https://www.investing.com/rss/commodities.rss"),
197
+ ("marketwatch_macro", "https://feeds.marketwatch.com/marketwatch/marketpulse/"),
198
+ )
199
+ GOLD_MACRO_KEYWORDS = re.compile(
200
+ r"\b("
201
+ r"gold|xauusd|fomc rate|cpi|inflation|non[- ]farm payrolls?|"
202
+ r"dxy|yields?"
203
+ r")\b",
204
+ re.IGNORECASE,
205
+ )
206
+
207
+
208
+ def filter_gold_macro_headlines(headlines: Iterable[Any]) -> list[str]:
209
+ """Keep only headlines tied to gold macro drivers."""
210
+ return [text for item in headlines if (text := str(item).strip())
211
+ and GOLD_MACRO_KEYWORDS.search(text)]
212
+
213
+
214
+ def fetch_live_news() -> pd.DataFrame:
215
+ rows: list[dict[str, Any]] = []
216
+ for source, feed_url in GOLD_NEWS_FEEDS:
217
+ try:
218
+ response = requests.get(feed_url, headers={"User-Agent": "macro-forecast-engine/1.0"}, timeout=30)
219
+ response.raise_for_status()
220
+ root = ET.fromstring(response.content)
221
+ except (requests.RequestException, ET.ParseError):
222
+ continue
223
+ for item in root.findall(".//item"):
224
+ title = (item.findtext("title") or "").strip()
225
+ date = pd.to_datetime(item.findtext("pubDate"), errors="coerce", utc=True)
226
+ if title and not pd.isna(date) and GOLD_MACRO_KEYWORDS.search(title):
227
+ rows.append({"date": date.tz_localize(None), "headline": title, "source": source})
228
+ result = pd.DataFrame(rows, columns=["date", "headline", "source"])
229
+ if result.empty:
230
+ return result
231
+ return result.drop_duplicates(subset=["headline"]).sort_values("date", ascending=False).reset_index(drop=True)
232
+
233
+
234
+ def score_headlines(headlines: Iterable[Any], model_name: str = "ProsusAI/finbert") -> float:
235
+ texts = filter_gold_macro_headlines(headlines)
236
+ if not texts:
237
+ return float("nan")
238
+ try:
239
+ from transformers import pipeline
240
+ try:
241
+ classifier = pipeline("text-classification", model=model_name, top_k=None, device=0)
242
+ except (RuntimeError, OSError, ValueError):
243
+ with warnings.catch_warnings():
244
+ warnings.simplefilter("ignore", UserWarning)
245
+ classifier = pipeline("text-classification", model=model_name, top_k=None, device=-1)
246
+ values = []
247
+ for output in classifier(texts, truncation=True, max_length=256):
248
+ labels = output if isinstance(output, list) else [output]
249
+ scores = {str(item["label"]).lower(): float(item["score"]) for item in labels}
250
+ positive = scores.get("positive", scores.get("bullish", 0.0))
251
+ negative = scores.get("negative", scores.get("bearish", 0.0))
252
+ neutral = scores.get("neutral", 0.0)
253
+ values.append((positive - negative) * (1.0 - 0.25 * neutral))
254
+ return float(np.mean(values)) if values else float("nan")
255
+ except (ImportError, OSError, RuntimeError, ValueError):
256
+ bullish = re.compile(
257
+ r"\b(weak dollar|dollar falls?|dxy falls?|lower yield|rate cut|"
258
+ r"dovish|recession|uncertainty|gold rises?)\b", re.I,
259
+ )
260
+ bearish = re.compile(
261
+ r"\b(strong dollar|dollar rises?|dxy rises?|higher yield|yields? rise|"
262
+ r"rate hike|hawkish|hot inflation|cpi rises|ppi rises|payrolls beat)\b", re.I,
263
+ )
264
+ values = []
265
+ for text in texts:
266
+ positive = len(bullish.findall(text))
267
+ negative = len(bearish.findall(text))
268
+ values.append((positive - negative) / max(1, positive + negative))
269
+ return float(np.mean(values)) if values else float("nan")
270
+
271
+
272
+ def build_feature_frame(daily: pd.DataFrame, macro: Optional[pd.DataFrame] = None,
273
+ cot: Optional[pd.DataFrame] = None,
274
+ news: Optional[pd.DataFrame] = None) -> pd.DataFrame:
275
+ """Align market, macro, positioning, ETF, volatility, and NLP velocity features."""
276
+ frame = daily.copy().sort_index()
277
+ if macro is not None:
278
+ frame = frame.join(macro.reindex(frame.index, method="ffill"), how="left")
279
+ if cot is not None and not cot.empty:
280
+ frame = frame.join(cot.reindex(frame.index, method="ffill"), how="left")
281
+ sentiment = 0.0
282
+ if news is not None and not news.empty:
283
+ scored_sentiment = score_headlines(news["headline"].tolist())
284
+ sentiment = 0.0 if pd.isna(scored_sentiment) else float(scored_sentiment)
285
+ frame = add_sentiment_and_flow(frame, sentiment, cot)
286
+ frame["sentiment"] = sentiment
287
+ frame["sentiment_velocity"] = frame["sentiment"].diff().fillna(0.0)
288
+ for column in ("tips10y", "curve_spread_10y2y", "commercial_positions",
289
+ "noncommercial_positions", "gvz_close", "gld_volume", "iau_volume"):
290
+ if column not in frame:
291
+ frame[column] = 0.0
292
+ frame[column] = pd.to_numeric(frame[column], errors="coerce").ffill().bfill().fillna(0.0)
293
+ scaled_columns = [
294
+ "gvz_close", "gld_volume", "iau_volume",
295
+ "commercial_positions", "noncommercial_positions",
296
+ ]
297
+ frame[scaled_columns] = RobustScaler().fit_transform(frame[scaled_columns])
298
+ frame["gold_return"] = frame["gold_close"].pct_change()
299
+ frame["dxy_return"] = frame["dxy_close"].pct_change()
300
+ return frame.replace([np.inf, -np.inf], np.nan).dropna(subset=["gold_close"])