Version: Merged Final (March 2026) Base: Claude Opus 4.6 review + original spec Panel additions cherry-picked from: Gemini 3.1 Pro, Grok 4.1, DeepSeek V3.1, GPT-4.1, Grok 4 Fast, Sonar Reasoning Pro Primary trading venue: OANDA (spot forex — all live price, order book, position book, spread, and swap data) Secondary venues: Kalshi and Polymarket (prediction market contracts on macro/economic data events — ladder structures for rate decisions, CPI, NFP outcomes) Status: Complete. Use this as the authoritative implementation reference.
This merged spec incorporates the Opus review (the most thorough: $1.07 cost, 8192 tokens out, grade B+) as the complete base, with unique additions cherry-picked from the other six panel members. The Opus review diagnosed the original spec accurately: exceptional domain depth, but approximately 3-4x larger than what is realistic to build simultaneously at $145/month. The build order in this merged spec reflects Opus's phased recommendation with targeted supplements from the rest of the panel.
Key findings from all reviewers (consensus):
A script that runs silently 24 hours a day, 5 days a week (Sunday 5PM ET to Friday 5PM ET) during forex market hours. It pulls price data and calculates indicators every 1-5 minutes, stores everything in a SQLite database, and never sends alerts or makes trades. Its only job is to build the deepest possible historical dataset so that AI research analysts can find patterns, confluences, and edges over time.
This script feeds data into a research pipeline. AI researchers and analysts receive snapshots from this database as "known facts" injected into their prompts. They don't search the internet for "what's EUR/USD doing" — they already know because this script tells them.
Primary trading venue: OANDA v20 REST API (live spot forex — all candle, order book, position book, spread, and carry data comes from here).
Secondary venues: Kalshi and Polymarket prediction markets carry contracts on macro outcomes (Fed rate decisions, CPI beats/misses, NFP ranges). These have ladder structures (multiple strikes, not just binary) and are tracked separately in Section 20.
Start with major and cross pairs that have liquid markets and prediction market contracts:
EUR/USD, GBP/USD, USD/JPY, USD/CHF, AUD/USD, NZD/USD, USD/CAD, EUR/GBP, EUR/JPY, GBP/JPY, AUD/JPY, NZD/JPY, EUR/AUD, GBP/AUD, EUR/CAD
Boss can add or remove pairs via a config file. The script reads the list at startup.
Every indicator below is calculated on these timeframes unless noted otherwise:
[PANEL: Sonar Reasoning Pro] For the complex calculations (S&R Engine, full ZigZag trend lines, Section 16 structure), restrict to 1H, 4H, Daily, Weekly, Monthly only. Use 5min and 15min strictly for basic indicators (RSI, MAs, SuperTrend) and entry timing. This reduces computational load by ~80% while preserving the institutional-grade macro edge.
The script pulls candle data (open, high, low, close, tick_volume) from OANDA for each timeframe and pair.
This is the primary trading strategy. Everything else in this spec supports it.
SuperTrend is a trend-following indicator. When price is above the line, the trend is bullish. When price is below, bearish. The moment it switches is a "flip."
What to calculate and store per pair, per timeframe, every candle close:
supertrend_direction: "bullish" or "bearish"supertrend_value: actual price level of the SuperTrend linesupertrend_flipped: boolean — did direction change on THIS candle?candles_since_last_flip: how many candles since the last flipprice_distance_to_st: current price minus SuperTrend valueWhen a flip occurs, create a separate event record:
flip_timestamp: exact time (UTC)flip_direction: "bullish" or "bearish"flip_price: price at flip momentst_line_at_flip: SuperTrend line value at flipatr_at_flip: ATR(14) value at that momentatr_trend_at_flip: "expanding" or "contracting" (compare ATR to its 5-candle moving average)higher_tf_agrees: does the next higher timeframe's SuperTrend agree?multi_tf_alignment_count: how many of the 7 timeframes currently agree with this flip?Outcome tracking (filled in later as price moves after the flip):
max_favorable_excursion: how far price moved in the flip's direction before reversing (pips + ATR multiples)max_adverse_excursion: how far price moved AGAINST the flip before the favorable movehit_2r: did price reach 2x ATR in the flip direction? (boolean + timestamp)hit_3r, hit_4r, hit_5r, hit_6r: same for 3x, 4x, 5x, 6x ATRtime_to_2r, time_to_3r, etc.: candles to reach each leveloutcome_window_expired: boolean — set to true after 50 candles if targets not hit. Freeze all outcome fields.[PANEL: Sonar Reasoning Pro] Critical: outcome backfill must have a hard stop. Set a maximum window of 50 candles per timeframe. After 50 candles, mark outcome_window_expired = true and freeze all outcome fields. Without this, win rate statistics are incomparable across trades — some tracked for 50 candles, others for 500.
[PANEL: Sonar Reasoning Pro] Weighted confluence scoring. The multi-indicator confluence score (Section 6) should weight signals by independence, not equal +1/-1. Suggested weights: SuperTrend direction: +1.0, ADX > 25: +0.5, RSI divergence: +1.0, price > SMA200: +0.5. These should be validated on historical data before being set in stone.
What to store per pair, per timeframe, every candle close:
atr_14: the ATR valueatr_14_expanding: boolean — is ATR higher than its 5-period moving average?atr_14_percentile_90d: where does current ATR rank vs last 90 days (0-100)atr_14_rate_of_change: percentage change from 5 candles agoAfter a SuperTrend flip, calculate and track the pullback entry level:
When a SuperTrend flip occurs, calculate and store:
stop_loss: SuperTrend line minus 5 pips (bullish) or plus 5 pips (bearish)entry_price: stop_loss plus 1.5x ATR (bullish) or minus 1.5x ATR (bearish)risk_distance: entry to stop in pips (= 1.5x ATR)target_2r through target_6r: entry +/- 2x through 6x risk_distanceOutcome tracking:
pullback_reached_entry: did price pull back to entry? (boolean + timestamp)entry_to_stop_hit, entry_to_2r_hit through entry_to_6r_hit: outcomes after fillno_fill: price never pulled back — flip ran awayWe track multiple sets of trend lines simultaneously. Each uses a different method for identifying swing points. Over time the database reveals which set is most predictive.
ZigZag Indicator (15/5): Depth 15, Deviation 5. Identifies significant swing highs and lows by filtering noise.
The Repainting Problem and Fix: ZigZag's most recent point is NOT reliable — it keeps moving. A point only locks in when a new swing in the OPPOSITE direction is confirmed after it. Rule: NEVER use unconfirmed ZigZag points.
[PANEL: Sonar Reasoning Pro] The deeper repainting problem: Even "confirmed" points can generate repainting trend lines. A confirmed B-to-C line repaints when a new D point shifts C's anchor. Fix: only lock a trend line when the swing AFTER its anchor point is confirmed. Report CLOSED/locked trend lines only — analysts use yesterday's frozen lines to trade today. Zero repainting, zero confusion.
[PANEL: Opus] Custom ZigZag implementation required. Do not use a library. Implement from scratch with explicit state management: maintain current_tentative_point (direction, price, timestamp), confirmed_points[] array. On each new candle, check if tentative point updates (price extended) or locks (opposite reversal exceeds threshold). Store tentative points with is_confirmed = false, flip to true when locked. Approximately 100-150 lines of careful code.
Higher-Timeframe (HTF) Validation:
| Chart Timeframe | Swing point must match the high or low of a closed... |
|---|---|
| 15min | 4H candle |
| 1H | Daily candle |
| 4H | Weekly candle |
| Daily | Monthly candle |
How trend lines are drawn: Take the last 3 qualifying swing points: A (oldest), B (middle), C (most recent). Draw:
As new qualifying points appear, A drops off, B becomes A, C becomes B, new point becomes C.
A point qualifies only if: (1) it is confirmed (not the most recent unconfirmed ZZ point), AND (2) it also matches the high or low of a completed higher-timeframe period.
Fields stored per pair, per timeframe, every candle close:
s1_point_a, s1_point_b, s1_point_c: price + timestamp of last 3 qualifying pointss1_line_ab_slope: slope of A-to-B trend line (pips per candle)s1_line_bc_slope: slope of B-to-C trend lines1_line_ab_current_value: where A-B line projects to at current candles1_line_bc_current_value: where B-C line projects to at current candles1_price_dist_to_line_ab: current price distance to line A-B (positive = above)s1_price_dist_to_line_bc: current price distance to line B-CUse only the highs and lows of completed higher-timeframe periods. No ZigZag. For 4H chart: take high and low of each closed week. Catches important levels ZigZag might skip.
Fields: Same structure as Set 1 but prefixed s2_
All confirmed ZigZag (15/5) swing points regardless of HTF match. Catches mid-period swings that don't coincide with a period boundary.
Fields: Same structure but prefixed s3_
RSI trend lines use the same concept but applied to RSI(14). RSI swing points are identified INDEPENDENTLY from price — they may be on different candles. That disagreement is divergence information.
How RSI swing points are selected: Within a closed higher-timeframe period, find the candle with the HIGHEST RSI reading and the candle with the LOWEST RSI reading. These are the RSI swing points for that period.
Same timeframe pairings as price:
| Chart Timeframe | RSI swing points from highs/lows of RSI within a closed... |
|---|---|
| 15min | 4H period |
| 1H | Daily period |
| 4H | Weekly period |
| Daily | Monthly period |
Fields stored per pair, per timeframe, every candle close:
rsi_tl_high_a, rsi_tl_high_b, rsi_tl_high_c: last 3 RSI swing highs (RSI value + timestamp + source HTF period)rsi_tl_low_a, rsi_tl_low_b, rsi_tl_low_c: last 3 RSI swing lowsrsi_tl_high_line_slope: slope of RSI high trend line (RSI units per candle)rsi_tl_low_line_slope: slope of RSI low trend linersi_tl_high_line_current: where RSI high line projects to at current candlersi_tl_low_line_current: where RSI low line projects torsi_dist_to_high_line: current RSI minus high line valuersi_dist_to_low_line: current RSI minus low line value| Set | Lines | What It Captures |
|---|---|---|
| Price Set 1 (ZZ + HTF) | 2 (A-B, B-C) | Strongest structural levels |
| Price Set 2 (HTF only) | 2 (A-B, B-C) | HTF structure ZZ might miss |
| Price Set 3 (ZZ only) | 2 (A-B, B-C) | Mid-period swings HTF misses |
| RSI (independent HTF) | 2 (highs, lows) | Momentum structure independent from price |
| Total | 8 trend lines |
This section tracks timing relationships between major events. Window: 10 candles (adjustable).
When a pairing is detected (ST flip + price TL break within 10 candles):
pair, timeframest_flip_timestamp, st_flip_directiontl_break_timestamp, tl_break_set (s1/s2/s3), tl_break_line (AB/BC), tl_break_directioncandles_apart: 0 = same candlewhich_first: "st_first", "tl_first", or "simultaneous"direction_agrees: do both signals point the same way?atr_at_event, higher_tf_agreespair, timeframersi_break_timestamp, rsi_break_line (high/low), rsi_break_directionprice_tl_break_timestamp, price_tl_break_set, price_tl_break_linecandles_apart, rsi_led (boolean — did RSI break BEFORE price?), direction_agreesSame structure, comparing RSI break to ST flip.
rsi_led_st: boolean — did RSI break before ST flipped?total_candle_span: candles from first to last eventsequence: e.g., "rsi_break → price_tl_break → st_flip"all_directions_agree: booleanBackfilled as subsequent candles arrive (hard cap: 50 candles, then freeze):
outcome_10_candles, outcome_20_candles, outcome_50_candles: price change in pips + ATR multiplesmax_favorable_excursion, max_adverse_excursionRegular Bullish (price lower low, RSI higher low): Selling pressure weakening. Reversal up signal.
Regular Bearish (price higher high, RSI lower high): Buying pressure weakening. Reversal down signal.
Hidden Bullish (price higher low, RSI lower low): Uptrend continuation. Pullback dip is a reset.
Hidden Bearish (price lower high, RSI higher high): Downtrend continuation. Rally is a reset.
pair, timeframe, divergence_type: "regular_bullish" / "regular_bearish" / "hidden_bullish" / "hidden_bearish"timestamp: when detected (candle where second swing point confirms)price_swing_1, price_swing_2: price level + timestamprsi_swing_1, rsi_swing_2: RSI value at each swingstrength: normalized divergence magnitude. [PANEL: Opus] Do NOT add raw slopes from different units. Normalize both price slope and RSI slope to their respective z-scores over the last 50 periods, then sum. Store the two z-scores separately as price_slope_zscore and rsi_slope_zscore plus their sum as divergence_strength.supertrend_direction: what ST says at divergence momentsupertrend_agrees: does divergence agree with ST?higher_tf_divergence: same divergence type present on next higher TF?at_support_resistance: is price near an S&R zone?Outcome tracking (hard cap 20 candles):
outcome_5_candles, outcome_10_candles, outcome_20_candlesreversal_occurred: for regular divergence (boolean + pip distance)continuation_occurred: for hidden divergence (boolean + pip distance)All calculated per pair, per timeframe, every candle close.
sma_20, sma_50, sma_100, sma_200price_vs_sma_20, price_vs_sma_50, price_vs_sma_100, price_vs_sma_200ma_alignment: "bullish" / "bearish" / "mixed"golden_cross: boolean — SMA50 crossed above SMA200 this candle?death_cross: boolean — SMA50 crossed below SMA200?rsi_14rsi_overbought: boolean (RSI > 70)rsi_oversold: boolean (RSI < 30)macd_line, macd_signal, macd_histogrammacd_cross_up, macd_cross_downmacd_above_zerobb_upper, bb_middle, bb_lowerbb_width_pct: (upper - lower) / middle * 100bb_squeeze: boolean — bb_width_pct in the bottom 10% of last 50 readings?price_position_in_bb: (price - lower) / (upper - lower)stoch_k, stoch_dstoch_overbought (K > 80), stoch_oversold (K < 20)adx_14: 0-100. Below 20 = no trend, 20-40 = developing, 40-60 = strong, 60+ = extremeplus_di, minus_didi_cross_up, di_cross_downadx_rising: boolean — trend strengthening?kc_upper: EMA(20) + 1.5 * ATR(14)kc_lower: EMA(20) - 1.5 * ATR(14)kc_middle: EMA(20)kc_width_pctprice_position_in_kckc_breakout_up, kc_breakout_down[PANEL: Gemini 3.1 Pro] Important: spot forex has no real volume, only tick counts. Calculating VWAP on OANDA tick data is mathematically imprecise and won't match institutional VWAP. Either rename these fields to TWAP (Time-Weighted Average Price) to reflect reality, or calculate using CME futures tick data. The label "VWAP" on tick-count data is misleading.
vwap (or twap), price_vs_vwapvwap_upper_1sd, vwap_lower_1sd, vwap_upper_2sd, vwap_lower_2sdvwap_24h: rolling 24-hour VWAP — institutional benchmarkvwap_48h: rolling 48-hour VWAPvwap_weekly: rolling 5-day VWAPOn-Balance Volume (OBV):
obv, obv_sma_20obv_divergence_bullish, obv_divergence_bearishChaikin Money Flow (CMF — 20-period):
cmf_20: -1 to +1. Positive = buying pressure. Negative = selling.cmf_cross_zero_up, cmf_cross_zero_downElder's Force Index (13-period EMA):
force_index_13: EMA(13) of (close - prev_close) * volumeforce_index_divergenceroc_10, roc_20From most recent major swing (identified by ZigZag on current timeframe):
fib_0, fib_236, fib_382, fib_500, fib_618, fib_786, fib_100nearest_fib_level, distance_to_nearest_fib[PANEL: Opus] Fibonacci direction clarification: Define explicitly which direction retracements measure. Take the last confirmed ZZ swing HIGH and the last confirmed ZZ swing LOW. If the HIGH is more recent, draw retracement from HIGH down to LOW (measuring pullback in a downtrend). If LOW is more recent, draw from LOW up to HIGH (measuring pullback in an uptrend). Store fib_direction field to make this explicit.
daily_pivot, daily_r1, daily_r2, daily_r3, daily_s1, daily_s2, daily_s3weekly_pivot, weekly_r1, etc.monthly_pivot, monthly_r1, etc.nearest_round_above, nearest_round_belowround_number_bracket: e.g., "1.0825-1.0850"distance_to_nearest_round[PANEL: Opus] Round number grid generates too many levels. Only generate round number candidates within 2 * ATR(14, Daily) of current price. Weight by significance: .0000 levels = 100% score, .0050 = 60%, .0025/.0075 = 30%. Weaker levels only survive clustering if another method confirms them.
eurusd_gbpusd_corr, usdjpy_dxy_corr, and 8-13 other meaningful pairscorrelation_break: boolean — any correlation deviated > 2 std deviations from 90-day norm?Lead-Lag Analysis:
lead_lag_matrix: who moves firstleading_pairs, lagging_pairs: ranked lists[PANEL: Opus] Lead-lag analysis is computationally expensive and relationships shift with regime. Collect the raw price data now; run lead-lag as a batch offline job after 3+ months of clean data. Do not build into the real-time collection engine.
Log specific multi-indicator setups as timestamped events.
Trigger: BB inside KC (bb_squeeze) AND RSI 40-60 AND ATR below 20-period average. Log: timestamp, pair, timeframe, squeeze duration, breakout direction when released.
Trigger: RSI regular divergence AND volume spike 2x+ 20-period average AND SMA20/SMA50 spread narrowing. Log: timestamp, pair, timeframe, exhaustion direction.
Trigger: ATR expanding AND BB squeeze just released (last 5 candles) AND volume spike. Log: timestamp, pair, timeframe, breakout direction, ATR at breakout.
Trigger: RSI < 30 or > 70 AND price > 3x ATR from SMA20 AND price near S&R zone. Log: timestamp, pair, timeframe, direction, distance from mean.
Per pair, per timeframe, every candle:
confluence_score: -5 to +5multi_tf_confluence_score: sum across all 7 timeframes. -35 to +35.[PANEL: Sonar Reasoning Pro] Equal weighting is naive — SuperTrend and RSI are semi-correlated (both momentum). Better: weight by signal independence. Suggested validated weights: SuperTrend +1.0, ADX > 25 +0.5, RSI divergence +1.0, price > SMA200 +0.5. Test these on historical data before locking in.
Pull every 15-20 minutes:
order_book_net_long_pctstop_cluster_long_distance: distance to largest cluster of long stop lossesstop_cluster_short_distance: distance to largest cluster of short stop lossesPull every 15-20 minutes:
retail_long_ratioretail_sentiment_zscore: 30-day z-score. Above 1.5 = contrarian signal.underwater_longs_pct, underwater_shorts_pct[PANEL: DeepSeek V3.1] Retail profit/loss ratios. Derive: when retail traders are heavily underwater (underwater_longs_pct or underwater_shorts_pct > 70%), brokers and institutions often pressure those positions aggressively. Track the combination as retail_trapped_side = "longs_trapped" / "shorts_trapped" / "balanced". This adds a Wyckoff-style squeeze detection from the OANDA book.
Pull every 1 minute:
bid_ask_spread: pipsspread_zscore: vs rolling 24-hour average[PANEL: Opus] During high-impact events (from calendar), increase spread polling to every 5 seconds for a 10-minute window around the event. Store event_max_spread, event_avg_spread, spread_time_to_normal. OANDA streaming API (v20) can provide real-time pricing for this.
Pull weekly (published Friday, data from Tuesday — always model the 3-day lag):
commercial_net_position, non_commercial_net_positionposition_change_week_over_weekextreme_positioning_percentile_52w[PANEL: Opus] COT is a weekly, lagged indicator (3-day minimum lag between data and publication). Use it only for weekly/monthly bias assessment, never for intraday or daily timing. Label every COT field with a cot_as_of_tuesday flag so downstream analysts always know the data is from Tuesday regardless of when it was ingested.
[PANEL: DeepSeek V3.1] CME FX futures open interest. Available as free delayed data (15-min delay) from CME Group. Shows whether new money is entering trends (open interest rising) vs position squaring (open interest falling on price moves). Distinguish from COT: this is more timely. Table: cme_oi with pair, timestamp, open_interest, open_interest_change_pct.
Pull daily:
swap_long, swap_short: overnight swap rate (pips)net_carry: swap_long - swap_shortcarry_direction: "long_pays" or "short_pays"carry_annualized_pctcarry_change_5dcarry_rank: this pair's carry rank among all 15 pairs[PANEL: GPT-4.1] Do NOT use Yahoo Finance for yields. FRED API is free, official, and reliable. It updates daily, which is sufficient for yield spread analysis — spreads don't change meaningfully intraday for forex purposes.
Pull daily via FRED API (series IDs: DGS2, DGS10, IRLTLT01DEM156N, IRLTLT01GBM156N, IRLTLT01JPM156N, etc.):
us_2y_yield, us_10y_yieldus_yield_curve_slope: 10Y minus 2Ygerman_10y_yield, uk_10y_yield, japan_10y_yield, australia_10y_yield, canada_10y_yield, nz_10y_yieldyield_spread_us_de, yield_spread_us_jp, yield_spread_us_uk, yield_spread_us_au, yield_spread_us_ca, yield_spread_us_nzyield_spread_2y_us_de, yield_spread_2y_us_jp (short-term rate differentials, often more responsive)yield_momentum_5d, spread_momentum_5d_us_deyield_vs_fx_divergence: boolean — yield spread and currency pair moving opposite directions?yield_fx_divergence_days: how many days has the divergence persisted?Pull daily:
sp500, nasdaq, nikkei_225, daxrisk_on_off_score: composite. Equities up + yields up + VIX down = risk-on. Opposite = risk-off.gold_price, wti_crude, copper_pricecopper_gold_ratiovix: daily pullvix_term_structure: VIX 9-day / VIX 30-day ratio[PANEL: Opus] DXY data source clarification — MUST-HAVE fix. DXY is NOT tradeable on OANDA. Yahoo Finance DXY (DX-Y.NYB) is delayed and only updates during US futures hours. The correct solution: calculate synthetic DXY from OANDA pairs you already have.
Formula: DXY = 50.14348112 × EURUSD^(-0.576) × USDJPY^(0.136) × GBPUSD^(-0.119) × USDCAD^(0.091) × USDSEK^(0.042) × USDCHF^(0.036)
Note: USD/SEK may not be on OANDA free tier. If unavailable, approximate with an EUR weight adjustment or add USD/SEK to the pair list. Store as dxy_synthetic so it's clearly labeled as calculated, not pulled from an exchange.
dxy_synthetic, dxy_momentum (5-day rate of change)[PANEL: Gemini 3.1 Pro] Central Bank Rate Probabilities. Yield spreads track levels, but FX markets price in specific PROBABILITIES of rate changes at upcoming meetings. A shift from 60% to 80% probability of a Fed cut drives USD instantly. Scrape CME FedWatch Tool or pull from Atlanta Fed / FRED PROBFIRSTHIKE or similar series.
fed_rate_prob_25bp_cut: probability of 25bp cut at next FOMC meetingecb_rate_prob_25bp_cut, boj_rate_prob_change
Source: CME FedWatch (scrape) or Atlanta Fed real-time estimates.[PANEL: Grok 4.1] Central bank speech and forward guidance sentiment. FX moves sharply on central bank rhetoric, not just official releases. Scrape central bank websites for speeches and minutes. Apply NLP sentiment scoring (free with Python NLTK or Hugging Face). Store per speech as cb_speech_events with bank, speaker, sentiment_score (-1 to +1), key_terms (JSON array), timestamp.
For each currency (USD, EUR, GBP, JPY, AUD, NZD, CAD, CHF):
currency_strength_roc: average rate of change vs all 7 other currenciescurrency_strength_zscore: z-score vs 30-day historyPull every 5 minutes from OANDA cross-pair data.
Why: Trading the strongest currency against the weakest gives sharper edges. If EUR/USD is breaking out, and EUR strength z-score is spiking but USD strength is flat, it's a Euro-driven move with more follow-through. If USD strength is crashing, EUR/USD rally reverses when USD pressure stops.
[PANEL: GPT-4.1] Session times must be calculated from UTC, not hardcoded in ET. Session start/end times shift with daylight saving transitions. Maintain a mapping table: UTC offset per region per week. Convert to ET for display only.
session_open_price, session_high, session_low, session_closesession_range_pips, session_range_vs_avg (ratio to 20-session average)session_direction, session_tick_volumeasian_range_pips: total Asian session rangelondon_initial_balance_high, london_initial_balance_low: first-hour London rangelondon_fix_behavior: price action around 4:00 PM London Fix (11:00 AM ET)ny_options_cut: price action around 10:00 AM ETasian_to_london_continuation: did London continue Asian direction or reverse?tokyo_holiday, london_holiday, us_holiday, eu_holiday, au_holidayholiday_affected_pairs: list of pairs likely affected[PANEL: GPT-4.1] True market holidays include partial days. Track Japanese Golden Week, US half-days, and other partial closures — not just full bank holidays. These create thin liquidity that breaks normal session statistics without a full holiday flag.
Source: Nager.Date API (free) + maintain a static calendar file updated yearly.
[PANEL: Sonar Reasoning Pro] Weekend gaps break indicator continuity. When the Asian session opens Sunday 5PM ET, 48-hour gaps cause ZigZag false swing points, stale moving averages, and discontinuous volume. Fix: detect gap size on session open (weekend_gap_pips), mark data as weekend_gap_period = true for the first 2 hours, and log gap_size_pct as a regime flag. Do not use pre-gap data for short-window calculations during this period.
hv_20_percentile_252d: current 20-day annualized vol ranked vs last 252 trading daysultra_low (<5th), low (5th-25th), normal (25th-75th), elevated (75th-90th), extreme (>90th)kaufman_er: absolute price change / sum of absolute per-bar changes. 0-1.fdi_30: 1.0-2.0. Below 1.5 = trending. Above 1.5 = mean-reverting. 1.5 = random walk.[PANEL: Opus] FDI is computationally expensive and adds marginal value over Kaufman ER (which captures the same trending-vs-choppy distinction). Defer to v2.
bb_inside_kc: boolean — BB completely inside KC?squeeze_duration: candles the squeeze has lastedsqueeze_release: boolean — squeeze just released?squeeze_release_direction[PANEL: Opus] Free vol spread signal from existing data. VIX (Section 8) is implied vol for equities. Compare to realized vol (HV percentile). When implied >> realized, market pricing in a move that hasn't happened. When realized >> implied, market is complacent. Store the spread and its z-score.
vix_vs_realized_spread: VIX level minus (hv_20_percentile_252d normalized to same scale)vix_vs_realized_zscore: z-score of the spread vs 90-day historyregime_adjusted_st_confidence: flip confidence by regimeregime_adjusted_breakout_threshold: 1.0 ATR (low vol) to 2.0 ATR (high vol)regime_adjusted_sr_zone_widthregime_trend_strategy_preference: "trend_following" / "mean_reversion" / "stay_out"[PANEL: Opus/Gemini/GPT-4.1/Grok — consensus] Do NOT scrape ForexFactory. It actively blocks scrapers using Cloudflare. Will break within days.
Use instead (in order of preference):
https://www.forexfactory.com/calendar?week=this (unofficial XML, more stable than HTML scraping but still unofficial)Build with primary + fallback: if primary source fails, try secondary and alert Telegram.
Pull daily, update every 6 hours:
next_high_impact_event: name of next high-impact release per major currencyminutes_to_next_high_impactevent_impact_level: 1-5consensus_vs_prior: expected minus previousactual_value: the released value (filled after release)surprise_magnitude: actual minus consensus (filled after release)[PANEL: Grok 4 Fast / Sonar Reasoning Pro] Actual release values are critical. The economic calendar must capture actual vs forecast data, not just event timing. Surprise magnitude is the primary driver of immediate market reaction. Source: FRED API for US releases (official, free). Store as economic_releases table with actual, consensus, previous, surprise, surprise_normalized (surprise / std deviation of historical surprises for this series).
nfp_friday, fomc_week, ecb_week, boj_week[PANEL: Opus] Rate decisions are the most impactful forex event. Track not just the meeting week but: cb_rate_decision table with bank, decision_timestamp, outcome ("hike"/"cut"/"hold"), amount_bp, matched_consensus (boolean), surprise_direction, surprise_magnitude_bp. FRED API provides actual rate levels for free (DFEDTAR for Fed funds, etc.).
event_nameactual_vs_consensus: "beat"/"miss"/"inline"surprise_magnitude5min_move, 30min_move, 1h_move, 4h_move (pips)retrace_pct: how much of initial 5min move was retraced within 4h[PANEL: GPT-4.1] Prediction markets provide real-money probability on macro outcomes. Kalshi and Polymarket carry contracts on Fed rate decisions, CPI beats/misses, NFP range outcomes. These are binary/ladder markets with real-money pricing — more predictive than survey consensus. Pull available markets around scheduled economic releases. See Section 20 for full prediction market spec.
day_of_week_avg_range, day_of_week_direction_bias per pairdays_until_month_end, month_end_window (last 3 trading days), quarter_end_windowholiday_flag, holiday_whichmonth_of_yearfiscal_year_end_japan: March — Japanese repatriation flows impact JPYdaylight_savings_transition: booleantokyo_fix_proximity: minutes until/since 9:55 AM Tokyo (~7:55 PM ET)london_fix_proximity: minutes until/since 4:00 PM London (11:00 AM ET)ny_options_cut_proximity: minutes until/since 10:00 AM ET[PANEL: Sonar Reasoning Pro] Forward bias protection. Forward returns must only be calculated on data older than 30 days. Any candle from the last 30 days does NOT get a forward return label yet — future data isn't in the database. This prevents training-test leakage where models learn to "predict" data they can already see. Add fwd_label_available boolean = false for candles within last 30 days.
fwd_return_1h, fwd_return_4h, fwd_return_24h (pips)fwd_max_favorable_excursion_24h (MFE)fwd_max_adverse_excursion_24h (MAE)fwd_label_available: booleanst_flip_win_rate_30dst_pullback_fill_rate_30ddivergence_win_rate_30dtl_break_continuation_rate_30dtrend_efficiency_ratio: net change / sum of individual rangesmean_reversion_half_life: candles to return to SMA20 after deviationeurusd_sp500_corr_20d, usdjpy_vix_corr_20d, and 8-13 other key cross-market correlationscorrelation_regime_break: boolean — any key correlation > 2 std deviations from 90-day norm?[PANEL: Opus/Gemini/GPT-4.1/Grok — CRITICAL] SQLite write throughput will bottleneck without WAL mode.
At 15 pairs × 7 timeframes, peak coincides with 4H close: indicators, S&R updates, ZigZag, trend lines, and event detection all fire simultaneously. SQLite uses a single-writer lock — concurrent writes queue up.
Required at startup:
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000; -- 64MB cache
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 268435456; -- 256MB memory map
Required in all write loops: Use single BEGIN TRANSACTION / COMMIT per collection cycle. Never write row-by-row.
[PANEL: Gemini 3.1 Pro] Offload heavy math to Node.js Worker Threads if using Node.js. S&R clustering and ZigZag recalculation on 4H/Daily closes can block the event loop for seconds, delaying next candle ingestion and causing database is locked errors. Worker threads share the write queue without blocking.
[PANEL: Grok 4.1] For future scale beyond 1-2 years of data: plan a migration path to PostgreSQL + TimescaleDB extension or InfluxDB. Keep the SQLite schema normalized now so migration is not a rewrite.
Main time series table:
CREATE TABLE forex_candles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL, -- UTC ISO8601
pair TEXT NOT NULL, -- e.g., 'EUR_USD'
timeframe TEXT NOT NULL, -- e.g., '4H', 'D', 'W'
open REAL NOT NULL,
high REAL NOT NULL,
low REAL NOT NULL,
close REAL NOT NULL,
tick_volume INTEGER,
is_imputed INTEGER DEFAULT 0, -- 1 if forward-filled
-- SuperTrend
supertrend_direction TEXT,
supertrend_value REAL,
supertrend_flipped INTEGER,
candles_since_last_flip INTEGER,
price_distance_to_st REAL,
-- ATR
atr_14 REAL,
atr_14_expanding INTEGER,
atr_14_percentile_90d REAL,
atr_14_rate_of_change REAL,
-- Moving averages
sma_20 REAL, sma_50 REAL, sma_100 REAL, sma_200 REAL,
price_vs_sma_20 REAL, price_vs_sma_50 REAL, price_vs_sma_100 REAL, price_vs_sma_200 REAL,
ma_alignment TEXT,
golden_cross INTEGER, death_cross INTEGER,
-- RSI
rsi_14 REAL, rsi_overbought INTEGER, rsi_oversold INTEGER,
-- MACD
macd_line REAL, macd_signal REAL, macd_histogram REAL,
macd_cross_up INTEGER, macd_cross_down INTEGER, macd_above_zero INTEGER,
-- Bollinger Bands
bb_upper REAL, bb_middle REAL, bb_lower REAL,
bb_width_pct REAL, bb_squeeze INTEGER, price_position_in_bb REAL,
-- Stochastic
stoch_k REAL, stoch_d REAL, stoch_overbought INTEGER, stoch_oversold INTEGER,
-- ADX/DMI
adx_14 REAL, plus_di REAL, minus_di REAL,
di_cross_up INTEGER, di_cross_down INTEGER, adx_rising INTEGER,
-- Keltner Channels
kc_upper REAL, kc_lower REAL, kc_middle REAL,
kc_width_pct REAL, price_position_in_kc REAL,
kc_breakout_up INTEGER, kc_breakout_down INTEGER,
-- VWAP / TWAP
vwap REAL, price_vs_vwap REAL,
vwap_upper_1sd REAL, vwap_lower_1sd REAL,
vwap_upper_2sd REAL, vwap_lower_2sd REAL,
vwap_24h REAL, vwap_48h REAL, vwap_weekly REAL,
-- Volume indicators
obv REAL, obv_sma_20 REAL,
obv_divergence_bullish INTEGER, obv_divergence_bearish INTEGER,
cmf_20 REAL, cmf_cross_zero_up INTEGER, cmf_cross_zero_down INTEGER,
force_index_13 REAL, force_index_divergence INTEGER,
-- Momentum
roc_10 REAL, roc_20 REAL,
-- Fibonacci
fib_0 REAL, fib_236 REAL, fib_382 REAL, fib_500 REAL,
fib_618 REAL, fib_786 REAL, fib_100 REAL,
fib_direction TEXT,
nearest_fib_level TEXT, distance_to_nearest_fib REAL,
-- Pivots
daily_pivot REAL, daily_r1 REAL, daily_r2 REAL, daily_r3 REAL,
daily_s1 REAL, daily_s2 REAL, daily_s3 REAL,
weekly_pivot REAL, weekly_r1 REAL, weekly_r2 REAL, weekly_r3 REAL,
weekly_s1 REAL, weekly_s2 REAL, weekly_s3 REAL,
monthly_pivot REAL, monthly_r1 REAL, monthly_r2 REAL, monthly_r3 REAL,
monthly_s1 REAL, monthly_s2 REAL, monthly_s3 REAL,
-- Round numbers
nearest_round_above REAL, nearest_round_below REAL,
round_number_bracket TEXT, distance_to_nearest_round REAL,
-- Correlations
eurusd_gbpusd_corr REAL, usdjpy_dxy_corr REAL,
correlation_break INTEGER,
-- Trend line Sets (s1, s2, s3)
s1_line_ab_current_value REAL, s1_line_bc_current_value REAL,
s1_line_ab_slope REAL, s1_line_bc_slope REAL,
s1_price_dist_to_line_ab REAL, s1_price_dist_to_line_bc REAL,
s2_line_ab_current_value REAL, s2_line_bc_current_value REAL,
s2_line_ab_slope REAL, s2_line_bc_slope REAL,
s2_price_dist_to_line_ab REAL, s2_price_dist_to_line_bc REAL,
s3_line_ab_current_value REAL, s3_line_bc_current_value REAL,
s3_line_ab_slope REAL, s3_line_bc_slope REAL,
s3_price_dist_to_line_ab REAL, s3_price_dist_to_line_bc REAL,
-- RSI trend lines
rsi_tl_high_line_slope REAL, rsi_tl_low_line_slope REAL,
rsi_tl_high_line_current REAL, rsi_tl_low_line_current REAL,
rsi_dist_to_high_line REAL, rsi_dist_to_low_line REAL,
-- Volatility regime
hv_20_percentile_252d REAL,
kaufman_er REAL, fdi_30 REAL,
bb_inside_kc INTEGER, squeeze_duration INTEGER,
squeeze_release INTEGER, squeeze_release_direction TEXT,
regime_trend_strategy_preference TEXT,
vix_vs_realized_spread REAL, vix_vs_realized_zscore REAL,
-- Confluence score
confluence_score REAL, multi_tf_confluence_score REAL,
-- Forward returns (labels for ML)
fwd_return_1h REAL, fwd_return_4h REAL, fwd_return_24h REAL,
fwd_mfe_24h REAL, fwd_mae_24h REAL, fwd_label_available INTEGER DEFAULT 0,
UNIQUE(timestamp, pair, timeframe)
);
CREATE INDEX idx_candles_pair_tf_ts ON forex_candles (pair, timeframe, timestamp);
CREATE INDEX idx_candles_ts ON forex_candles (timestamp);
Event tables:
CREATE TABLE supertrend_flips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
flip_timestamp TEXT NOT NULL,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
flip_direction TEXT NOT NULL,
flip_price REAL,
st_line_at_flip REAL,
atr_at_flip REAL,
atr_trend_at_flip TEXT,
higher_tf_agrees INTEGER,
multi_tf_alignment_count INTEGER,
stop_loss REAL,
entry_price REAL,
risk_distance REAL,
target_2r REAL, target_3r REAL, target_4r REAL, target_5r REAL, target_6r REAL,
-- Outcome tracking (backfilled, hard cap 50 candles)
max_favorable_excursion_pips REAL,
max_favorable_excursion_atr REAL,
max_adverse_excursion_pips REAL,
hit_2r INTEGER, hit_3r INTEGER, hit_4r INTEGER, hit_5r INTEGER, hit_6r INTEGER,
hit_2r_timestamp TEXT, hit_3r_timestamp TEXT,
time_to_2r_candles INTEGER, time_to_3r_candles INTEGER,
outcome_window_expired INTEGER DEFAULT 0,
outcome_candles_checked INTEGER DEFAULT 0
);
CREATE TABLE pullback_setups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
flip_id INTEGER REFERENCES supertrend_flips(id),
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
setup_timestamp TEXT NOT NULL,
entry_price REAL,
stop_loss REAL,
risk_distance REAL,
pullback_reached_entry INTEGER,
pullback_reached_timestamp TEXT,
entry_to_stop_hit INTEGER,
entry_to_2r_hit INTEGER, entry_to_3r_hit INTEGER,
entry_to_4r_hit INTEGER, entry_to_5r_hit INTEGER, entry_to_6r_hit INTEGER,
no_fill INTEGER DEFAULT 0,
outcome_window_expired INTEGER DEFAULT 0
);
CREATE TABLE zigzag_points (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
timestamp TEXT NOT NULL,
price REAL NOT NULL,
type TEXT NOT NULL, -- 'high' or 'low'
is_confirmed INTEGER DEFAULT 0,
htf_validated INTEGER DEFAULT 0,
htf_period TEXT -- which HTF period validated this
);
CREATE TABLE trendline_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
timestamp TEXT NOT NULL,
tl_set TEXT NOT NULL, -- 's1', 's2', 's3', 'rsi'
tl_line TEXT NOT NULL, -- 'AB', 'BC', 'high', 'low'
event_type TEXT NOT NULL, -- 'touch', 'break', 'retest'
direction TEXT,
magnitude_pips REAL,
volume_at_event INTEGER,
held INTEGER -- for retests: did it hold?
);
CREATE TABLE st_tl_correlations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
correlation_type TEXT NOT NULL, -- 'st_tl', 'rsi_tl', 'rsi_st', 'triple'
st_flip_timestamp TEXT,
st_flip_direction TEXT,
tl_break_timestamp TEXT,
tl_break_set TEXT,
tl_break_line TEXT,
rsi_break_timestamp TEXT,
rsi_break_line TEXT,
candles_apart INTEGER,
which_first TEXT,
direction_agrees INTEGER,
rsi_led INTEGER,
sequence TEXT, -- for triple: order of three events
all_directions_agree INTEGER,
atr_at_event REAL,
higher_tf_agrees INTEGER,
outcome_10_candles REAL,
outcome_20_candles REAL,
outcome_50_candles REAL,
max_favorable_excursion REAL,
max_adverse_excursion REAL,
outcome_window_expired INTEGER DEFAULT 0
);
CREATE TABLE divergence_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
divergence_type TEXT NOT NULL,
timestamp TEXT NOT NULL,
price_swing_1_price REAL, price_swing_1_timestamp TEXT,
price_swing_2_price REAL, price_swing_2_timestamp TEXT,
rsi_swing_1 REAL, rsi_swing_2 REAL,
price_slope_zscore REAL, rsi_slope_zscore REAL, divergence_strength REAL,
supertrend_direction TEXT,
supertrend_agrees INTEGER,
higher_tf_divergence INTEGER,
at_support_resistance INTEGER,
outcome_5_candles REAL, outcome_10_candles REAL, outcome_20_candles REAL,
reversal_occurred INTEGER, reversal_pips REAL,
continuation_occurred INTEGER, continuation_pips REAL,
outcome_window_expired INTEGER DEFAULT 0
);
CREATE TABLE confluence_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL,
event_type TEXT NOT NULL, -- 'momentum_squeeze', 'trend_exhaustion', 'breakout_setup', 'mean_reversion_extreme'
timestamp TEXT NOT NULL,
direction TEXT,
squeeze_duration INTEGER,
breakout_direction TEXT,
atr_at_event REAL
);
CREATE TABLE news_releases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_timestamp TEXT NOT NULL,
pair TEXT,
currency TEXT,
event_name TEXT NOT NULL,
impact_level INTEGER,
actual_value REAL,
consensus_value REAL,
previous_value REAL,
surprise_magnitude REAL,
actual_vs_consensus TEXT, -- 'beat', 'miss', 'inline'
move_5min_pips REAL, move_30min_pips REAL,
move_1h_pips REAL, move_4h_pips REAL,
retrace_pct REAL
);
CREATE TABLE economic_calendar (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_timestamp TEXT NOT NULL,
currency TEXT,
event_name TEXT NOT NULL,
impact_level INTEGER, -- 1-5
consensus_value REAL,
previous_value REAL,
source TEXT -- which source provided this
);
Reference / slow tables:
CREATE TABLE cot_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
as_of_tuesday TEXT NOT NULL, -- always store which Tuesday this covers
published_friday TEXT,
commercial_net_position INTEGER,
non_commercial_net_position INTEGER,
position_change_week_over_week INTEGER,
extreme_positioning_percentile_52w REAL
);
CREATE TABLE cme_open_interest (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timestamp TEXT NOT NULL,
open_interest INTEGER,
open_interest_change_pct REAL
);
CREATE TABLE session_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
session TEXT NOT NULL, -- 'asian', 'london', 'ny', 'overlap'
pair TEXT NOT NULL,
open_price REAL, close_price REAL, high_price REAL, low_price REAL,
range_pips REAL, range_vs_avg REAL,
direction TEXT, tick_volume INTEGER,
is_holiday INTEGER DEFAULT 0
);
CREATE TABLE economic_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
data_type TEXT NOT NULL, -- 'yield', 'equity', 'commodity', 'vix', 'dxy'
source TEXT NOT NULL, -- 'FRED', 'Yahoo', 'Twelve Data'
series_id TEXT,
value REAL,
pair_context TEXT -- which forex pair this most influences
);
CREATE TABLE currency_strength (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
currency TEXT NOT NULL,
strength_roc REAL,
strength_zscore REAL
);
CREATE TABLE carry_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL,
pair TEXT NOT NULL,
swap_long REAL, swap_short REAL,
net_carry REAL, carry_direction TEXT,
carry_annualized_pct REAL, carry_change_5d REAL, carry_rank INTEGER
);
CREATE TABLE cb_speech_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
bank TEXT NOT NULL, -- 'FED', 'ECB', 'BOJ', 'BOE', etc.
speaker TEXT,
sentiment_score REAL, -- -1.0 to +1.0
key_terms TEXT, -- JSON array
source_url TEXT
);
CREATE TABLE cb_rate_decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
decision_timestamp TEXT NOT NULL,
bank TEXT NOT NULL,
outcome TEXT NOT NULL, -- 'hike', 'cut', 'hold'
amount_bp REAL,
matched_consensus INTEGER,
surprise_direction TEXT,
surprise_magnitude_bp REAL
);
CREATE TABLE system_health (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
check_type TEXT NOT NULL,
pair TEXT,
timeframe TEXT,
last_candle_timestamp TEXT,
candle_age_minutes REAL,
db_size_mb REAL,
api_response_time_ms REAL,
status TEXT, -- 'ok', 'stale', 'gap', 'error'
notes TEXT
);
| Data Type | Frequency |
|---|---|
| Price candles + all calculated indicators | Every candle close |
| OANDA order book + position book | Every 15-20 minutes |
| OANDA spread | Every 1 minute (every 5 seconds around high-impact events) |
| Cross-market: yields (FRED) | Daily |
| Cross-market: equities, VIX, commodities | Daily (Yahoo Finance / Twelve Data) |
| Currency strength | Every 5 minutes |
| Session metrics | Continuously, summarized at session close |
| Economic calendar | Pull daily, update every 6 hours |
| COT data | Weekly (Friday publish, Tuesday data) |
| CME open interest | Hourly (free delayed) |
| Derived metrics (win rates, correlations) | Daily recalculation |
| Forward return labels | Continuous backfill, label only for candles > 30 days old |
| Outcome backfill (events) | Every candle, hard stop at 50 candles |
[PANEL: Opus] Design request batching from day one. OANDA v20 REST API limits: ~100 requests/second burst. Sustained polling of 105 pair-timeframe combinations (15 pairs × 7 TF) every 5 minutes is feasible but requires careful batching. OANDA returns max ~5000 candles per request — for 5min timeframe that's only ~17 days of history. Backfill strategy required at startup.
Required implementation:
is_imputed = 1 flag.outcome_window_expired = 1.Tracks statistics from closed/completed periods only — never live data. Non-repainting.
Calculated per pair per timeframe (Daily, Weekly, Monthly), updated at period close.
last_high, last_low, prev_high, prev_lowavg_high, avg_lowhigh_change_pct, low_change_pctprev_high_change_pct, prev_low_change_pctlast_range_pct, prev_range_pcthigh_struct: "HH" or "LH"low_struct: "HL" or "LL"last_close, prev_close, last_open, prev_openclose_change_pct, open_change_pctclose_position_pct (Last and Prev): (Close - Low) / (High - Low) * 100close_position_shift: Last close_position_pct - Prev close_position_pctclose_bias: "Upper" (>67%) / "Middle" (33-67%) / "Lower" (<33%)last_body_pct, prev_body_pct: |Close - Open| / (High - Low) * 100body_expansion_pctbody_direction (Last, Prev): "Bull" / "Bear" / "Doji" (body < 0.1% of range)body_flip: boolean — did direction change?upper_wick_pct (Last, Prev): (High - Max(Open, Close)) / (High - Low) * 100lower_wick_pct (Last, Prev): (Min(Open, Close) - Low) / (High - Low) * 100wick_ratio: Upper Wick% / Lower Wick%shadow_imbalance: Upper Wick% - Lower Wick%range_expansion_ratio: Last Range / Prev Rangerange_change_pctprice_overlap_pct: Max(0, Min(Last High, Prev High) - Max(Last Low, Prev Low)) / Min(Last Range, Prev Range) * 100midpoint_shift_pct: ((Last H+L)/2 - (Prev H+L)/2) / ((Prev H+L)/2) * 100trend_structure_type: "UpTrend" (HH+HL) / "DownTrend" (LH+LL) / "Choppy_Expanding" (HH+LL) / "Compression" (LH+HL)is_inside_bar, is_outside_barclose_vs_prev_range: "Breakout_Up" (Last Close > Prev High) / "Breakout_Down" / "Inside_Close"failed_new_high: Last High > Prev High BUT Last Close < Prev Highfailed_new_low: Last Low < Prev Low BUT Last Close > Prev Lowgap_pct: (Last Open - Prev Close) / Prev Close * 100gap_type: "Up" / "Down" / "None"gap_filled, gap_persistencenet_travel_pct (Last, Prev): (Close - Open) / Open * 100velocity_ratio: Last Net% / Prev Net%momentum_label: "Expanding" / "Contracting" / "Reversing"high_velocity: Last High - Prev Closelow_velocity: Prev Close - Last Lowvelocity_bias: "Bullish" / "Bearish" / "Neutral"last_volume, prev_volume, volume_change_pctvolume_struct: "HV" or "LV"volume_range_efficiency: Volume / Rangeeffort_vs_result: "Anomaly_Absorption" (volume +50% AND range expansion < 0.5) / "Anomaly_Breakout" (volume +50% AND range expansion > 2.0) / "Normal"body_volume_confirmation: "Confirmed" / "Unconfirmed"dw_high_struct_agree, dw_low_struct_agreewm_high_struct_agree, wm_low_struct_agreedm_high_struct_agree, dm_low_struct_agreefull_alignment: all 3 timeframes same High + Low structtimeframe_divergence_count: 0-6cross_tf_range_ratio_dw, cross_tf_range_ratio_wmmicro_close_in_macro_pct: (Last Daily Close - Last Weekly Low) / Last Weekly Range * 100cross_tf_close_position_align: "Aligned" / "Divergent"structure_score: -5 to +5 (weighted HH/HL/LH/LL + close position + body direction + wicks)structure_strength: "Strong" (>=3) / "Moderate" (1-2) / "Weak" (-1 to 0) / "Failing" (<=-2)range_efficiency: |Close - Open| / Rangefvg_up: Last Low > Prev High (Fair Value Gap, bullish)fvg_down: Last High < Prev Low (FVG, bearish)volume_price_divergence: price moved but volume declinedemr: 2 * (Last Close - Prev Close) / (Last Range + Prev Range) — Efficient Momentum Rationrr_daily_weekly: Daily Range% / (Weekly Range% / 5) — Normalized Range Rationrr_weekly_monthly: Weekly Range% / (Monthly Range% / 4)true_range_last, true_range_prev, true_range_change_pctrange_atr_ratio: Last Range / ATR(14)candle_type: "Marubozu" (body >= 80%) / "Pinbar" (wick >= 66%) / "Doji" (body < 10%) / "Normal"breakout_retest: close_vs_prev_range is Breakout_Up/Down AND price retested from other sidereversal_up, reversal_down: two-bar reversal patternsmtf_clv_agree_dw, mtf_clv_agree_wmCREATE TABLE closed_period_structure (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pair TEXT NOT NULL,
timeframe TEXT NOT NULL, -- 'D', 'W', 'M'
period_close_timestamp TEXT NOT NULL,
-- 16.1 Base Structure
last_high REAL, last_low REAL, prev_high REAL, prev_low REAL,
avg_high REAL, avg_low REAL,
high_change_pct REAL, low_change_pct REAL,
last_range_pct REAL, prev_range_pct REAL,
high_struct TEXT, low_struct TEXT,
-- 16.2 Close/Open
last_close REAL, prev_close REAL, last_open REAL, prev_open REAL,
close_change_pct REAL, open_change_pct REAL,
close_position_pct_last REAL, close_position_pct_prev REAL,
close_position_shift REAL, close_bias TEXT,
-- 16.3 Body
last_body_pct REAL, prev_body_pct REAL, body_expansion_pct REAL,
body_direction_last TEXT, body_direction_prev TEXT, body_flip INTEGER,
upper_wick_pct_last REAL, lower_wick_pct_last REAL,
upper_wick_pct_prev REAL, lower_wick_pct_prev REAL,
wick_ratio REAL, shadow_imbalance REAL,
-- 16.4 Range/Vol
range_expansion_ratio REAL, range_change_pct REAL,
price_overlap_pct REAL, midpoint_shift_pct REAL,
-- 16.5 Structure
trend_structure_type TEXT, is_inside_bar INTEGER, is_outside_bar INTEGER,
close_vs_prev_range TEXT,
-- 16.6 Failures
failed_new_high INTEGER, failed_new_low INTEGER,
-- 16.7 Gaps
gap_pct REAL, gap_type TEXT, gap_filled INTEGER, gap_persistence INTEGER,
-- 16.8 Momentum
net_travel_pct_last REAL, net_travel_pct_prev REAL,
velocity_ratio REAL, momentum_label TEXT,
high_velocity REAL, low_velocity REAL, velocity_bias TEXT,
-- 16.9 Volume
last_volume REAL, prev_volume REAL, volume_change_pct REAL, volume_struct TEXT,
volume_range_efficiency REAL, effort_vs_result TEXT, body_volume_confirmation TEXT,
-- 16.10 MTF Alignment
dw_high_struct_agree INTEGER, dw_low_struct_agree INTEGER,
wm_high_struct_agree INTEGER, wm_low_struct_agree INTEGER,
dm_high_struct_agree INTEGER, dm_low_struct_agree INTEGER,
full_alignment INTEGER, timeframe_divergence_count INTEGER,
cross_tf_range_ratio_dw REAL, cross_tf_range_ratio_wm REAL,
micro_close_in_macro_pct REAL, cross_tf_close_position_align TEXT,
-- 16.11 Composite
structure_score REAL, structure_strength TEXT, range_efficiency REAL,
-- 16.12 Advanced
fvg_up INTEGER, fvg_down INTEGER, volume_price_divergence INTEGER,
emr REAL, nrr_daily_weekly REAL, nrr_weekly_monthly REAL,
true_range_last REAL, true_range_prev REAL, true_range_change_pct REAL,
range_atr_ratio REAL, candle_type TEXT,
breakout_retest INTEGER, reversal_up INTEGER, reversal_down INTEGER,
mtf_clv_agree_dw INTEGER, mtf_clv_agree_wm INTEGER,
-- Cross-references
sr_zone_id_at_high TEXT, sr_zone_id_at_low TEXT,
structure_high_at_resistance INTEGER, structure_low_at_support INTEGER,
structure_break_through_sr INTEGER, close_near_sr_zone INTEGER,
UNIQUE(pair, timeframe, period_close_timestamp)
);
CREATE INDEX idx_structure_pair_tf ON closed_period_structure (pair, timeframe);
Updated at: Daily = 5PM ET, Weekly = Friday 5PM ET, Monthly = last trading day 5PM ET. Keep full history. Never delete. Builds the long-term pattern dataset.
The most important cross-referencing system. Every S&R zone is always TWO prices (a range, never a single point). Both boundaries are rounded OUTWARD to the nearest .0025 increment.
This engine is asset-class agnostic. Only the round-number grid and session times change per market.
Method 1: Multi-Touch Zones Scan price history for levels where price bounced 2+ times. A "touch" = price within 0.3 * ATR(14), then reversed at least 1.0 * ATR(14). More touches = stronger.
Method 2: Retest Zones (Polarity Flip) Levels that acted as resistance AND later as support (or vice versa). Zone where resistance and support overlap within 0.5 * ATR(14).
Method 3: ZigZag Swing Points Confirmed ZZ (15/5) swings, HTF-validated (Set 1). Each swing point = zone centered on swing price, width = 0.25 * ATR(14) per side.
Method 4: Weekly/Daily/Monthly Highs and Lows Each completed period's high and low = candidate S&R level.
Method 5: Volume Profile / High Volume Nodes Per completed week/month: divide price range into bins (10 pips or 0.25 * ATR). Accumulate tick volume per bin. HVNs = top 10% of volume. POC = highest volume bin. VAH/VAL = range containing 70% of volume.
[PANEL: Opus] Volume profile is an approximation. OANDA provides tick volume (count of price updates) not actual traded volume. Label all fields as estimated_volume_profile. Distribute candle volume as: 1/3 to close price bin, 2/3 evenly across O-H-L range. Imperfect but still useful for identifying high-activity zones.
Method 6: Order Blocks Last opposing candle before a strong impulse (>2 ATR in one direction). Bullish OB: last red candle before big up move (zone = open-to-low). Bearish OB: last green candle before big down move (zone = open-to-high). Confirm with FVG (imbalance after impulse).
Method 7: Liquidity Sweeps / Stop Hunts Price briefly exceeds a known swing high/low (1-3 candles max) then immediately reverses. Sweep zone = from original swing to extreme of sweep candle. After sweep, zone flips: swept high becomes potential support.
Method 8: Fibonacci Levels Fib levels from significant swings (>5 ATR minimum swing length) become candidate zones. Also: Fibonacci extensions (1.000, 1.272, 1.618, 2.618) for targets.
Method 9: Round Numbers (.0025 Grid) .0000 levels strongest, .0050 next, .0025/.0075 weakest. JPY: .00, .50, .25/.75.
Method 10: Dynamic-to-Static Conversion If price bounces off SMA 20/50/100/200 or VWAP 3+ times within 20 candles, graduate that price area to a static zone. Zone = range between highest and lowest bounce points near that MA.
| Factor | Calculation | Max Points |
|---|---|---|
| Touch Count | 10 per touch, cap at 30. After 5+ touches, score starts declining. | 30 |
| Bounce Quality | Average % move after each touch. 5% avg = max. | 20 |
| Timeframe Weight | 4H +5, Daily +5, Weekly +7, Monthly +3 (stacks across TFs) | 20 |
| Volume Profile | Zone at HVN/POC percentile | 15 |
| Recency | 15 - (days_since_last_touch / 10) | 15 |
| Method Confluence | +5 per additional method confirming same zone | uncapped |
| Retest Bonus | +10 if zone confirmed as polarity flip | 10 |
| Sweep Bonus | +8 if zone from confirmed liquidity sweep | 8 |
| Structure Alignment | +6 if D/W/M structure point (Section 16) coincides | 6 |
| Dynamic Confluence | +5 if zone aligns with respected MA/VWAP | 5 |
| Fibonacci Confluence | +5 if zone aligns with Fib level (0.5/0.618 = full, others = 3) | 5 |
| Round Number | +4 big figure (.0000), +2 half (.0050) | 4 |
Zone labels: 80+ = "Fortress", 60-79 = "Strong", 40-59 = "Moderate", 20-39 = "Weak", <20 = "Expired"
[PANEL: Sonar Reasoning Pro] Exact scoring algorithm:
MERGE zones within 0.5 * ATR distance
SCORE = (touch_count * 10, capped at 30)
+ (avg_bounce_pct * 4, capped at 20)
+ (timeframe_weight_total, capped at 20)
+ (volume_node_percentile * 0.15)
+ (15 - days_since_last_touch/10, min 0)
+ (additional_methods * 5)
+ bonuses
CAP score display at 100, but allow computed to exceed for ranking
After 5 touches: apply degradation factor: score *= 0.95^(touches - 5)
Proximity threshold: 0.5 * ATR(14) of higher timeframe.
Merge logic:
sources JSON array: ["multi_touch", "volume_profile", "fibonacci"]decay_factor = 0.995 ^ days_since_creationtest_decay = 0.98 ^ (days - 30)untested_penalty = 0.9Zone breaks:
is_flipped = true), drop to 25% strength, rebuild if respected from new sideExpiration: Archived when score < 20 AND untouched 90+ days (4H/Daily) or 180+ days (W/M). Archived zones stay in database forever but excluded from active analysis.
Multipliers: Monthly 4.0x, Weekly 2.5x, Daily 1.5x, 4H 1.0x.
At every period close, generate:
structure_high_at_resistance: last period's HH or LH stopped at active resistance?structure_low_at_support: HL or LL stopped at active support?structure_break_through_sr: HH broke above resistance OR LL broke below support?sr_zone_id_at_high, sr_zone_id_at_lowclose_near_sr_zone: period close within 0.3 * ATR of any zone?sr_zone_plus_news: zone within range AND high-impact event within 24h?sr_zone_plus_cot_shift: price at zone AND COT shows institutional shift same direction?sr_zone_plus_sentiment_divergence: price at support BUT sentiment overwhelmingly bearish?sr_zone_plus_session_extreme: zone formed at/near London/NY/Asia session high/low?sr_zone_plus_volume_spike: zone interaction coincided with unusual volume?dynamic_confluence = true, +5 scorez = (bounce_rate - 0.5) / sqrt(0.25 / n)CREATE TABLE sr_zones (
zone_id TEXT PRIMARY KEY,
pair TEXT NOT NULL,
zone_low REAL NOT NULL,
zone_high REAL NOT NULL,
round_bracket TEXT,
strength_score REAL,
raw_score REAL,
status TEXT DEFAULT 'Active', -- 'Active', 'Broken', 'Flipped', 'Expired', 'Noise'
is_flipped INTEGER DEFAULT 0,
detection_methods TEXT, -- JSON array
method_count INTEGER,
touch_count INTEGER DEFAULT 0,
bounce_count INTEGER DEFAULT 0,
break_count INTEGER DEFAULT 0,
bounce_rate REAL,
z_score REAL,
avg_bounce_pct REAL,
avg_penetration_pct REAL,
source_timeframes TEXT, -- JSON array
highest_tf TEXT,
tf_weight REAL,
created_at TEXT,
last_touch_at TEXT,
last_decay_at TEXT,
structure_interactions INTEGER DEFAULT 0,
fundamental_flags TEXT, -- JSON
dynamic_confluence INTEGER DEFAULT 0,
notes TEXT
);
CREATE TABLE sr_zone_events (
event_id TEXT PRIMARY KEY,
zone_id TEXT REFERENCES sr_zones(zone_id),
pair TEXT NOT NULL,
event_type TEXT NOT NULL, -- 'touch', 'bounce', 'break', 'sweep', 'flip'
timestamp TEXT NOT NULL,
price_at_event REAL,
penetration_pct REAL,
reaction_pips REAL,
reaction_bars INTEGER,
volume_at_event REAL,
structure_label TEXT,
fundamental_context TEXT
);
CREATE INDEX idx_sr_zones_pair ON sr_zones (pair);
CREATE INDEX idx_sr_zones_status ON sr_zones (status);
CREATE INDEX idx_sr_events_zone ON sr_zone_events (zone_id);
cot_strength_st_aligned: boolean — all three pointing same direction?cot_strength_st_divergence: boolean — any one pointing opposite?yield_fx_agreement: yield spread and currency pair moving same direction?yield_fx_divergence_days: how many days has divergence lasted?retail_orders_at_zone: count of OANDA pending orders within each active zoneretail_positions_at_zone: count of open positions near zonestop_cluster_at_zone: major stop-loss cluster just below support or above resistance?eur_strength_source: "EUR buying" / "USD selling" / "both"pre_event_compression: squeeze AND high-impact event within 24h?post_event_regime_shift: vol regime changed within 4h of high-impact release?carry_at_risk: high-carry pairs showing positive carry BUT risk regime shifting to risk-off?zone_test_during_overlap: zone tested during London/NY overlap (highest volume)?zone_test_thin_market: zone tested during holiday or Asian session below-average volume?[PANEL: DeepSeek V3.1] Derived from OANDA position book + price:
retail_trapped_side: "longs_trapped" / "shorts_trapped" / "balanced"trapped_squeeze_likely: retail_trapped_side != 'balanced' AND stop_cluster is near current priceWhen budget allows, these are the additions that separate retail from institutional analysis.
[PANEL: GPT-4.1] Kalshi and Polymarket carry real-money contracts on macro/economic data events. These are secondary venues (OANDA is primary) but provide direct market-implied probabilities that complement yield spreads and COT data.
Kalshi (primary prediction market venue):
Polymarket:
CREATE TABLE prediction_market_contracts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
venue TEXT NOT NULL, -- 'Kalshi', 'Polymarket'
contract_id TEXT NOT NULL,
event_name TEXT NOT NULL, -- e.g., 'Fed Rate Dec 2026 - 25bp Cut'
event_type TEXT, -- 'rate_decision', 'cpi', 'nfp', 'macro'
currency_context TEXT, -- which forex pairs this most influences
strike_description TEXT, -- e.g., '25bp cut', 'CPI >= 3.2'
timestamp TEXT NOT NULL,
yes_price REAL, -- probability of YES (0-1)
no_price REAL,
open_interest INTEGER,
volume_24h REAL,
event_date TEXT, -- when the event resolves
resolved INTEGER DEFAULT 0,
resolution_value TEXT
);
CREATE TABLE prediction_market_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
contract_id TEXT REFERENCES prediction_market_contracts(contract_id),
timestamp TEXT NOT NULL,
yes_price REAL,
volume_at_snapshot REAL
);
pm_fed_cut_probability: latest Kalshi/Polymarket probability of 25bp Fed cut at next meetingpm_cpi_beat_probability: probability of above-consensus CPI printpm_nfp_above_200k_probability: probability of NFP > 200kThese supplement (not replace) the CME FedWatch rate probabilities (Section 8) with real-money market-implied signals.
Collection cadence: Pull daily for all open contracts. Pull every 6 hours for contracts within 2 weeks of resolution.
[PANEL: Opus] Heartbeat and health monitoring is a MUST-HAVE. Without it, you'll discover the script died 3 days ago when an analyst asks why data is stale. A 24/5 data collection engine needs uptime monitoring, data freshness checks, database size tracking, disk space alerts, and a daily summary.
-- Query: check data freshness per pair/timeframe
SELECT pair, timeframe,
MAX(timestamp) as last_candle,
ROUND((JULIANDAY('now') - JULIANDAY(MAX(timestamp))) * 24 * 60, 1) as minutes_ago
FROM forex_candles
GROUP BY pair, timeframe
ORDER BY minutes_ago DESC;
-- Query: database size
SELECT page_count * page_size / 1024 / 1024.0 as db_size_mb
FROM pragma_page_count(), pragma_page_size();
Every day at session close (5PM ET Friday), send to EDGE TEAM group:
On startup, for each pair/timeframe:
MAX(timestamp) from forex_candles for that pair/TF[PANEL: Sonar Reasoning Pro] Explicit data validation rules prevent silent corruption.
Every incoming candle must pass these checks before storage:
high >= low: always true (reject and alert if violated)high >= open AND high >= close: always truelow <= open AND low <= close: always truetick_volume > 0: reject zero-volume candles (usually indicates a bad API response)ATR != 0: reject candles where ATR calculation produces zero (divide-by-zero in downstream calcs)RSI defined: reject candles where RSI is undefined/NaN (insufficient history for the period)UNIQUE(timestamp, pair, timeframe) before insert, log and skip duplicatesis_gap_candle = 1timestamp - prev_candle_timestamp > 50 hours, flag as weekend_gap = 1Log all rejected candles to a data_validation_errors table with reason.
[PANEL: Sonar Reasoning Pro] The most important missing piece. The spec collects 50+ indicators across 7 timeframes on 15 pairs. At candle close, that's ~5,250 data points. Even with 128k context, filling 30-40% of context with raw numbers destroys signal-to-noise ratio.
Build a filtering/aggregation layer BEFORE collecting months of data:
-- Query: top 5 pairs by confluence strength at current moment
SELECT pair,
multi_tf_confluence_score,
supertrend_direction,
hv_20_percentile_252d,
regime_trend_strategy_preference,
adx_14,
rsi_14
FROM forex_candles
WHERE timeframe = '4H'
AND timestamp = (SELECT MAX(timestamp) FROM forex_candles WHERE timeframe = '4H')
ORDER BY ABS(multi_tf_confluence_score) DESC
LIMIT 5;
-- Query: active high-confidence signals right now
SELECT pair, timeframe, flip_direction, multi_tf_alignment_count, atr_trend_at_flip
FROM supertrend_flips
WHERE flip_timestamp > DATETIME('now', '-24 hours')
AND outcome_window_expired = 0
AND multi_tf_alignment_count >= 4
ORDER BY flip_timestamp DESC;
-- Query: S&R zones being tested right now
SELECT z.pair, z.zone_low, z.zone_high, z.strength_score, z.status,
c.close, c.supertrend_direction
FROM sr_zones z
JOIN forex_candles c ON c.pair = z.pair AND c.timeframe = '4H'
AND c.timestamp = (SELECT MAX(timestamp) FROM forex_candles WHERE pair = z.pair AND timeframe = '4H')
WHERE z.status = 'Active'
AND c.close BETWEEN z.zone_low * 0.999 AND z.zone_high * 1.001
ORDER BY z.strength_score DESC;
Reducer output format for AI analysts:
multi_tf_confluence_score absolute value > 3[PANEL: Opus — primary. Supplemented by panel consensus.]
PHASE 1 — Core Infrastructure (3-5 days) OANDA API connection, candle fetcher, SQLite database with WAL mode, config file for pairs, basic error handling and Telegram alerts. Include gap detection and backfill at startup. Prove VPS, API, and storage all work together.
PHASE 2 — Core Indicators (3-5 days) SuperTrend (10/2), ATR(14), RSI(14), SMA(20/50/100/200), MACD, Bollinger Bands, ADX/DMI, Stochastic, Keltner Channels. Calculate on candle close for 15min, 1H, 4H, Daily, Weekly, Monthly (defer 5min). Use proven library (technicalindicators npm or ta-lib).
PHASE 3 — SuperTrend Flip Detection + Outcome Tracking (2-3 days) Primary trading strategy. Flip events, outcome backfill (with hard 50-candle cap), pullback entry setups. This gives analysts their most important signal immediately.
PHASE 4 — Closed-Period Structure Table (2-3 days, Sections 16.1-16.11 only) Runs only at period closes. Low-frequency, low-risk. Base metrics only (16.1-16.11). Skip 16.12 advanced metrics — calculate retroactively from raw data in v2.
PHASE 5 — Cross-Market Data (2-3 days) Yields via FRED API (reliable, free, daily granularity). Synthetic DXY from OANDA components. VIX, gold, oil, equities via Yahoo Finance / Twelve Data daily. FRED API for CME rate probabilities.
PHASE 6 — ZigZag + Price Set 1 Trend Lines (4-6 days) Custom ZigZag from scratch with confirmation state machine. Set 1 trend lines only (tightest filter, highest quality). Sets 2 and 3 deferred.
PHASE 7 — RSI Divergence Detection (3-4 days) Depends on ZigZag (Phase 6) and RSI (Phase 2). Regular and hidden divergence with normalized strength scores and outcome tracking.
PHASE 8 — S&R Zone Engine v1 (5-7 days) Methods 1, 3, 4, 9 only (multi-touch, ZigZag swings, period highs/lows, round numbers). Build clustering, scoring, decay infrastructure properly — that's the hard part. Add methods 2, 5, 6, 7, 8, 10 in v2.
PHASE 9 — OANDA Order Book + Position Book + Spread (1-2 days) Free, unique, high-value contrarian data. Pull every 20 minutes for book, every minute for spread.
PHASE 10 — Session Tracking + Economic Calendar (2-3 days) Session ranges and event proximity. Use reliable calendar source (myfxbook RSS or investing.com). Session tracking = time-windowed aggregation of existing data.
PHASE 11 — Currency Strength + Confluence Scoring (1-2 days) Both derived from existing pair data. No new API calls. High value, low effort.
PHASE 12 — Volatility Regime (1-2 days) HV percentile, Kaufman ER, BB/KC squeeze. Calculated from existing data. Defer FDI.
PHASE 13 — Health Monitoring + Gap Detection + Database Maintenance (2-3 days) Before adding more features, make it robust. Health endpoint, daily Telegram summary, automatic gap detection, database VACUUM and index optimization.
PHASE 14 — AI Prompt Reducer (2-3 days) Build the filtering/aggregation layer that transforms 5,250 data points into 30-50 actionable signals. This is the interface the analysts actually use. Do this before analysts start complaining about noise.
V2 FEATURES (weeks to months, incremental): Remaining S&R methods (2, 5, 6, 7, 8, 10), Price Sets 2 and 3, RSI trend lines, event timing correlations (Section 3), pullback entry strategy, Section 16.12 advanced metrics, lead-lag analysis, 5-minute timeframe, COT data, seasonal patterns, prediction market integration (Section 20), CME open interest, central bank speech NLP, VWAP vs TWAP decision, carry data, cross-section intelligence flags (Section 18).
| Data Type | Rows/Day | Row Size | GB/Year |
|---|---|---|---|
| forex_candles (15 pairs, 6 TFs, no 5min) | ~2,160 | ~2KB | ~1.6 GB |
| supertrend_flips | ~20-40 | ~0.5KB | ~7 MB |
| sr_zones (active) | ~500 steady state | ~1KB | ~0.2 MB |
| sr_zone_events | ~200 | ~0.5KB | ~36 MB |
| closed_period_structure | ~45/day | ~3KB | ~49 MB |
| cross_market data | ~100 | ~0.5KB | ~18 MB |
| session_summaries | ~60 | ~0.5KB | ~11 MB |
| Total estimated Year 1 | ~2 GB |
SQLite handles 2GB comfortably. Monitor at the 3GB mark and consider partitioning by year.
-- Current market state for a specific pair and timeframe
SELECT timestamp, pair, timeframe,
supertrend_direction, candles_since_last_flip,
adx_14, rsi_14, kaufman_er,
regime_trend_strategy_preference,
multi_tf_confluence_score,
hv_20_percentile_252d,
bb_inside_kc as squeeze_active
FROM forex_candles
WHERE pair = 'EUR_USD' AND timeframe = '4H'
ORDER BY timestamp DESC LIMIT 1;
-- All active SuperTrend flips with multi-TF alignment >= 4
SELECT f.*, c.adx_14, c.hv_20_percentile_252d
FROM supertrend_flips f
JOIN forex_candles c ON c.pair = f.pair AND c.timeframe = f.timeframe
AND c.timestamp = (SELECT MAX(timestamp) FROM forex_candles
WHERE pair = f.pair AND timeframe = f.timeframe)
WHERE f.outcome_window_expired = 0
AND f.multi_tf_alignment_count >= 4
AND f.flip_timestamp > DATETIME('now', '-7 days')
ORDER BY f.flip_timestamp DESC;
-- Win rate by timeframe for SuperTrend flips (last 90 days, minimum 10 events)
SELECT timeframe,
COUNT(*) as total_flips,
ROUND(AVG(CASE WHEN hit_2r = 1 THEN 100.0 ELSE 0 END), 1) as pct_hit_2r,
ROUND(AVG(CASE WHEN hit_3r = 1 THEN 100.0 ELSE 0 END), 1) as pct_hit_3r,
ROUND(AVG(max_favorable_excursion_atr), 2) as avg_mfe_atr
FROM supertrend_flips
WHERE flip_timestamp > DATETIME('now', '-90 days')
AND outcome_window_expired = 1
GROUP BY timeframe
HAVING COUNT(*) >= 10
ORDER BY pct_hit_2r DESC;
-- Strongest active S&R zones near current price for a pair
SELECT z.zone_low, z.zone_high, z.strength_score, z.status,
z.touch_count, z.bounce_rate, z.z_score,
z.detection_methods, z.source_timeframes,
ABS(c.close - (z.zone_low + z.zone_high) / 2) as distance_to_midpoint
FROM sr_zones z
JOIN forex_candles c ON c.pair = z.pair AND c.timeframe = 'D'
AND c.timestamp = (SELECT MAX(timestamp) FROM forex_candles
WHERE pair = z.pair AND timeframe = 'D')
WHERE z.pair = 'GBP_USD' AND z.status IN ('Active', 'Flipped')
ORDER BY z.strength_score DESC
LIMIT 10;
-- Yield spread vs FX pair divergence check
SELECT
yd.timestamp,
yd.value as us_de_10y_spread,
LAG(yd.value, 5) OVER (ORDER BY yd.timestamp) as spread_5d_ago,
c.close as eurusd_price,
LAG(c.close, 5) OVER (ORDER BY c.timestamp) as eurusd_5d_ago
FROM economic_data yd
JOIN forex_candles c ON DATE(c.timestamp) = DATE(yd.timestamp)
AND c.pair = 'EUR_USD' AND c.timeframe = 'D'
WHERE yd.data_type = 'yield' AND yd.series_id = 'US_DE_10Y_SPREAD'
ORDER BY yd.timestamp DESC LIMIT 20;
-- Top pairs by multi-TF confluence right now
SELECT pair,
multi_tf_confluence_score,
supertrend_direction,
adx_14,
rsi_14,
regime_trend_strategy_preference
FROM forex_candles
WHERE timeframe = '4H'
AND timestamp = (SELECT MAX(timestamp) FROM forex_candles WHERE timeframe = '4H')
ORDER BY ABS(multi_tf_confluence_score) DESC
LIMIT 5;
-- Divergences that preceded reversals (model training data)
SELECT d.*,
(d.outcome_20_candles / d.rsi_swing_2) as signal_quality
FROM divergence_events d
WHERE d.divergence_type IN ('regular_bullish', 'regular_bearish')
AND d.outcome_window_expired = 1
AND d.reversal_occurred = 1
AND d.timeframe IN ('4H', 'D')
ORDER BY d.timestamp DESC;
-- Retail trapped signal with S&R zone context
SELECT
f.pair, f.retail_trapped_side,
z.zone_low, z.zone_high, z.strength_score,
ob.stop_cluster_long_distance, ob.stop_cluster_short_distance
FROM (
SELECT pair,
CASE
WHEN underwater_longs_pct > 70 THEN 'longs_trapped'
WHEN underwater_shorts_pct > 70 THEN 'shorts_trapped'
ELSE 'balanced'
END as retail_trapped_side,
underwater_longs_pct, underwater_shorts_pct
FROM (
SELECT pair,
AVG(underwater_longs_pct) as underwater_longs_pct,
AVG(underwater_shorts_pct) as underwater_shorts_pct
FROM oanda_position_book
WHERE timestamp > DATETIME('now', '-1 hour')
GROUP BY pair
)
WHERE underwater_longs_pct > 70 OR underwater_shorts_pct > 70
) f
JOIN sr_zones z ON z.pair = f.pair AND z.status = 'Active'
JOIN oanda_order_book ob ON ob.pair = f.pair
AND ob.timestamp = (SELECT MAX(timestamp) FROM oanda_order_book WHERE pair = f.pair)
WHERE z.strength_score > 60
ORDER BY z.strength_score DESC;
is_imputed flag for forward-filled valuesoutcome_window_expired flag when cap reachedThese components from this spec are designed for reuse across all other desks:
| Component | Reusable for |
|---|---|
| OANDA API client (rate limiting, retry, backfill) | Any future desk using a REST price API |
| SQLite WAL batched-write infrastructure | Every desk — all use SQLite |
| Technical indicator library (ST, RSI, MACD, BB, ATR) | Crypto, Stocks, Futures desks |
| Custom ZigZag with confirmation state machine | Crypto (BTC/ETH swings), Stocks (index swings) |
| S&R zone engine (detection, clustering, scoring, decay) | Every asset class — only round-number grid changes |
| Economic calendar fetcher + post-release tracking | Stocks (earnings), Crypto (protocol events), Prediction markets |
| VIX, yield, equity index data (Section 8) | Stocks (directly), Crypto (risk-on/off), Options |
| Session timing and holiday calendar | Stocks, Futures, Crypto |
| Volatility regime classification | Options, Crypto, Futures |
| Round number grid (.0025 for forex, adaptable to $0.25/$1 for stocks) | Stocks, Futures, Options |
| Forward return / MFE/MAE tracking | All desks |
| Outcome backfill with hard candle cap | All desks — prevents statistics contamination |
| AI prompt reducer pattern | All desks — every desk needs signal filtering |
End of Forex Desk Merged Final Spec. Total panel cost for all rounds: ~$1.20. Panels contributing: Opus (base), Gemini 3.1 Pro, Grok 4.1, DeepSeek V3.1, GPT-4.1, Grok 4 Fast, Sonar Reasoning Pro.
src/pipeline/ta/plugins/forex-metrics.ts — proprietary metricssrc/pipeline/ta/plugins/forex-overlay.ts — overlay enrichmentta_forex_metrics — computed proprietary metric valuesta_forex_overlay — cross-desk overlay data8 models on sidecar (port 5050): Chronos-T5-Small, Chronos-T5-Base, Chronos-2, Kronos-mini, Kronos-base, Moirai, TimesFM 2.5, Lag-Llama (on-demand)
Status: NOT BUILT — add when this desk goes live for execution
Current state: All Kalshi data (prices, order books, trades) is fetched via REST API polling on cron schedules. This is fine for edge detection and monitoring, but NOT sufficient for live trade execution.
Why WebSocket matters:
Note: This desk primarily trades on other exchanges, but Kalshi has event markets (rate decisions, index levels, crypto milestones) that overlap with this desk. WebSocket upgrade applies when trading any Kalshi markets.
Added 2026-03-29 — upgrade REST to WS when desk moves to live execution