40 KiB
APEX — Currency Strength Engine
Version: 1.1.0
Timeframe: Swing / Position Trading (1–5 day holds)
Architecture: S.A.T.O.R.I. (Statistical Arbitrage Trading & Orchestrated Reversion Index)
Layer: Layer 1 (Fundamental) + Layer 2 (Technical/Statistical)
Methodology: Dr. Giavon's Deconstructed Currency Strength Indexing
1. Project Overview
APEX is a desktop-based Currency Strength Engine that implements institutional-quality statistical arbitrage (StatArb) for the forex market. It deconstructs all 28 major cross-pairs to isolate the true strength/weakness of individual currencies, then generates mean-reversion signals when statistical divergences reach extreme thresholds.
Core Principle
Instead of analyzing EUR/USD as a single entity, APEX decomposes every pair to isolate individual currency strength indices:
Individual Currency Strength = Average Z-Score Across ALL 7 Pairs Involving That Currency
EUR_Strength = avg(Z(EUR_USD), Z(EUR_GBP), Z(EUR_JPY), Z(EUR_AUD), Z(EUR_CAD), Z(EUR_CHF), Z(EUR_NZD))
USD_Strength = avg(Z(USD_EUR), Z(USD_GBP), Z(USD_JPY), Z(USD_AUD), Z(USD_CAD), Z(USD_CHF), Z(USD_NZD))
... and so on for all 8 currencies
Trading Philosophy
| Component | Strategy |
|---|---|
| Timeframe | Swing / Position — 1 to 5 day holds |
| Entry Trigger | Matrix Cross divergence: one currency overbought (Z > +2.0) across ALL pairs, another oversold (Z < -2.0) simultaneously |
| Execution | Short the strongest, buy the weakest — bet on mathematical mean reversion |
| Risk Management | No single-pair stop losses. Basket hedging across correlated pairs + grid hedging |
| Exit | Aggregate portfolio P&L goes net positive (portfolio-based exit, not per-pair) |
| Z-Score Anchor | 288 M5 bars = 24 hours of historical data (not tick noise) |
| Session Tracking | Tracks Tokyo / London / New York opens with Session Relative Velocity |
2. Architecture
┌────────────────────────────────────────────────────────────────────┐
│ APEX APPLICATION │
├────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ UI LAYER (6 Tabs) │ │
│ │ ┌──────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ ┌──────┐ │ │
│ │ │Dashboard │ │Data │ │Layer 2 │ │Confluence│ │Hist.│ │ │
│ │ │(Fundamen)│ │Entry │ │Monitor │ │Signals │ │ │ │ │
│ │ └────┬─────┘ └────────┘ └────┬─────┘ └────┬────┘ └──────┘ │ │
│ └───────┼───────────────────────┼─────────────┼────────────────┘ │
│ │ │ │ │
│ ┌───────▼───────────────────────▼─────────────▼────────────────┐ │
│ │ BUSINESS LOGIC LAYER │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ Scoring (L1) │ │Technical (L2)│ │ Matrix Engine │ │ │
│ │ │ scorer.py │ │layer2_tech │ │ currency_strength│ │ │
│ │ │ │ │ .py │ │ _matrix.py │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────▼────────────────▼──────────────────▼──────────┐ │ │
│ │ │ CONFLUENCE FILTER │ │ │
│ │ │ confluence_filter.py │ │ │
│ │ │ Layer 1 + Layer 2 + Matrix = Entry Signal │ │ │
│ │ └──────────────────────┬──────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────────▼──────────────────────────────┐ │ │
│ │ │ RISK MANAGEMENT SYSTEM │ │ │
│ │ │ risk_management.py │ │ │
│ │ │ Position Sizing + Grid Hedge + Basket Hedge + │ │ │
│ │ │ Portfolio P&L Tracking + Aggregate Exit │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ┌───────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │
│ │ │ FRED API │ │ MT5 Data │ │ SQLite │ │ Excel Import │ │ │
│ │ │(interest │ │(forex │ │ Database │ │ (CPI/PMI) │ │ │
│ │ │ rates) │ │ prices) │ │ apex.db │ │ │ │ │
│ │ └───────────┘ └──────────┘ └──────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
The Two Layers
| Layer | Input | Frequency | Output |
|---|---|---|---|
| Layer 1 (Fundamental) | Interest rates (FRED), CPI, PMI (manual/Excel) | Monthly | Currency scores (0-100), Strongest/Weakest ranking |
| Layer 2 (Technical) | 288 M5 bars (24h) + live poll prices | Bar-anchored, tick-displayed | 28 pair Z-scores (anchored to 24h μ/σ), 8 currency strength indices, Matrix Cross |
Critical design: Z-scores are NOT calculated over raw tick polls. On connect, the analyzer is seeded with 288 M5 bars of historical close prices. μ and σ are computed from this 24-hour window. Live tick prices are compared against this stable anchor, producing meaningful multi-hour deviation readings that don't flip on every tick.
Fallback: If bar data hasn't been seeded yet, the system falls back to a 20-tick deque for immediate display. Once seed_bars() is called, the bar anchor takes over permanently.
3. Directory Structure
apex_layer1/
│
├── __init__.py # Package marker (v1.0.0)
├── main.py # Application entry point
├── main_window.py # QMainWindow + tab assembly
├── config.py # All configuration & constants from .env
│
├── data_feeder.py # MT5 + Mock data feeders
├── fred_client.py # FRED interest rate API client
├── database.py # SQLite database manager
│
├── scorer.py # Layer 1 scoring engine
├── layer2_technical.py # Layer 2 Z-score engine (28 pairs)
├── currency_strength_matrix.py # S.A.T.O.R.I. currency strength index
├── confluence_filter.py # Layer 1 + Layer 2 + Matrix merging
├── risk_management.py # Position sizing, hedging, portfolio mgmt
│
├── create_excel_template.py # Excel/CSV template generator
├── requirements.txt # Python dependencies
│
├── .env # Live configuration (API keys)
├── .env.example # Configuration template
├── apex.db # SQLite database (auto-created)
│
├── ui/
│ ├── __init__.py
│ ├── dashboard_tab.py # Tab 1: Layer 1 fundamental signals
│ ├── entry_tab.py # Tab 2: CPI/PMI data entry
│ ├── layer2_monitor_tab.py # Tab 3: Live Z-scores + Matrix
│ ├── confluence_tab.py # Tab 4: Merged signals
│ ├── history_tab.py # Tab 5: Past signals
│ └── settings_tab.py # Tab 6: Configuration
4. File-by-File Breakdown
4.1 Entry Point
main.py
Launches the PyQt5 application. Validates FRED API key exists, creates QApplication, instantiates MainWindow, runs event loop.
main()— Application entry point. Checksconfig.FRED_API_KEY, createsQApplication, showsMainWindow, starts event loop.
__init__.py
Package marker. Exports __version__ = "1.0.0".
4.2 Configuration
config.py
Loads .env via python-dotenv. Defines ALL constants used across the application.
| Constant | Default | Description |
|---|---|---|
CURRENCIES |
["USD","EUR","GBP","JPY","AUD","CAD","CHF","NZD"] |
The 8 major currencies |
CB_TARGETS |
Per-currency dict | Central bank inflation targets (2.0% most, AUD=2.5, CHF=1.5) |
FRED_SERIES |
Per-currency dict | FRED series IDs for interest rates |
WEIGHT_RATE |
0.50 |
Interest rate weight in L1 scoring |
WEIGHT_CPI |
0.30 |
CPI deviation weight in L1 scoring |
WEIGHT_PMI |
0.20 |
PMI composite weight in L1 scoring |
MIN_GAP_TO_TRADE |
20 |
Minimum score gap required for signal |
Z_SCORE_THRESHOLD |
2.0 |
Overbought/oversold threshold (std devs) |
Z_SCORE_LOOKBACK |
20 |
Bars for Z-score calculation |
MT5_SYMBOL_SUFFIX |
"" |
Broker-specific MT5 suffix (e.g., .m) |
ACCOUNT_BALANCE |
10000 |
Starting account balance |
RISK_PER_TRADE |
0.01 |
1% risk per trade |
MAX_PORTFOLIO_LEVERAGE |
2.0 |
Max 2:1 leverage |
GRID_LEVELS |
3 |
Hedge grid levels |
USE_GRID_HEDGING |
true |
Enable grid hedging |
DEBUG |
false |
Debug output toggle |
validate_config()— Validates all config on import. RaisesValueErrorif FRED key or critical settings are missing.
4.3 Data Layer
data_feeder.py
Two data feeder implementations with the same interface (polymorphic):
Class Mt5DataFeeder — Real data from MetaTrader 5 terminal.
| Method | Returns | Description |
|---|---|---|
initialize() |
bool |
Connect to MT5 terminal |
test_connection() |
bool |
Alias for initialize |
shutdown() |
— | Disconnect MT5 |
get_connection_status() |
str |
Human-readable status |
get_current_price(pair) |
dict|None |
Bid/ask/mid via symbol_info_tick() |
fetch_all_rates() |
dict |
Fetch 7 USD pairs, derive all 28 cross rates |
get_all_major_pairs() |
list[str] |
All 28 pairs (56 permutations) |
get_exchange_rate(from, to) |
float|None |
Single cross rate |
get_historical_candles(...) |
list[dict]|None |
OHLC bars via copy_rates_from_pos() |
stream_prices(...) |
— | Threaded polling loop |
stop_streaming() |
— | Stop the poll loop |
Strategy: Fetches only 7 major USD pairs (EUR_USD, GBP_USD, AUD_USD, NZD_USD, USD_JPY, USD_CAD, USD_CHF), converts each to "how many USD per 1 unit", then derives all 28 cross rates mathematically. This avoids the problem that most MT5 brokers don't have symbols for exotic crosses like AUDEUR, AUDGBP, etc.
Key internal:
USD_PAIRS = ["EUR_USD", "GBP_USD", "AUD_USD", "NZD_USD",
"USD_JPY", "USD_CAD", "USD_CHF"]
# For EUR_USD: usd_rates["EUR"] = mid_price
# For USD_JPY: usd_rates["JPY"] = 1.0 / mid_price
# Cross rate: rate[base][quote] = usd_rates[base] / usd_rates[quote]
Class MockDataFeeder — Simulates prices with random walk around base prices for 8 major pairs. Same interface as Mt5DataFeeder for testability.
fred_client.py
Class FredClient — Fetches interest rates from FRED API.
| Method | Returns | Description |
|---|---|---|
__init__(api_key, timeout) |
— | Validates API key |
fetch_rate(currency, max_retries) |
float|None |
Single currency rate with exponential backoff |
fetch_all_rates(max_retries) |
dict |
All 8 currencies |
get_cached_rate(currency) |
float|None |
Cache lookup |
clear_cache() |
— | Reset cache |
Uses FRED series IDs from config.FRED_SERIES:
- USD →
FEDFUNDS, EUR →ECBDFR, GBP →BOEBR, JPY →IRSTJPN - AUD →
RBATCTR, CAD →BOCCRT, CHF →SNBPOL, NZD →RBNZOCR
database.py
Class Database — SQLite database with 4 tables.
| Table | Columns | Purpose |
|---|---|---|
rates |
currency, rate, updated_at, source |
Interest rates from FRED |
monthly_data |
month, currency, cpi_actual, pmi_actual, entered_at |
CPI/PMI entries |
scores |
month, currency, score_rate, score_cpi, score_pmi, total_score, rank |
Calculated scores |
signals |
month, strongest, weakest, gap, signal, status |
Trade signals |
All tables use ON CONFLICT ... DO UPDATE (upsert) for idempotent writes. Foreign keys enforced via PRAGMA.
Key methods: upsert_rate(), update_monthly_cpi(), update_monthly_pmi(), save_scores(), save_signal(), get_month_scores(), get_all_signals(), get_month_completeness().
4.4 Business Logic — Layer 1 (Fundamental)
scorer.py
Pure functions (no classes). Implements the scoring formula:
Score = (Rate_Differential × 50%) + (CPI_Deviation × 30%) + (PMI × 20%)
Each component is min-max normalized to 0-100 before weighting.
| Function | Returns | Description |
|---|---|---|
normalise(values) |
list[float] |
Min-max scaling to 0-100 |
calculate_rate_differentials(rates) |
dict |
Rate minus G8 average |
calculate_cpi_deviations(cpi_values) |
dict |
Actual CPI minus CB target |
score_all_currencies(rates, cpi, pmi) |
dict |
Full scoring pipeline |
get_ranked_list(scores) |
list[tuples] |
Sorted by score descending |
pair_currencies(scores) |
(strongest, weakest, gap) |
Top vs bottom score |
generate_signal(scores) |
(signal, status, gap_desc) |
"SHORT X/Y" or "NO TRADE" |
validate_scores(scores) |
bool |
Validates all fields and ranges |
4.5 Business Logic — Layer 2 (Technical)
layer2_technical.py
Class TechnicalAnalyzer — Real-time Z-score engine.
Initializes 56 deques (all permutations of 8 currencies) with maxlen=20. Each incoming price tick appends to the deque and recalculates the Z-score.
| Method | Description |
|---|---|
add_price_data(pair, price, volume) |
Append price, recalculate Z-score |
get_z_score(pair) |
Current Z-score for any pair |
is_extreme(pair) |
` |
get_overbought_pairs() |
All pairs with Z >= 2.0 |
get_oversold_pairs() |
All pairs with Z <= -2.0 |
get_volatility(pair) |
Standard deviation of recent prices |
get_mean_price(pair) |
Mean price over lookback |
is_mean_reverting(pair) |
` |
get_last_price(pair) |
Most recent price |
get_all_z_scores() |
Dict of all 56 pair Z-scores |
get_status_for_pair(pair) |
Dict with label (SEVERELY OVERBOUGHT → Neutral) |
Z-score formula: Z = (current_price - mean) / std_dev
Class TechnicalSignal — Signal generation from Z-scores.
should_enter_on_extreme()→ True if|Z| >= 2.0should_exit_on_mean_reversion()→ True if|Z| < 0.5get_signal_strength()→ 0-100 scale
currency_strength_matrix.py
Class CurrencyStrengthMatrix — S.A.T.O.R.I. individual currency strength index.
This is the core mathematical innovation. Deconstructs all 28 pair Z-scores into 8 individual currency strength indices.
How it works:
For each currency, collects Z-scores from all 7 pairs where it is the base:
EUR_Strength = avg(Z(EUR_USD), Z(EUR_GBP), Z(EUR_JPY), Z(EUR_AUD), Z(EUR_CAD), Z(EUR_CHF), Z(EUR_NZD))
USD_Strength = avg(Z(USD_EUR), Z(USD_GBP), Z(USD_JPY), Z(USD_AUD), Z(USD_CAD), Z(USD_CHF), Z(USD_NZD))
Output:
| Currency | Avg Z-Score | Direction |
|---|---|---|
| EUR | +2.3 | OVERBOUGHT |
| USD | +1.1 | NEUTRAL |
| ... | ... | ... |
| JPY | -2.5 | OVERSOLD |
The Matrix Cross = Strongest currency vs Weakest currency (e.g., EUR_JPY).
| Method | Description |
|---|---|
update(z_scores) |
Recompute from 56 pair Z-scores |
get_strongest() |
Highest avg Z-score currency |
get_weakest() |
Lowest avg Z-score currency |
get_matrix_cross() |
Strongest_Weakest pair |
get_divergence_gap() |
strongest_z - weakest_z |
has_divergence() |
True if one overbought AND one oversold |
get_strong_currencies() |
List of overbought currencies |
get_weak_currencies() |
List of oversold currencies |
get_ranked_list() |
All 8 sorted by strength |
get_report() |
Dict with all matrix data |
confluence_filter.py
Class ConfluenceFilter — Merges all three signal sources.
Entry logic (two-tier):
-
Primary — Matrix Divergence:
- One currency overbought across ALL pairs
- Another currency oversold across ALL pairs
- Trade the Matrix Cross (strongest vs weakest)
- Confidence = spread / 4.0 × 100
-
Secondary — Layer 1 + Layer 2:
- Layer 1 bias (fundamental strongest/weakest)
- Layer 2 pair extreme (|Z| >= 2.0 on that specific pair)
- 50% gap confidence + 50% Z confidence
Exit logic (two-tier):
- Matrix divergence gap collapses (divergence no longer exists)
- Single-pair Z-score mean reverts below 0.5
| Method | Description |
|---|---|
set_layer1_bias(strongest, weakest, gap) |
Store current L1 signal |
check_entry_confluence() |
(bool, reason, strength) |
check_exit_confluence() |
(bool, reason) |
is_conflicting() |
L1 bullish but L2 bearish |
get_confluence_report() |
Full report with matrix data |
get_all_signals() |
All ranked signals |
Class SignalHistory — Tracks up to 1000 signals with win-rate calculation.
risk_management.py
Five classes implementing professional risk management:
Class PositionSizer
- Risk-based position sizing:
size = (balance × 0.01) / (stop_loss × pip_value) × confidence_multiplier - Clamped to 0.01–5.0 lots
Class GridHedging
- Creates N-level hedge grid below entry price
- Each hedge level = 50% × position_size / (N-1)
Class PortfolioExposure
- Tracks all open positions
- Enforces max leverage (default 2:1)
- Rejects new positions that would exceed limit
Class BasketHedging — S.A.T.O.R.I. statistical arbitrage hedging.
- Pre-defined correlation clusters:
EUR_USD→ hedges withEUR_GBP,EUR_JPY,GBP_USDGBP_USD→ hedges withGBP_JPY,EUR_GBP,EUR_USDUSD_JPY→ hedges withUSD_CHF,USD_CAD,EUR_JPYAUD_USD→ hedges withAUD_JPY,NZD_USD,AUD_CADNZD_USD→ hedges withAUD_USD,NZD_JPY,NZD_CAD
- Each correlated pair gets 30% of primary size / len(cluster)
Class RiskManagementSystem — Combines all four.
execute_signal()→ full trade execution with sizing + grid + basketcalculate_basket_pnl()→ aggregate unrealized P&L across ALL positions + hedgesshould_exit_portfolio()→ exit when total P&L > 0 (portfolio-based, not per-pair)close_all_trades()→ close all positions at given exit pricesget_portfolio_summary()→ positions, exposure, leverage, P&L
4.6 UI Layer
main_window.py
Class MainWindow(QMainWindow) — Application shell.
Creates 6-tab QTabWidget, instantiates all tabs, connects inter-tab signals.
Data flow assembly:
1. Entry tab saves data → Dashboard refreshes
2. Entry tab saves data → History tab refreshes
3. Dashboard generates signal → Confluence tab receives bias
4. Dashboard requests fetch → FredFetchWorker starts
5. FRED completes → Dashboard updates rates
Class FredFetchWorker(QThread) — Background FRED API fetch. Saves rates to DB, emits rates_fetched or error_occurred.
ui/dashboard_tab.py — Tab 1
Class DashboardTab(QWidget)
Displays Layer 1 fundamental analysis:
- Signal card — Large text: "SHORT JPY/USD" or "NO TRADE", gap score, tier, timestamp
- Score table — 8 rows × 8 columns (Rank, Currency🇺🇸, Rate%, CPI%, PMI, Score, Signal, Strength bar)
- Strongest row highlighted green with "BUY" tag
- Weakest row highlighted red with "SELL" tag
- Color-coded score bars (green/red/gray for Rate/CPI/PMI contributions)
- "Fetch Rates (FRED)" button
Signals: fetch_rates_requested, signal_generated(strongest, weakest, gap)
ui/entry_tab.py — Tab 2
Class MonthlyEntryTab(QWidget)
Manual data entry for CPI and PMI:
- Month selector (dropdown, 24 months)
- CPI table: Currency, Target%, Actual CPI (spinbox), Delta (color-coded), Done
- PMI table: Currency, Neutral 50, PMI (spinbox), Signal label (Expanding/Contracting), Done
- Progress bar: X/16 fields filled
- Import Excel button (supports both multi-sheet xlsx and CSV)
- Save button (enabled only when 16/16 complete)
- On save: loads rates from DB → runs
scorer.score_all_currencies()→ saves scores → generates signal → emitsdata_saved
Signals: data_saved(month)
ui/layer2_monitor_tab.py — Tab 3
Class Layer2MonitorTab(QWidget)
Class DataStreamerThread(QThread)
Real-time technical analysis with S.A.T.O.R.I. matrix:
- Connection panel: Source dropdown (MT5 Live / Mock Test), Connect/Disconnect, status indicator
- Z-score table: All 28 pairs with Price, Z-Score (red when extreme), Volatility, Mean, Status, Signal
- Overbought/Oversold alerts: Comma-separated lists
- Currency Strength Matrix panel:
- Matrix Cross label (strongest vs weakest currency)
- Divergence Gap (sigma spread)
- DIVERGENCE DETECTED alert (red) when one currency overbought + one oversold
- Ranked currency table: 8 rows × 4 columns (Rank, Currency, Strength Z, Direction)
- Color-coded: OVERBOUGHT (red), OVERSOLD (green)
- Auto-refresh checkbox, Refresh Now button
Data flow: Streamer thread polls feeder → emits price_updated → feeds TechnicalAnalyzer → recomputes CurrencyStrengthMatrix → refreshes display.
ui/confluence_tab.py — Tab 4
Class ConfluenceSignalsTab(QWidget)
Merged signal display and execution:
- Status card: Layer 1 bias (pair, gap), Layer 2 extreme (pair, Z-score), Matrix Cross, Top 3 → Bottom 3 ranked currencies, Confluence result with confidence %
- Signals table: 10 rows × 8 columns (Pair, L1 Gap, L2 Z-Score, Status, Confidence, Entry Price, Position Size, Action)
- Matrix divergence signals shown in purple, standard confluence in green
- Risk panel: Portfolio exposure progress bar, leverage ratio
- Buttons: Refresh, Execute Top Signal (runs
RiskManagementSystem) - Auto-refresh every 5 seconds
ui/history_tab.py — Tab 5
Class HistoryTab(QWidget)
Past signal history:
- Table with 6 columns: Month, Signal, Gap, Strongest (flag), Weakest (flag), Status
- Status color-coded: ACTIVE (green), NO_TRADE (red)
- Click any row → popup with full score breakdown for all 8 currencies
- Auto-refreshes when new data saved
ui/settings_tab.py — Tab 6
Class SettingsTab(QWidget)
Class FredTestWorker(QThread)
Class Mt5TestWorker(QThread)
Configuration interface:
- FRED API: Key input (masked), Test Connection button, status
- MT5: Symbol suffix input, Test Connection button, status
- CB Targets: Read-only display of all 8 targets
- Scoring Weights: 3 spinboxes (Rate/CPI/PMI %) with live total validation (must = 100%)
- Trading Rules: Minimum gap spinbox (5-100)
- App Settings: Auto-fetch checkbox
- Save: Writes .env file (requires restart)
- Reset: Confirmation dialog, restores defaults
4.7 Utility
create_excel_template.py
Generates example Excel/CSV files for data import testing:
example_monthly_data.xlsx(multi-sheet: CPI + PMI)example_monthly_data_single_sheet.xlsx(all in one sheet)example_monthly_data.csv
Each contains 8 currencies with example values.
5. Data Flow Diagrams
Layer 1 (Fundamental) — Monthly Cycle
User enters CPI/PMI
│
▼
Entry Tab → Save Clicked
│
├──► Read all 8 CPI + 8 PMI from spinboxes
├──► Load interest rates from DB (from FRED)
├──► scorer.score_all_currencies(rates, cpi, pmi)
│ ├── normalise(rate_differentials) × 0.50
│ ├── normalise(cpi_deviations) × 0.30
│ ├── normalise(pmi_raw) × 0.20
│ └── sum → total_score 0-100
├──► scorer.generate_signal(scores)
│ ├── pair_currencies → strongest, weakest, gap
│ ├── gap >= 20 → "SHORT {weak}/{strong}"
│ └── gap < 20 → "NO TRADE"
├──► database.save_scores()
├──► database.save_signal()
└──► emit data_saved → Dashboard + History refresh
Layer 2 (Technical) — Real-time with Bar Seeding
MT5 Terminal (or Mock)
│
├── On Connect:
│ generate_mock_bars(288) ◄── Mock: simulates 24h of M5 data
│ │ or
│ fetch_historical_bars(288, M5) ◄── MT5: real bars from terminal
│ │
│ ▼
│ TechnicalAnalyzer.seed_bars(bars) ◄── Populates bar_history with 288 closes
│ │ ◄── μ and σ now anchored to 24h
│ │
│ ▼
│ DataStreamerThread.start()
│
└──► every 1s:
fetch_all_rates() → 7 USD pairs → derive 28 crosses
│
├──► emit price_updated(pair, mid)
│
▼
Layer2MonitorTab._on_price_received
│
├──► TechnicalAnalyzer.add_price_data(pair, price)
│ └── _update_z_score(pair)
│ μ, σ = _get_mean_std(pair)
│ │ priority: bar_history (288 bars) → tick_fallback (20 ticks)
│ ▼
│ Z = (current_price - μ) / σ
│
├──► CurrencyStrengthMatrix(z_scores)
│ └── For each currency: avg Z across 7 base pairs
│
└──► _refresh_display()
├── Update 28-pair Z-score table
├── Update currency strength matrix table
├── Update matrix cross / divergence alerts
├── Update overbought/oversold lists
└── Show active session (Tokyo/London/New York)
Confluence — Entry Signal
Layer 1 (monthly) Layer 2 (real-time)
│ │
▼ ▼
dashboard_tab.signal_generated ThermalAnalyzer.z_scores
│ │
▼ ▼
ConfluenceFilter.set_layer1_bias CurrencyStrengthMatrix
│ │
└──────────┬───────────────────────┘
▼
ConfluenceFilter.check_entry_confluence()
│
├── Matrix divergence? → YES → Trade matrix cross
├── L1 + L2 extreme? → YES → Trade paired pair
└── Neither? → NO TRADE
│
▼
RiskManagementSystem.execute_signal()
│
├── PositionSizer → size = f(confidence)
├── GridHedging → 3-level hedge grid
├── BasketHedging → correlated pair hedges
└── PortfolioExposure → leverage check
6. Database Schema
-- Table 1: Interest rates from FRED
CREATE TABLE rates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
currency TEXT NOT NULL UNIQUE,
rate REAL NOT NULL,
updated_at TEXT NOT NULL,
source TEXT DEFAULT 'FRED',
CONSTRAINT valid_currency CHECK (currency IN ('USD','EUR','GBP','JPY','AUD','CAD','CHF','NZD'))
);
-- Table 2: Monthly CPI + PMI entries
CREATE TABLE monthly_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
month TEXT NOT NULL,
currency TEXT NOT NULL,
cpi_actual REAL,
pmi_actual REAL,
entered_at TEXT NOT NULL,
UNIQUE(month, currency),
CONSTRAINT valid_currency CHECK (currency IN ('USD','EUR','GBP','JPY','AUD','CAD','CHF','NZD')),
CONSTRAINT valid_month CHECK (month LIKE '____-__')
);
-- Table 3: Calculated scores
CREATE TABLE scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
month TEXT NOT NULL,
currency TEXT NOT NULL,
score_rate REAL,
score_cpi REAL,
score_pmi REAL,
total_score REAL NOT NULL,
rank INTEGER NOT NULL,
calculated_at TEXT NOT NULL,
UNIQUE(month, currency)
);
-- Table 4: Trade signals
CREATE TABLE signals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
generated_at TEXT NOT NULL,
month TEXT NOT NULL UNIQUE,
strongest TEXT NOT NULL,
weakest TEXT NOT NULL,
gap REAL NOT NULL,
signal TEXT NOT NULL,
status TEXT NOT NULL,
CONSTRAINT valid_status CHECK (status IN ('ACTIVE', 'NO_TRADE', 'CLOSED'))
);
7. Configuration (.env)
FRED_API_KEY=your_fred_api_key
MT5_SYMBOL_SUFFIX=
DB_PATH=apex.db
DEBUG=true
WEIGHT_RATE=0.50
WEIGHT_CPI=0.30
WEIGHT_PMI=0.20
MIN_GAP=20.0
AUTO_FETCH_RATES_ON_STARTUP=true
Z_SCORE_THRESHOLD=2.0
Z_SCORE_LOOKBACK=20
ACCOUNT_BALANCE=10000.0
RISK_PER_TRADE=0.01
MAX_PORTFOLIO_LEVERAGE=2.0
USE_GRID_HEDGING=true
GRID_LEVELS=3
8. Technology Stack
| Component | Technology | Version |
|---|---|---|
| Language | Python | 3.10+ |
| UI Framework | PyQt5 | 5.15.9 |
| Database | SQLite | Built-in |
| HTTP Client | requests | 2.31+ |
| Data Processing | pandas | 2.1+ |
| Excel Support | openpyxl | 3.1+ |
| Environment | python-dotenv | 1.0+ |
| Forex Data | MetaTrader5 | Latest |
| Interest Rates | FRED API | Free tier |
| Packaging | PyInstaller | 6.1+ |
9. Scoring Formula Reference
Layer 1 — Fundamental Score
rate_diff[i] = rate[i] - G8_average_rate
cpi_dev[i] = actual_cpi[i] - cb_target[i]
pmi_raw[i] = pmi_value[i]
normalize(x) = (x - min) / (max - min) × 100 // 0-100 scale
score_total[i] = normalise(rate_diff)[i] × 0.50
+ normalise(cpi_dev)[i] × 0.30
+ normalise(pmi_raw)[i] × 0.20
gap = score_total[strongest] - score_total[weakest]
Layer 2 — Technical Score (Bar-Anchored)
Step 1: Seed bar_history with 288 M5 close prices (24 hours)
Step 2: μ_bars = mean(bar_history), σ_bars = stdev(bar_history)
Step 3: For each incoming tick:
Z[pair] = (current_tick_price - μ_bars) / σ_bars
Fallback (if bar_history empty):
Z[pair] = (current_tick_price - mean(ticks)) / stdev(ticks)
Step 4: Individual Currency Strength = avg(Z[currency_X] over all 7 base pairs)
Step 5: Session Relative Velocity (SRV):
At session open (Tokyo/London/NY), snapshot all prices.
SRV[pair] = ((current_price - session_open_price) / session_open_price) × 100
Entry Conditions
Matrix Divergence: any(avg_Z > +2.0) AND any(avg_Z < -2.0) → Trade Matrix Cross
Pair Confluence: L1_gap >= 20 AND L2_Z >= 2.0 on same pair → Trade that pair
10. 28 Currency Pairs (Generated)
All 8 currencies produce 56 permutations (28 pairs × 2 directions):
| Base | Pairs (base_quote) |
|---|---|
| USD | USD_EUR, USD_GBP, USD_JPY, USD_AUD, USD_CAD, USD_CHF, USD_NZD |
| EUR | EUR_USD, EUR_GBP, EUR_JPY, EUR_AUD, EUR_CAD, EUR_CHF, EUR_NZD |
| GBP | GBP_USD, GBP_EUR, GBP_JPY, GBP_AUD, GBP_CAD, GBP_CHF, GBP_NZD |
| JPY | JPY_USD, JPY_EUR, JPY_GBP, JPY_AUD, JPY_CAD, JPY_CHF, JPY_NZD |
| AUD | AUD_USD, AUD_EUR, AUD_GBP, AUD_JPY, AUD_CAD, AUD_CHF, AUD_NZD |
| CAD | CAD_USD, CAD_EUR, CAD_GBP, CAD_JPY, CAD_AUD, CAD_CHF, CAD_NZD |
| CHF | CHF_USD, CHF_EUR, CHF_GBP, CHF_JPY, CHF_AUD, CHF_CAD, CHF_NZD |
| NZD | NZD_USD, NZD_EUR, NZD_GBP, NZD_JPY, NZD_AUD, NZD_CAD, NZD_CHF |
Each currency's individual strength is computed from its 7 base pairs.
11. Refactoring Changelog (Session-Based Quantitative Engine)
Task 1.1 — Statistical Lookback Window (config.py, layer2_technical.py)
config.py: AddedBAR_TIMEFRAME,BAR_LOOKBACK_HOURS,BAR_LOOKBACK_BARS,HISTORICAL_POLL_INTERVALconstants. Default lookback changed from 20 ticks to 288 bars (24h of M5 data).layer2_technical.py:TechnicalAnalyzernow maintains two data streams:bar_history(deque of M1/M5 close prices, length =BAR_LOOKBACK_BARS) — the multi-hour statistical anchorprice_history(short deque of tick/poll data) — for UI display_get_bar_mean_std()computes μ/σ from bar history only_update_z_score()usesZ = (current_tick - μ_bars) / σ_barsadd_bar()method for feeding completed M1/M5 candles into the historical frame
Task 1.2 — Session-Based Indexing (currency_strength_matrix.py)
- New
SessionTrackerclass:- Detects active session from UTC hour (Tokyo 00-08, London 07-16, New York 13-22)
- On session open, snapshots start prices for all 28 pairs
- Computes Session Relative Velocity (SRV):
% change = (current - session_start) / session_start × 100
CurrencyStrengthMatrix.update()now acceptscurrent_pricesdict for session trackingCurrencyStrengthdataclass has newsession_srv: floatfieldget_report()includesactive_sessionkey
Task 2.1 — Live Order Book Subscriptions (data_feeder.py)
Mt5DataFeeder.get_order_book(pair)— fetches live bid/ask/spread viamt5.symbol_info_tick()+mt5.symbol_info()for the exact trade symbolPositionSizer.calculate_position_size()accepts optionalbid,ask,spreadparams; wide spreads reduce position size by up to 20%MockDataFeederhas matchingget_order_book()implementation
Task 2.2 — SQLite WAL Mode + Bar Cache (database.py)
- Connection now sets:
PRAGMA journal_mode=WAL,PRAGMA synchronous=NORMALfor concurrent read/write performance - New
bar_cachetable:(id, pair, timeframe, bar_time, open, high, low, close, volume)with unique constraint on(pair, timeframe, bar_time)and compound index - New methods:
upsert_bar(),get_bars(),get_latest_bar_time()
Task 3.1 — Layer 1 as Directional Regime Filter (confluence_filter.py)
check_entry_confluence()now uses Layer 1 as a Directional Regime Filter:- Primary signal: Matrix divergence (self-sufficient)
- Secondary: Layer 2 extremes only valid if aligned with Layer 1 macro bias
- Contrarian Layer 2 signals (Z < -threshold opposite Layer 1 direction) → BLOCKED with reason
- Aligned signals capped at 70% confidence (downgraded vs matrix divergence)
layer1_is_activeflag replaces raw gap comparison
Task 3.2 — Dynamic Pearson Correlation (risk_management.py, data_feeder.py)
- New
pearson_correlation(x, y)function:r = Σ(x-x̄)(y-ȳ) / √(Σ(x-x̄)² · Σ(y-ȳ)²) - New
CorrelationEngineclass:update_series(historical_closes)— feeds 30 days of close pricesget_correlation(pair_a, pair_b)— computes/caches r between any two pairsget_top_correlated(target, n=3, min_r=0.75)— returns top N pairs with |r| ≥ 0.75
BasketHedging.get_correlated_pairs()now delegates toCorrelationEngineinstead of hardcoded dictMt5DataFeeder.fetch_historical_closes_all_pairs(days=30)fetches the required dataMockDataFeederhas matching implementation
Task 3.3 — Aggregate Portfolio Profit Target Exit (risk_management.py)
get_dynamic_exit_target()— confidence-scaled profit target (base = 1% of equity, scales with avg confidence)- Background monitor thread
_monitor_exit_loop()pollscalculate_basket_pnl()every second - When net aggregate P&L > dynamic target, fires
close_all_trades()via registered callbacks start_exit_monitor(),stop_exit_monitor(),on_portfolio_exit()lifecycle management
Task 4.1 — Session Visualizations + σ Highlights (ui/layer2_monitor_tab.py)
- Active session indicator label with color-coded background: Tokyo (purple), London (blue), New York (orange), Off-Hours (gray)
- Currency Strength Matrix Z-score cells: solid red background with white text for ≥ +2.0σ, solid green with white text for ≤ -2.0σ
- New 5th column in matrix table: "Session SRV" showing percentage change since session open
- Emoji indicators removed from status labels for cleaner display
12. Bug Fixes & Stability (Round 2)
Fix 1 — Bar History Never Seeded (Z-scores always 0.0)
layer2_technical.py: Addedseed_bars(historical_bars)method to populatebar_historywith 288 M5 close prices on connectdata_feeder.py (Mock): Addedgenerate_mock_bars(n_bars=288)— generates 24h of simulated M5 data using an Ornstein-Uhlenbeck process (mean reversion + drift + noise) for all 28 pairs via USD pair derivationui/layer2_monitor_tab.py:_connect()calls_seed_historical_bars()before starting the streamer — bars are always seeded first
Fix 2 — Tick Price Anchored to Initial Base, Not Bar Data
data_feeder.py (Mock):_tick_price()now uses the last bar close as its anchor with ±0.0002 noise, instead of the initial base price with ±0.01 noise_current_bar_prices()returns the last cached bar close for each pair- This ensures Z-scores reflect the bar position relative to 24h history, not random tick noise
Fix 3 — Default Source Changed to Mock
ui/layer2_monitor_tab.py:source_combodefaults to"Mock (Test)"at index 0 to prevent unintended MT5 terminal connections on startup
Fix 4 — Persistent Matrix Instance
ui/layer2_monitor_tab.py:CurrencyStrengthMatrixis now a persistentself.matrixinstance, recreated only once.update()is called each refresh instead of creating a new object, preservingSessionTrackerstate across refreshes
Fix 5 — FRED Series IDs Updated
config.py: Updated 6 invalid/deprecated FRED series IDs (BOEBR,IRSTJPN,RBATCTR,BOCCRT,SNBPOL,RBNZOCR) to commonly used alternatives (BOEIR,IRSTCI01JPM156N,RBATR,BOCARR,SNBON,RBNZR)