Fonflo System Architecture
ES futures trading intelligence system. Auto-generated documentation for continuous improvement.
Recent updates (April 2026)
• Databento migrated as primary historical data source (561K bars / 12mo of 1-min data, local parquet)
• Strategy lab throttled from 8,640 → 12 cycles/day, switched to Opus 4.6 (premium hypotheses, $14/wk budget)
• 10pt minimum edge floor applied to both signal validation and strategy grading (volatile market hardening)
• Hallucination guard validates every price in narrative against known levels
• Schwab auto-fallback on token expiry (yfinance + Discord DM)
• JSON file locking via safe_json.py atomic writes
• IBKR auto-reconnect at 5:30 AM PST
System Overview
Fonflo is an automated ES (E-mini S&P 500) futures signal generation and monitoring system. It runs 24/7 with market-hours task scheduling, integrates multiple external signal sources, applies institutional-grade confidence scoring, and publishes signals and analytics to Discord.
Tech Stack
| Component | Technology |
|---|---|
| Language | Python 3.14 |
| Historical Data (Primary) | Databento — local parquet (561K bars, 12mo, no API auth issues) |
| Live Quotes (Primary) | Schwab API — current price, auto-fallback to yfinance on token expiry |
| Live DOM/Ticks | IBKR TWS — Level 2 DOM, tick-by-tick (auto-reconnect at 5:30 AM) |
| Fallback Tier | yfinance — used when Schwab and Databento both fail |
| LLM | Claude Haiku (high-volume), Sonnet (morning report narrative) |
| Communication | Discord webhooks + bot token |
| Intelligence | Discord user token, Substack RSS, Gmail IMAP |
| Graphics | Playwright HTML-to-PNG rendering |
| Hosting | Cloudflare Pages (web), local Windows machine (runtime) |
| State | JSON files in data/ directory (no database) |
Task Schedule (PST)
| Time | Task | Component |
|---|---|---|
| 6:00 AM | Level calc, gamma, inventory, prev-day classify | level_engine, gamma_monitor, etc. |
| 6:15 AM | Morning report draft + spawn monitor | morning_report.generate_draft() |
| 6:25 AM | Opening auction classifier | opening_auction |
| 6:00-2:00 PM | Market-hours components active | regime, orderflow, price_alerts, etc. |
| 1:55 PM | RTH snapshot (before Schwab resets) | scheduler.snapshot_rth_session() |
| 2:00 PM | EOD report grading | report_grader |
| 2:05 PM | EOD summary PNG | scheduler.send_eod_summary() |
| 2:15 PM | Production audit + missed opp analysis | production_tracker, missed_opp |
| 2:30 PM | Lab site deploy | lab_publisher |
| Always-on | Discord/Substack/Email/X/News monitors | discord_monitor, email_monitor, etc. |
Component Groups
- Always-running: discord_monitor, substack_monitor, email_monitor, x_monitor, news_monitor, welcome_bot
- Market-hours (6 AM - 2 PM PST): regime_detector, session_manager, correlation_filter, internals_monitor, orderflow_engine, price_alerts, alert_engine, quant_signals
Signal Pipeline
Every signal flows through a strict pipeline: Detection → Dedup → Scoring → Validation → Discord → Outcome Tracking.
Signal Detection
Primary signal engine. Monitors ES price against pivot/R1/R2/S1/S2 levels. Fires on rejection/bounce patterns.
- Bearish: R1/R2 rejection — price touched resistance (within 8pts), pulled back 15pts, trend down
- Bullish: S1/S2/Pivot bounce — price touched support (within 8pts), bounced 15pts, trend up
- Breakout: R1 break with momentum + intel confirmation
- Breakdown: S1 break with momentum + intel confirmation
IBKR TWS Level 2 DOM + tick data. Generates orderflow-based signals.
- CVD Divergence: Price makes new high/low but cumulative delta diverges
- Absorption: Large unfilled prints on breakout, absorbed quickly
- Large Print Bounce: Single mega-print fails to push price
- Delta Imbalance: Consistent positive/negative delta bars
Confidence Scoring
Base confidence from signal engine (6 default)
Session adjustment:
PREMARKET (6-9:30 AM): base - 1
OPEN (9:30-11 AM): base - 2 (choppy)
MIDMORNING (11-12): base + 0
LUNCH (12-1 PM): base + 1
AFTERNOON (1-3 PM): base - 1
POWERHOUR (3-4 PM): base - 2 (wild)
Regime adjustment:
TRENDING: +0
RANGING: +2 (mean reversion setups)
VOLATILE: +3 (tight stops)
Confluence bonuses:
+ intel source match: +2
+ level confluence: +1
+ internals (tick/trin): +1
+ correlation (NQ/CL): +1
+ aplus filter: +1
Risk penalties:
- Event lockout: -2
- VIX extreme: -1
- Counter-trend regime: -4
- Inventory extreme: -2
Final: clipped to 4-9 range
Gate: Only post if confidence >= 8
Deduplication Rules
Price threshold: 4 points — signals within 4pts of an active signal are suppressed
Time window: 15 minutes — same-direction signals within 15min are suppressed
Max same-direction: 2 active signals in same direction at once
Min price move: 15 points between consecutive signals
Flip cooldown: 15 minutes after direction change
Signal 4+: Escalating confidence — must meet Edge Detection (85%+) threshold
Edge Detection
Formula: 60% * (sources_aligned / 2) + 40% * (confluence / 5)
Threshold: >= 85% triggers "Fonflo Edge Detection" badge
Sources: intel_1 alignment, intel_2 alignment
Confluence: VIX, major level, price action, time-of-day, volume
Signal 1-3: No Edge Detection required
Signal 4+: Must pass Edge Detection or suppressed
Morning Report
The largest module (2786 lines). Generates a pre-market narrative report as PNG for Discord at 6:15 AM PST.
Generation Flow
Verification Checks
- HOD/LOD: Validates high-of-day and low-of-day references against real data
- RTH/ETH confusion: Catches mixing of RTH close with overnight prices
- Price accuracy: Compares every mentioned price against actual data
- Narrative tone: Flags "flat" for +2% days, "sold off" for +1% days, etc.
- Level ordering: R3 > R2 > R1 > Pivot > S1 > S2 > S3
- Level staleness: Flags pivot >100pts from current live price
- VIX accuracy: Compares mentioned VIX against actual
- Economic events: Validates event times and names
- Claude deep-check: LLM reviews for subtle logical errors
Draft Approval Workflow
Draft posted → poll immediately for reactions
Thumbs up: Publish instantly to #premarket-report
No response after 5 min: Auto-publish
Thumbs down: Hold for edits, extend deadline to 15 min
Duplicate guard: _published_today flag prevents double posting
Levels Engine
level_engine.py (994 lines) — Three-tier data sourcing with LLM-based smart money cross-reference.
Prior RTH Data Fallback Chain
- RTH Snapshot (data/rth_session.json) — captured at 1:55 PM PST. Must match yesterday's date or skipped.
- yfinance daily bars — clean RTH H/L/C from ES=F ticker
- yfinance 5m intraday — filter to 9:30 AM - 4:00 PM ET window
Level Calculation
Pivot = (H + L + C) / 3
R1 = 2*Pivot - L R2 = Pivot + (H-L) R3 = H + 2*(Pivot-L)
S1 = 2*Pivot - H S2 = Pivot - (H-L) S3 = L - 2*(H-L)
Separation: min 8pts between levels (or ATR*0.5)
Rounding: nearest 0.25
Gap Detection: if current price >100pts from pivot,
recalculate using 24hr H/L/C (includes overnight)
Regime Detector
ATR_short = ATR(10 bars) ATR_long = ATR(30 bars)
MA_fast = SMA(20) MA_slow = SMA(50)
VOLATILE: ATR_short/ATR_long >= 1.5 and range_ratio > 1.3
TRENDING_UP: close > MA_fast > MA_slow and close > VWAP and 3 higher closes
TRENDING_DOWN: close < MA_fast < MA_slow and close < VWAP and 3 lower closes
RANGING: ATR_ratio <= 0.85
DEFAULT: VWAP bias + close direction
Intelligence Sources
Three proprietary intelligence feeds. Never mentioned by name in public channels.
Source 1 — Discord Monitor
Monitors external Discord channel. Classifies messages: SIGNAL, REPOST, DUPLICATE, COMMENTARY, IMAGE_ONLY. Extracts prices/levels/bias via regex + Claude. Tracks accuracy. Posts to internal channel. Applies +2 confidence bonus to matching signals (decays after 120min).
State: adam_log.json, adam_accuracy.json, adam_posts_cache.json
Source 2 — Substack RSS
Polls Substack RSS every 15min (market hours) / 60min (off). HTML → text → Claude Haiku → structured output. Extracts bias, levels, summary. Finds confluence with level_engine (±5pts).
State: mancini_log.json, mancini_cache.json
Source 3 — Email (Gmail IMAP)
Monitors Gmail for daily plans from external source. Skips free-version stubs. Extracts HTML → text → Claude. Parses bias, scenarios, levels, macro context. Finds confluence with level_engine.
State: tictoc_log.json, email_cache.json
Quant Lab
Autonomous AI-powered strategy discovery engine running 24/7.
Strategy Discovery Pipeline
Grade Criteria
| Criteria | Grade A | Grade B | Grade F |
|---|---|---|---|
| Mean P&L per trade | ≥10pts | ≥10pts | <10pts (auto-kill) |
| Full PF | >1.5 | >1.3 | Below |
| OOS PF | >1.3 | >1.0 | Below |
| OOS Win Rate | >50% (Wilson CI) | >45% | Below |
| Edge vs Random | >2x | >1x | No edge |
| Sample (IS/OOS) | ≥30/20 | ≥25/15 | Below |
10pt floor rationale: Volatile ES sessions have 50-100pt daily ranges. Sub-10pt edges get eaten by spread, slippage, and intra-bar variance. Strategies must demonstrate they can pull at least 10pts per trade (after costs) to survive. This applies to both in-sample and out-of-sample mean P&L.
Production Tracking
Probation: First 10 trades. PF <1.0 after 5 trades = immediate kill.
Active: Normal monitoring. PF <0.8 = DEGRADED. PF <0.6 after 30 trades = auto-kill.
Dormant: No signal in 15 trading days. Flagged for review.
Killed: Logged with reason. Fed back as negative constraint to AI inventor.
Lab Token Budget
Cap: $20/week. Strategy lab uses Opus 4.6 for premium hypothesis generation.
Throttle: 12 cycles/day max, one every 2 hours. Daily counter resets at midnight ET.
Per-cycle cost (Opus 4.6): ~1,150 input + 2,000 output tokens = $0.17/cycle
Daily lab spend: 12 × $0.17 = $2.04/day
Weekly lab spend: ~$14.28/week (leaves ~$5/week headroom for morning report + verification)
Why Opus over Haiku: 12 high-quality strategies/day beats 8,640 mediocre ones. Sample size is wasted if hypotheses are noise.
Lab Cost Breakdown
| Component | Model | Volume | Cost/day |
|---|---|---|---|
| Strategy lab (Opus) | Opus 4.6 | 12 cycles | $2.04 |
| Morning report narrative | Sonnet 4.6 | 1 call | $0.05 |
| Verify deep-check | Haiku 4.5 | 1 call | $0.005 |
| Intel parsing (Discord/Substack/Email) | Haiku 4.5 | ~20 calls | $0.02 |
| Level engine LLM | Haiku 4.5 | 1 call | $0.005 |
| Total daily | ~$2.12 | ||
| Total weekly | ~$14.84 | ||
Rules & Guardrails
Signal Rules
Min target: 10 points. Sub-10pt edges get eaten by spread/slippage in volatile markets.
Max target: 30 points. T1 capped at 30pts from entry.
Max stop: 20 points. No signal with stop wider than 20pts.
R:R range: 1.2 to 3.0. Outside this = suppressed.
Daily cap: 3 signals/day from price_alerts. Goes silent after 3.
First-touch filter: Each level alerts once per session. Re-alert only after price travels 8+pts away.
Stale entry: If price moved >10pts from detection, suppress. 5-10pts → re-price.
Counter-trend penalty: -4 confidence for counter-regime signals.
Min price move: 15pts between consecutive signals.
Flip cooldown: 15 minutes after direction change.
Invalidation: Bypasses all cooldowns and gates. Immediate post.
Morning Report Rules
Voice: Professional trader, dry, confident, honest about uncertainty. Active voice.
Paragraphs: Max 2-3 per section. Max 3 sentences each.
No decimals: All prices are whole numbers (6563 not 6563.25).
No commas: 6563 not 6,563.
No parentheses around prices: "the 6563 pivot" not "the pivot (6563)".
Gap days: If current price >100pts from pivot, reference where price IS, not where it closed.
Scenarios: Technical only — no events, news, or macro. Price levels, volume, structure.
Never say: globex, POC, net short inventory, spike day, settlement, structural deterioration, gamma posture, value area, traders may want to, consider, potential
Public Channel Rules
NEVER mention external trader names, sources, or services.
Replace with: "confluence confirmed", "key pivot", "multiple confluences align"
Level confidence labels:
- CALCULATED: our methodology only
- CONFIRMED: our level + one internal source agrees
- CONFIRMED (double): our level + two sources agree
No confidence scores in public output.
Data Rules
Never use hardcoded or sample data. Every number from live APIs or real logs.
Data source priority: Schwab first, IBKR second (DOM only), yfinance last. Always log which source used.
RTH snapshot: Must capture at 1:55 PM PST or data is lost (Schwab resets H/L/C after close).
Stale snapshot detection: Check date matches yesterday's trading day before using.
Schwab token: Expires every 7 days. Run reauth_schwab.py to refresh.
Active contract: ESM6 (June 2026, expires 2026-06-20). Symbol format: single-digit year.
File Map
Core Pipeline
| File | Lines | Purpose |
|---|---|---|
| scheduler.py | 572 | Master controller, task scheduling, component lifecycle |
| morning_report.py | 2786 | Pre-market report generation, draft workflow, Claude narrative |
| price_alerts.py | 802 | Primary signal detection (level rejection/bounce/breakout) |
| orderflow_engine.py | 989 | IBKR DOM/tick signal detection (CVD, absorption, delta) |
| alert_engine.py | 1000 | Confidence scoring, multi-stage filtering, Discord posting |
| level_engine.py | 994 | S/R level calculation, LLM cross-reference |
| verify_daily_alpha.py | 800+ | Report auditor (price, tone, levels, VIX, events) |
| fonflo_graphics.py | 852 | PNG rendering (signal cards, results, reports, EOD) |
| signal_logger.py | 446 | Signal dedup, Edge Detection, outcome tracking |
| signal_gateway.py | ~200 | Unified dedup across engines (built, not wired in) |
| realtime_flo.py | 951 | Smart setups: IB fade, failed auction, single prints, etc. |
| schwab_data.py | 240 | Schwab API client (quotes, snapshots) |
| config.py | 166 | Centralized .env config, all thresholds |
Intelligence
| File | Lines | Purpose |
|---|---|---|
| discord_monitor.py | 1273 | Source 1: Discord signal extraction + accuracy tracking |
| substack_monitor.py | 520 | Source 2: Substack RSS → Claude parsing |
| email_monitor.py | 657 | Source 3: Gmail IMAP → Claude parsing |
| x_monitor.py | ~400 | Source 1 X/Twitter feed via Grok API |
| news_monitor.py | ~500 | Overnight news aggregation via Grok |
Market Context
| File | Purpose |
|---|---|
| regime_detector.py | Real-time market regime classification |
| correlation_filter.py | NQ/CL divergence checks |
| internals_monitor.py | Market breadth (TICK, TRIN, ADD) |
| gamma_monitor.py | Gamma exposure zones |
| inventory_analyzer.py | Market maker positioning |
| previous_day_classifier.py | Prior session day-type classification |
| opening_auction.py | Opening type detection (gap, drive, auction) |
Quant Lab
| File | Lines | Purpose |
|---|---|---|
| quant/strategy_lab.py | 1100 | Autonomous discovery framework (detect → backtest → grade) |
| quant/strategy_inventor.py | 550 | Claude Haiku generates Python detection code |
| quant/signal_engine.py | 700 | Live signal generation from promoted strategies |
| quant/day_classifier.py | 400 | 6 day types + real-time prediction at 4 checkpoints |
| quant/production_tracker.py | 400 | Strategy lifecycle: probation → active → degraded → killed |
| quant/lab_publisher.py | 2200 | HTML report generation + Cloudflare deployment |
State Files (data/)
| File | Purpose |
|---|---|
| draft_state.json | Current morning report draft + approval status |
| level_cache.json | Cached level engine output |
| levels.json | Full level detail with notes and confluences |
| rth_session.json | RTH snapshot (H/L/C) from 1:55 PM capture |
| session_state.json | Inventory, prev-day, opening auction state |
| gamma_state.json | Gamma zone + flip level |
| adam_log.json | Source 1 parsed signals |
| mancini_log.json | Source 2 parsed analyses |
| tictoc_log.json | Source 3 parsed plans |
| news_log.json | Aggregated overnight news |
| signals_database.json | All signals with outcomes |
| econ_calendar_cache.json | Economic events for today |
Data Flow Summary
IBKR TWS ──→ orderflow_engine ──→ signal_queue ──→ alert_engine ──→ Discord
│ ↑
└──→ bar_queue ──→ regime_detector ─────┘
│
Schwab API ──→ price_alerts ─────────────→ alert_engine
│ │
└──→ level_engine ──→ levels.json ──→ morning_report
│ │
└──→ schwab_data ──→ fonflo_graphics ↓
│ PNG → Discord
Discord ──→ discord_monitor ──→ adam_log.json ──→ alert_engine (bonus)
Substack ──→ substack_monitor ──→ mancini_log.json ──→ morning_report
Gmail ──→ email_monitor ──→ tictoc_log.json ──→ morning_report
Claude Haiku ──→ strategy_inventor ──→ strategy_lab ──→ production_tracker
│
signal_engine ──→ Discord
Known Issues & Fragile Patterns
- Global state via module singletons (regime_detector._regime_state) — no synchronization for async access
- JSON state files are last-write-wins — no file locking, concurrent writes can corrupt
- Signal master log can grow unbounded; no archiving or pruning
- RTH snapshot stale detection added (date check) but rth_session.json has no file locking
- Schwab token expires every 7 days — manual refresh required via reauth_schwab.py
- IBKR TWS auto-logs out at 11:59 PM daily — must re-login before 5:45 AM
- Discord user token auth (not bot) for monitors — violates Discord TOS
- yfinance rate limiting can fail silently — returns empty DataFrame
- signal_gateway.py built but not wired in — dedup is per-engine, not unified
- Single global _active_signal — only one direction tracked at a time
- Outcome tracking relies on same module's price feed — if price lags, outcome is wrong
- Entry re-pricing moves stop but recalculates distance from OLD entry
- 2786 lines — single largest file, doing too much (data fetch + LLM + verify + PNG + draft workflow)
- Claude narrative can reference levels not in CALCULATED_LEVELS (hallucination)
- Level fallback chain has 3 tiers but error recovery between tiers is inconsistent
- Contract expiry hardcoded to 2026-06-20 — needs manual update for roll
- All algo thresholds are static in config.py — no dynamic adaptation, requires restart to change
- Market holidays hardcoded through 2027 — needs annual update
- Session time weights are fixed; no adaptation to observed win rates
- Signal queues are unbounded — could grow without limit under load
Implementation Logic (Pseudocode)
Actual decision logic extracted from source code. Line numbers reference the Python files.
Signal Detection — price_alerts.py _check_signals()
# FREQUENCY GATES (applied before any detection)
IF abs(current_price - last_signal_price) < 15pts: SKIP # MIN_PRICE_MOVE
IF (now - last_signal_time) < 15min AND direction flipped: SKIP # FLIP_COOLDOWN
IF len(price_history) < 5: SKIP # need min samples for rejection detection
# BEARISH at R1/R2 (resistance rejection)
FOR each level in [R1, R2]:
recent_high = max(price_history[-5:])
IF recent_high >= level - 8pts # touched resistance (PROXIMITY_PTS=8)
AND current_price < level - 15pts # pulled back (REJECTION_PTS=15)
AND trend == DOWN: # confirmed downtrend
# Intel filter: skip if any source is explicitly BULLISH
IF mancini_bias == "BULLISH" OR tictoc_bias == "BULLISH": SKIP
# Confluence check: count aligned factors
confluence = 0
IF mancini_bias == "BEARISH": confluence += 1
IF tictoc_bias == "BEARISH": confluence += 1
IF vix > 25: confluence += 1
IF price_action_bearish: confluence += 1
# If both sources SILENT: require 2+ non-intel factors
IF mancini_bias is None AND tictoc_bias is None:
IF confluence < 2: SKIP
IF confluence == 0: SKIP
entry = int(current_price)
stop = min(level + 8, entry + 20) # MAX_STOP_PTS = 20
target = next_lower_level(pivot, S1)
# R:R validation
risk = abs(entry - stop)
reward = abs(entry - target)
IF reward > 30: target = entry - 30 # MAX_TARGET_PTS = 30
rr = reward / risk
IF rr < 1.2 OR rr > 3.0: SKIP # MIN/MAX_RR_RATIO
FIRE BEARISH signal
# BULLISH at S1/S2/Pivot (support bounce) — mirror logic
FOR each level in [S1, S2, Pivot]:
recent_low = min(price_history[-5:])
IF recent_low <= level + 8pts AND current_price > level + 15pts AND trend == UP:
[same intel/confluence checks, reversed direction]
entry = int(current_price)
stop = max(level - 8, entry - 20)
target = next_higher_level(pivot, R1)
FIRE BULLISH signal
# BREAKOUT (R1 break with momentum)
IF current_price > R1 + 8pts
AND recent_low < R1 # was below R1 recently
AND trend == UP
AND (mancini == "BULLISH" OR tictoc == "BULLISH"): # requires intel confirmation
entry = current_price
stop = max(R1 - 5, entry - 20)
target = R2
FIRE BULLISH breakout
# BREAKDOWN (S1 break with momentum) — mirror of breakout
IF current_price < S1 - 8pts
AND recent_high > S1 AND trend == DOWN
AND (mancini == "BEARISH" OR tictoc == "BEARISH"):
FIRE BEARISH breakdown
Validation Gate — price_alerts.py _send_alert()
# HARD VALIDATION (before anything posts to Discord)
IF abs(entry - stop) > 20: SUPPRESS # stop too wide
IF reward/risk < 1.2 OR > 3.0: SUPPRESS # R:R out of range
# STALE ENTRY CHECK
current_price = schwab.get_es_quote()["last"]
IF direction == LONG:
overshoot = current_price - entry
IF overshoot > 10pts: SUPPRESS # entry too stale, price ran away
IF overshoot > 5pts: RE-PRICE entry to current, adjust stop/target
ELIF direction == SHORT:
overshoot = entry - current_price
[same logic reversed]
# EDGE DETECTION GATE (signal #4+)
IF daily_signal_count >= 3:
confidence = (sources_aligned/2 * 0.6) + (confluence_count/5 * 0.4)
IF confidence < 0.75: SUPPRESS # not enough edge for 4th+ signal
# If all gates pass:
# 1. Generate PNG via fonflo_graphics
# 2. POST to Discord
# 3. Set _active_signal for outcome tracking
# 4. Increment counters
Confidence Scoring — alert_engine.py
# Step-by-step confidence calculation
total = signal.base_confidence # Tier 1 = 3, Tier 2 = 2
# DELTA STRENGTH
IF delta_strength == "Strong": total += 2
ELIF delta_strength == "Weak": total -= 1
# LEVEL CONFLUENCE
IF strong_level within 3.0pts: total += 2
ELIF weak_level nearby: total += 1
# LEVEL RECLAIM PRIORITY
IF signal_type == "LEVEL_RECLAIM":
IF priority <= 2: total += 2
ELIF priority <= 4: total += 1
# PIVOT CONFLUENCE
IF price within 5.0 of pivot: total += 2
IF LONG AND price >= pivot: total += 1 # pivot reclaim bonus
IF SHORT AND price <= pivot: total += 1 # pivot loss bonus
# INTEL AGREEMENT
IF source1_signal matches direction AND not expired (120min): total += 2
IF source2_level within 5pts of signal: total += 2 (failed breakdown) or +1 (regular)
IF source3_bias matches direction: total += 1
IF all three agree: total += 1 (triple confluence bonus)
# REGIME
IF regime matches signal direction: total += 1
IF regime OPPOSES signal (counter-trend): total -= 4 # HEAVY PENALTY
IF regime == VOLATILE: total -= 1
# CORRELATION (NQ/CL)
total += correlation.confidence_adjustment # typically -1 to +1
# MULTI-TIMEFRAME
IF signal.timeframe == 5min: total += 1
# MARKET INTERNALS
total += internals.confidence_for_direction() # -1 to +2
# SESSION WEIGHTS (time-of-day adjustment)
# Pre-market: threshold +2 | Open: -1 | Midmorning: 0
# Lunch: +3 | Afternoon: -1 | Power hour: -1
# GATE: Only post if total >= ALERT_MIN_CONFIDENCE (8)
Edge Detection Formula — signal_logger.py
# Edge Detection — determines if signal gets "Fonflo Edge Detection" badge
source_weight = 0.6 # 60% weight on source alignment
confluence_weight = 0.4 # 40% weight on confluence factors
source_score = (sources_aligned / 3) * 100 * 0.6
# sources: intel_1, intel_2, level_at_major
confluence_score = (confluence_factors / 5) * 100 * 0.4
# factors: VIX, alert signal, time-of-day, price action, volume
confidence = round(source_score + confluence_score)
EDGE_TRIGGERED = confidence >= 85
# Examples:
# 3/3 sources + 4/5 factors = (1.0 * 60) + (0.8 * 40) = 92% ✓ TRIGGERED
# 2/3 sources + 5/5 factors = (0.67 * 60) + (1.0 * 40) = 80% ✗ NOT TRIGGERED
# 2/3 sources + 4/5 factors = (0.67 * 60) + (0.8 * 40) = 72% ✗ NOT TRIGGERED
# 3/3 sources + 3/5 factors = (1.0 * 60) + (0.6 * 40) = 84% ✗ NOT TRIGGERED
Deduplication Logic — signal_logger.py
PRICE_THRESHOLD = 4.0 pts
TIME_WINDOW = 15 minutes
MAX_SAME_DIRECTION = 2
# HARD CAP: max 2 active signals in same direction
same_dir_active = [s for s in active_signals
if s.direction == new.direction
and s.status in (PENDING, ACTIVE)]
IF len(same_dir_active) >= 2: SUPPRESS "MAX_SAME_DIRECTION"
# PROXIMITY CHECK: same area + recent = duplicate
FOR each active signal of same direction:
price_diff = abs(signal.entry - new_entry)
time_diff = (now - signal.timestamp).minutes
IF price_diff <= 4.0 AND time_diff <= 15:
UNLESS signal.status in (STOPPED_OUT, SCRATCH):
SUPPRESS "DUPLICATE"
# Cleanup: remove signals older than 8 hours
# Or older than 15min AND status is closed
Regime Classification — regime_detector.py
# Inputs: 1-min bars from IBKR via bar_queue
atr_short = ATR(last 10 bars) # 10-min momentum
atr_long = ATR(last 30 bars) # 30-min structural
atr_ratio = atr_short / atr_long
ma_fast = SMA(last 20 bars) # 20-min MA
ma_slow = SMA(last 50 bars) # 50-min MA
vwap = cumsum(price * volume) / cumsum(volume)
range_ratio = avg_range(last 10 bars) / avg_range(prior 10 bars)
higher_closes = bars[-3].close > bars[-4].close > bars[-5].close
lower_closes = bars[-3].close < bars[-4].close < bars[-5].close
# Classification (checked in order — first match wins)
IF atr_ratio >= 1.5 AND range_ratio > 1.3:
regime = VOLATILE # both conditions required
ELIF close > ma_fast > ma_slow AND close > vwap AND higher_closes:
regime = TRENDING_UP
ELIF close < ma_fast < ma_slow AND close < vwap AND lower_closes:
regime = TRENDING_DOWN
ELIF atr_ratio <= 0.85:
regime = RANGING # tight compression
ELSE: # default fallback
IF close > vwap AND higher_closes: regime = TRENDING_UP
ELIF close < vwap AND lower_closes: regime = TRENDING_DOWN
ELSE: regime = RANGING
# Confidence threshold adjustments
IF VOLATILE: threshold_delta = +3 # raise thresholds (harder to fire)
IF RANGING: threshold_delta = +2
ELSE: threshold_delta = 0
RTH Data Fallback Chain — morning_report.py
# _get_prior_rth_ohlc() — 3-tier fallback for yesterday's RTH H/L/C
# Determine yesterday's trading date
yesterday = (now - 1 day)
WHILE yesterday is weekend: yesterday -= 1 day
IF yesterday in MARKET_HOLIDAYS: yesterday -= 1 day (skip holidays too)
# TIER 1: RTH Snapshot (data/rth_session.json)
snapshot = load("data/rth_session.json")
IF snapshot.date == str(yesterday): # DATE MUST MATCH
IF snapshot.high > 0 AND snapshot.low > 0:
RETURN {open, high, low, close from snapshot}
ELSE:
LOG WARNING "Snapshot STALE — date={snapshot.date}, expected={yesterday}"
# TIER 2: yfinance daily bars (clean RTH H/L/C)
daily = yfinance.Ticker("ES=F").history(period="5d", interval="1d")
IF len(daily) >= 2:
yesterday_row = daily.iloc[-2] # second to last (last = today's partial)
IF high > 0 AND low > 0 AND close > 0:
RETURN {open, high, low, close from yesterday_row}
# TIER 3: yfinance 5m intraday bars, filtered to RTH window
hist = yfinance.Ticker("ES=F").history(period="2d", interval="5m")
rth_bars = [bar for bar in hist if 9:30 AM ET <= bar.time < 4:00 PM ET
AND bar.date == yesterday]
IF rth_bars:
RETURN {open=first.open, high=max(highs), low=min(lows), close=last.close}
RETURN {0, 0, 0, 0} # all tiers failed
Gap Detection — morning_report.py _get_levels()
# After calculating standard RTH pivots, check if they're relevant
current_price = schwab.get_es_quote()["last"]
pivot = levels["pivot"]
IF abs(current_price - pivot) > 100: # MASSIVE GAP
# Standard RTH pivots are useless — recalculate with 24hr data
gap_h = schwab_quote["high"] # includes overnight
gap_l = schwab_quote["low"] # includes overnight
gap_c = schwab_quote["close"] # prior RTH close
gap_pivot = (gap_h + gap_l + gap_c) / 3
gap_r1 = 2 * gap_pivot - gap_l
gap_r2 = gap_pivot + (gap_h - gap_l)
gap_r3 = gap_h + 2 * (gap_pivot - gap_l)
gap_s1 = 2 * gap_pivot - gap_h
gap_s2 = gap_pivot - (gap_h - gap_l)
gap_s3 = gap_l - 2 * (gap_h - gap_l)
LOG WARNING "GAP DETECTED: current={current}, RTH pivot={pivot}"
REPLACE levels with gap-adjusted levels
# Scenario fallbacks also adapt:
IF current_price > R1:
bull_scenario = "Hold above R1, push through R2. Extended target R3."
bear_scenario = "Rejection at R2. Fade back to R1 then pivot."
ELIF current_price > pivot + 50:
bull_scenario = "Hold above pivot. Target R1 then R2."
bear_scenario = "Lose R1 and fail to hold pivot. Target S1."
ELSE:
bull_scenario = "Reclaim pivot with volume. Target R1 then R2."
bear_scenario = "Lose pivot. Target S1 then S2."
Verification Checks — verify_daily_alpha.py
# 9 verification checks run on every morning report draft
CHECK A: HOD/LOD
Find "high" or "low" near 4-5 digit prices in draft
IF mentioned_high differs from actual es_high by > 10pts: ERROR
IF mentioned_low differs from actual es_low by > 10pts: ERROR
CHECK B: RTH/ETH Confusion
IF (current_globex - rth_close) < 10: SKIP (no confusion possible)
Find "closed at X" mentions
IF mentioned_price closer to globex than RTH: ERROR "RTH/ETH confusion"
Also flag recovery language ("bounced", "found support") without
qualifier ("overnight", "globex") when RTH was actually down
CHECK C: Price Accuracy
Find "closed at X" mentions
IF abs(mentioned - es_close) > 2pts: ERROR
CHECK D: Narrative Tone
Map daily_pct_change to expected descriptors:
abs(pct) < 0.3%: "flat" OK
pct < -0.3%: "down/sold/declined" expected
pct > +0.3%: "up/rallied/gained" expected
IF flat_word found but abs(pct) >= 0.3%: ERROR
IF up_word found but pct < -0.3% (without qualifier): ERROR
IF down_word found but pct > +0.3% (without qualifier): ERROR
CHECK E: Level Ordering
Extract R3, R2, R1, Pivot, S1, S2, S3 from draft
Verify strict: R3 > R2 > R1 > Pivot > S1 > S2 > S3
IF abs(pivot - es_close) > 50pts: ERROR "unreasonable pivot"
IF abs(pivot - current_live_price) > 100pts: ERROR "stale levels"
CHECK F: VIX Accuracy
Find "VIX at X" mentions
IF abs(mentioned - vix_actual) > 2: ERROR
CHECK G: Economic Events
Extract "EVENT at HH:MM AM/PM ET" from draft
Match against known calendar
IF stated_time != expected_time: ERROR
CHECK H: Claude Deep-Check
Send draft + market data to Claude API
Flag ONLY: tone vs reality, wrong close, wrong direction, RTH/ETH confusion
Do NOT flag: style, opinions, level calculations, approximate language
Smart Setups — realtime_flo.py
# 5 intraday setups, max 3 alerts per session
SETUP 1: IB FADE [BACKTESTED — 64% WR]
Delegates to quant.signal_engine.check_auction_fade()
Condition: IB breakout fails, price traps back inside
Posted as full signal card with entry/stop/target
SETUP 2: FAILED AUCTION [OBSERVATION ONLY]
FOR each of last 3 bars:
IF bar.high >= IB_high + 1.0 AND bar.high <= IB_high + 3.0:
IF any of next 2 bars closes below IB_high:
Signal: SHORT, stop = bar.high + 2, target = IB_mid
[Mirror for IB low break → LONG]
SETUP 3: SINGLE PRINTS FILL [OBSERVATION ONLY]
Track 5-min bars with no overlap to adjacent bars (profile gaps)
When price returns to fill gap: setup triggered
SETUP 4: EXCESS VOLUME DIVERGENCE [OBSERVATION ONLY]
New price extremes on declining volume = exhaustion
Low-volume new highs → bearish divergence
Low-volume new lows → bullish divergence
SETUP 5: VALUE AREA MIGRATION [OBSERVATION ONLY]
Developing value area shifts direction mid-session
Shift UP = bullish continuation, shift DOWN = bearish continuation
POSTING RULES:
Market hours only (9:30 ET - 4:00 PM ET)
Min 60 seconds between posts
Max 3 setup alerts per session
Backtested → signal card | Non-backtested → observation only
If no setups by 10:30 ET → post silence message
Backtest Engine — quant/strategy_lab.py
# Walk-forward backtest on 1-min bars
CONSTANTS:
SLIPPAGE = 0.25 pts per side
COMMISSION = $4.50 RT = 0.09 pts equivalent
MIN_SAMPLE_IS = 30 trades (in-sample)
MIN_SAMPLE_OOS = 20 trades (out-of-sample)
_sim_trade(bars, entry, stop, target, direction):
FOR each bar after entry:
IF direction == SHORT:
IF bar.high >= stop:
exit = stop + SLIPPAGE
pnl = entry - exit - COMMISSION_PTS
RETURN LOSS
IF bar.low <= target:
exit = target + SLIPPAGE
pnl = entry - exit - COMMISSION_PTS
RETURN WIN
IF direction == LONG:
IF bar.low <= stop:
exit = stop - SLIPPAGE
pnl = exit - entry - COMMISSION_PTS
RETURN LOSS
IF bar.high >= target:
exit = target - SLIPPAGE
pnl = exit - entry - COMMISSION_PTS
RETURN WIN
# Session ended without hit → exit at last close
pnl = (last_close - entry) * direction_sign - SLIPPAGE - COMMISSION_PTS
RETURN WIN if pnl > 0 else LOSS
# WALK-FORWARD SPLIT:
# Train: months 1-4 (in-sample)
# Test: months 5-6 (out-of-sample, strict)
# Entry: t+1 bar open (zero lookahead)
# Stops: checked intrabar on H/L, not close
# MONTE CARLO RANDOM CONTROL:
FOR i in 1..200:
Generate random entries on same trading days
Use identical stop/target distances
Compute full P&L distribution
strategy_edge = strategy_mean_pnl / random_mean_pnl
# WILSON SCORE CI (95% on win rate):
z = 1.96
p = win_rate
denom = 1 + z²/n
center = (p + z²/(2n)) / denom
margin = z * sqrt(p(1-p)/n + z²/(4n²)) / denom
ci_lower = max(0, center - margin)
ci_upper = min(1, center + margin)
Strategy Grading — quant/strategy_lab.py
# Grade assignment after backtest + Monte Carlo + VIX decomposition
GRADE A (promote immediately):
full_profit_factor > 1.5
AND oos_profit_factor > 1.3
AND oos_win_rate > 50% (Wilson CI lower bound)
AND edge_vs_random > 2.0x
AND n_trades_is >= 30
AND n_trades_oos >= 20
GRADE B (promote with monitoring):
full_profit_factor > 1.3
AND oos_profit_factor > 1.0
AND oos_win_rate > 45%
AND edge_vs_random > 1.0x
AND n_trades_is >= 25
AND n_trades_oos >= 15
GRADE C (archive, do not promote):
full_profit_factor > 1.0
AND (marginal OOS OR thin sample)
GRADE F (killed):
full_profit_factor <= 1.0
OR oos_profit_factor < 1.0
OR oos_win_rate < 45%
OR fails random test (edge < 1.0x)
OR n_total < 20
# VIX REGIME DECOMPOSITION:
# Performance split across: VIX < 16 (low), 16-22 (normal), > 22 (elevated)
# Strategy that only works in one regime = conditional bet, not robust edge
# Optimal regime encoded as production filter
Production State Machine — quant/production_tracker.py
# Strategy lifecycle: PROBATION → ACTIVE → DEGRADED → KILLED / DORMANT
track_live_trade(strategy_name, trade):
# PROBATION PHASE (first 10 trades)
IF n_trades <= 10:
IF n_trades >= 5 AND live_pf < 1.0:
status = KILLED
reason = "Failed probation: PF < 1.0 after 5 trades"
IF n_trades == 10:
status = ACTIVE # passed probation
LOG "PROBATION_PASSED"
# ACTIVE MONITORING
IF n_trades >= 30 AND live_pf < 0.6:
status = KILLED
reason = "Live PF < 0.6 after 30+ trades"
ELIF n_trades >= 20 AND live_pf < 0.8:
status = DEGRADED
reason = "Live PF < 0.8 after 20+ trades"
ELIF n_trades >= 20 AND (backtest_wr - live_wr) > 0.20:
status = DEGRADED
reason = "Live WR dropped 20pp below backtest"
# RESTORATION
IF status == DEGRADED AND n_trades >= 20:
IF live_pf >= 0.8 AND (backtest_wr - live_wr) <= 0.15:
status = ACTIVE # restored from degradation
# DORMANCY
IF days_since_last_signal >= 15 AND status == ACTIVE:
status = DORMANT
reason = "No signals in 15+ trading days"
# State diagram:
# [NEW] → PROBATION(10 trades) → ACTIVE
# ↓ (PF<1.0 @ 5) ↓ (PF<0.6 @ 30)
# KILLED KILLED
# ↑
# ACTIVE → DEGRADED (PF<0.8 @ 20) → KILLED (if persists)
# ↓ ↓ (PF recovers)
# DORMANT ACTIVE (restored)
All Configuration Thresholds
Signal Quality
| Parameter | Value | Source |
|---|---|---|
| ALERT_MIN_CONFIDENCE | 8 | config.py |
| EDGE_DETECTION_THRESHOLD | 85% | signal_logger.py |
| MAX_STOP_PTS | 20 | price_alerts.py |
| MAX_TARGET_PTS | 30 | price_alerts.py |
| MIN_TARGET_PTS | 10 | price_alerts.py (NEW) |
| MAX_DAILY_SIGNALS | 3 | price_alerts.py (NEW) |
| MIN_RR_RATIO | 1.2 | price_alerts.py |
| MAX_RR_RATIO | 3.0 | price_alerts.py |
| PROXIMITY_PTS | 8 | price_alerts.py |
| REJECTION_PTS | 15 | price_alerts.py |
| MIN_PRICE_MOVE | 15 pts | price_alerts.py |
| FLIP_COOLDOWN | 15 min | price_alerts.py |
Orderflow
| Parameter | Value | Source |
|---|---|---|
| CVD_DIVERGENCE_BARS | 3 | config.py |
| ABSORPTION_VOLUME_MULTIPLIER | 2.0x | config.py |
| LARGE_PRINT_THRESHOLD | 100 contracts | config.py |
Deduplication
| Parameter | Value | Source |
|---|---|---|
| PRICE_THRESHOLD | 4.0 pts | signal_logger.py |
| TIME_WINDOW | 15 min | signal_logger.py |
| MAX_SAME_DIRECTION | 2 | signal_logger.py |
Confidence Adjustments
| Factor | Value | Notes |
|---|---|---|
| Intel source match | +2 | Per source, decays after 120min |
| Level confluence | +1 to +2 | Strong level = +2, weak = +1 |
| Pivot confluence | +2 to +3 | +2 base, +1 reclaim/loss bonus |
| All-three-agree | +1 | Triple confluence bonus |
| Counter-trend regime | -4 | Heavy penalty |
| Volatile regime | -1 | Additional penalty |
| Event lockout | -2 | Within 5min of data release |
| VIX extreme | -1 | VIX > 28 |
| Inventory extreme | -2 | Market maker at limits |
Session Weights
| Session | Time (ET) | Threshold Adj |
|---|---|---|
| Pre-market | 6:00-9:30 | +2 |
| Open | 9:30-10:30 | -1 |
| Midmorning | 10:30-12:00 | 0 |
| Lunch | 12:00-13:00 | +3 |
| Afternoon | 13:00-16:00 | -1 |
| Power hour | 16:00-17:00 | -1 |
Market Internals
| Parameter | Value | Source |
|---|---|---|
| TICK_BULL_THRESHOLD | +600 | config.py |
| TICK_BEAR_THRESHOLD | -600 | config.py |
| TRIN_BULL_THRESHOLD | 0.75 | config.py |
| TRIN_BEAR_THRESHOLD | 1.50 | config.py |
| VIX_HIGH_THRESHOLD | 28 | config.py |
| GAP_THRESHOLD | 100 pts | morning_report.py |
Backtest
| Parameter | Value | Source |
|---|---|---|
| SLIPPAGE | 0.25 pts/side | strategy_lab.py |
| COMMISSION_RT | $4.50 (0.09pts) | strategy_lab.py |
| MONTE_CARLO_ITERATIONS | 200 | strategy_lab.py |
| WILSON_SCORE_Z | 1.96 (95% CI) | strategy_lab.py |
| WALK_FORWARD_TRAIN | Months 1-4 | strategy_lab.py |
| WALK_FORWARD_TEST | Months 5-6 | strategy_lab.py |
Production Tracker
| Parameter | Value | Trigger |
|---|---|---|
| Probation limit | 10 trades | First 10 trades monitored |
| Probation kill | PF < 1.0 @ 5 trades | Immediate kill |
| Active kill | PF < 0.6 @ 30 trades | Auto-kill |
| Degradation | PF < 0.8 @ 20 trades | Flag degraded |
| WR degradation | WR drops 20pp vs backtest | Flag degraded |
| Restoration | PF ≥ 0.8 AND WR within 15pp | Restore to active |
| Dormancy | 15 days no signals | Flag dormant |
Last updated: — Auto-generated from codebase audit