{
  "_note": "config.json for the new standalone breakout project (screener/trade), built piece by piece alongside screener/premarket's sip_bot -- see docs/breakout_bot_design_conversation.pdf for the full design. This file has NO runtime relationship to screener/premarket/config.json; the two are read by completely separate processes with separate .env credentials (currently the same paper account, but nothing here imports or reads the other project's files). Only the scanner piece is implemented so far -- monitor/stream/entry/exit sections will be added as each piece is built and checked.",

  "sr14": {
    "enabled": true,
    "lookback_days": 14,
    "tol_atr": 0.25,
    "min_touches": 2,
    "_note": "[2026-09-28] 14-day support/resistance reference (volatility.sr_levels / sr_position): levels from the last 14 daily highs+lows grouped within 0.25 x daily ATR (>= 2 touches); every entry check logs metrics.sr14 = nearest resistance above / support below, distance in % and ATR, position in the 14-day range. RECORDING ONLY -- no buy/sell rule reads it until tested."
  },

  "mode": {
    "execution_mode": "paper",
    "_execution_mode_options": ["simulation", "paper", "live"],
    "_note": "paper = real orders sent to the NEW, independent Alpaca paper account created for this project (see .env) -- separate account from screener/premarket's sip_bot, so the two bots can never collide on positions/buying power. simulation = no orders sent, market data only. live = real money, not recommended until validated."
  },

  "schedule": {
    "timezone": "America/New_York",
    "market_open_time": "09:30:00",
    "market_close_time": "16:00:00",
    "premarket_scan_time": "09:28:00",
    "no_new_entries_after": "15:15:00",
    "force_liquidate_time": "15:55:00",
    "poll_interval_seconds": 5,
    "eod_liquidation_max_wait_seconds": 120,
    "_note": "market_open_time/market_close_time feed market_time.py's minutes_since_open()/session_length_minutes(), which smart_engine.py uses to pace-normalize RVOL. premarket_scan_time/no_new_entries_after/force_liquidate_time/poll_interval_seconds/eod_liquidation_max_wait_seconds are used by monitor.py's daily lifecycle -- same values and same roles as sip_bot's own schedule, not independently tuned yet."
  },

  "universe": {
    "price_min": 5.0,
    "price_max": 15.0,
    "exclude_symbols": [],
    "include_only_symbols": [],
    "min_avg_daily_volume": 500000,
    "exclude_etfs_and_funds": true,
    "exclude_leveraged_inverse": true,
    "max_name_syllables": 5,
    "_note": "Same universe filters as the breakout_bot design doc calls for (Part II section 2), which are themselves the same filters sip_bot already uses -- price $5-$15, 500k avg daily volume floor, OTC/ETF/leveraged-inverse exclusion, max-5-syllable names. No min_daily_atr_pct floor here (unlike sip_bot's config) -- the breakout scanner is deliberately a pure continuous score with no hard reject gates beyond universe membership, per the design doc's philosophy that candidate_score answers 'which stocks deserve attention' not 'which stock will go up'."
  },

  "scanner": {
    "enabled": true,
    "top_candidate_count": 30,
    "rescan_interval_minutes": 30,
    "true_premarket_high": true,
    "_note_true_premarket_high": "[2026-09-24] BUG found: after the open, scanner.scan()'s 6h lookback includes regular-session bars, so rescans/restarts saved the high-so-far-today as premarket_high (58 of 68 rescanned symbols on 9/24). true: the saved premarket_high level uses premarket bars only (scoring unchanged). Recommended ON.",
    "first_rescan_after_open_minutes": 10,
    "rescan_interval_first_hour_minutes": 0,
    "_note_early_rescan": "[2026-09-24] DNA started moving after the open and was only picked up by the 9:58 rescan (+9% already). first_rescan_after_open_minutes (e.g. 10 = 9:40) forces one extra early rescan; rescan_interval_first_hour_minutes (e.g. 15) shortens the interval during the first hour. 0 = off.",
    "_note_rescan": "[2026-09-23] User request: refresh the top-30 list every 30 min during the session (background thread in monitor.py; stops at no_new_entries_after; 0 = off). Dropped symbols are unsubscribed unless a position is open; resistance levels stay from the first scan a symbol appeared in. Rescans write data/candidates/{date}_scanner_HHMM.json; the opening scan file is unchanged (simulate.py replays only that one).",
    "weights": {
      "relative_volume": 30,
      "premarket_volume": 20,
      "gap_or_price_movement": 20,
      "premarket_price_strength": 15,
      "distance_to_resistance": 15
    },
    "rvol_full_credit_multiple": 3.0,
    "premarket_volume_full_credit": 100000,
    "gap_full_credit_pct": 10.0,
    "price_strength_full_credit_pct": 5.0,
    "resistance_full_credit_distance_pct": 8.0,
    "_note": "Weights are the design doc's own explicit split (Part II section 3: 30/20/20/15/15). The four *_full_credit* thresholds are first-cut placeholders carried over from screener/premarket's breakout_scanner.py shadow-run (2026-09-16) -- tune once there are a few days of this project's own scanner output to compare against real outcomes. gap_or_price_movement and premarket_price_strength are scored SYMMETRICALLY (clamped to [-1, 1], not [0, 1]) so an actively declining stock scores worse than a flat one, not the same -- carried over from the same 2026-09-16 bugfix."
  },

  "smart_engine": {
    "_note": "[FEATURE 2026-09-16] The 3-stage live entry decision engine (smart_engine.py) -- Stage 1 'is this worth trading right now' (continuously re-checked, resets Stage 2's timer on any hard disqualifier), Stage 2 'is momentum actually developing' (an 18-20s continuous-hold persistence timer, same mechanic and same real-world-vetted value as sip_bot's fast_entry_gate.py), Stage 3 'is there room to run' (checked fresh the instant Stage 2's hold completes -- nearest resistance above price vs. an ATR-based stop, gated on a minimum reward:risk ratio). Deliberately takes already-computed bars/quote/resistance levels as input and does no I/O itself -- it doesn't know or care whether its inputs came from a live stream, a replay, or a manual test, which is what makes it safe to wire into whatever the future live-monitoring loop looks like without changing this file.",
    "atr_period": 14,
    "trend": {
      "min_flat_threshold_pct": 0.05,
      "flat_threshold_atr_fraction": 0.25,
      "_note": "The 'no one rule fits all' piece: instead of one fixed flat_slope_threshold_pct for every symbol (sip_bot's intraday_health.py's real gap -- it computes ATR per symbol but never uses it to scale anything), the flat/trend boundary here scales with THIS symbol's own ATR%. A naturally quiet, low-ATR grinder (like IIIV, the 2026-09-16 whipsaw case) gets a proportionally tighter band than a naturally violent mover, so a slow grind higher reads as 'flat-but-fine' instead of tripping the same STALE-style read a real reversal would. min_flat_threshold_pct is a floor so an extremely low-ATR name doesn't get an effectively-zero (all-noise-counts-as-a-trend) band."
    },
    "structure_lookback_bars": 4,
    "pullback_max_retrace_pct": 50.0,
    "max_spread_pct": 1.0,
    "min_volume_pace_ratio": 0.4,
    "_note_min_volume_pace_ratio": "Same value and same technique as sip_bot's fast_entry_gate.py's min_volume_pace_ratio (real-volume-so-far / prior-day-total, divided by how much of today's session has elapsed) -- carried over as a starting point since it's already been live-validated there; re-tune independently once this project has its own real fills.",
    "persistence_seconds": 18,
    "max_evaluation_gap_seconds": 15,
    "_note_persistence": "Same values as sip_bot's fast_entry_gate.py (18s hold, 15s max gap before a stale 'since' timestamp is discarded) -- reused as a starting point, not yet independently validated for this project.",
    "atr_multiplier_stop": 3.0,
    "min_reward_risk_ratio": 1.5,
    "clear_air_requires_target": false,
    "clear_air_atr_target_multiplier": 3.0,
    "_note_stage3": "atr_multiplier_stop raised 1.2 -> 3.0 on 2026-09-22 per user request: on 1-minute bars the 1.2x stop sat only 1-3c below entry (e.g. SG 1.9c), inside normal tick noise, once exit.py began enforcing the entry stop_price. Originally 1.2 to match sip_bot's atr_multiplier_initial (risk_manager.py). Not yet backtested at 3.0. clear_air_requires_target/min_reward_risk_ratio/clear_air_atr_target_multiplier are the ORIGINAL reward:risk Stage 3 -- currently INACTIVE (see use_reward_risk_gate below), kept as-is so it can be re-enabled without rebuilding it.",
    "use_reward_risk_gate": false,
    "imbalance_window_seconds": 20,
    "imbalance_buy_threshold": 0.50,
    "imbalance_cross_threshold": 0.30,
    "_note_imbalance_stage3": "[2026-09-17] Stage 3 is now trade-flow imbalance instead of reward:risk, pending more days of live validation (use_reward_risk_gate=true flips back to the original reward:risk gate above). imbalance_window_seconds is how far back stream.py's get_trade_imbalance() looks (matches persistence_seconds' hold window). Two tiers: imbalance >= imbalance_buy_threshold (50%) passes outright, no need to wait for price to cross a resistance level -- order flow alone is confirmation. imbalance >= imbalance_cross_threshold (30%) requires price to have ALSO cleared the nearest REAL resistance level (premarket_high/prev_day_high/range_20d_high -- 'session_high' is deliberately excluded from this check, since it's recomputed live every poll and is never more than a few cents above price during an active rally, i.e. not a real level -- this was Bug #4 from the 2026-09-17 review). Thresholds are a first cut derived from ONE day's 4 real breakout instants (ABSI/SECZ/CRML +30% to +71%, all good moves; USDE -71%, a breakout that immediately faded) -- retune both as more days of data come in, per the user's explicit 'work with the threshold to properly adjust it, with more days testing it.'",
    "min_entry_score": 55.0,
    "entry_score": {
      "weight_imbalance": 0.30,
      "weight_volume_pace": 0.25,
      "weight_volatility": 0.20,
      "weight_slope": 0.15,
      "weight_volume_accel": 0.10,
      "pace_full_credit_ratio": 3.0,
      "pace_zero_credit_ratio": 15.0,
      "atr_full_credit_pct": 0.3,
      "atr_zero_credit_pct": 2.0,
      "slope_full_credit_pct": 0.2,
      "slope_zero_credit_pct": 1.2,
      "vol_accel_full_credit": 1.0,
      "vol_accel_zero_credit": 3.0
    },
    "_note_entry_score": "[2026-09-22] Live entry-quality score (smart_engine._compute_entry_score), checked the instant Stage 3 passes -- distinct from scanner.py's candidate_score, which only runs once premarket and (per the user) only really governs the day's FIRST entries, not slots that free up mid-session. Weights/bands are a first-cut hypothesis built from the SIGN of weak correlations (|r|<0.3, n=39, not statistically significant) between live Stage1/Stage3 metrics and real P/L across 2026-09-18+2026-09-21's actual trades -- NOT a validated model. min_entry_score=55.0 set 2026-09-22 per a simulate.py --min-entry-score sweep (0/40/55/65) against both days: 55 and 65 both beat the min_entry_score=0 baseline on both days (09-18 +$13, 09-21 +$39), 55 chosen as the less-aggressive of the two winners. Still only 2 backtested days and the weights were fit on the SAME trades used to sweep the threshold (in-sample, not proof) -- watch real live results and be ready to revert to 0.0 if it doesn't hold up.",
    "entry_slope_rule": {"enabled": false, "lookback_bars": 5, "mode": "positive"},
    "use_setup_plan": true,
    "entry_mode": "v2",
    "_note_entry_mode": "[2026-09-28] 'j' = setup J (smart_engine stages + trade plan, as through 9/28); 'v2' = the user's entry rules v2 (entry_v2.py, settings in entry_v2 below). Exits are the same either way. setup J full backup: backups/setupJ_2026-09-28/ (RESTORE.sh).",
    "max_extension_from_open": {"enabled": true, "base_pct": 3.0, "step_pct": 1.0, "strong_pace_ratio": 3.0, "strong_imbalance": 0.5},
    "_note_max_extension_from_open": "[2026-09-25] User rule after APPS (bought +5.6% above the open at the high of day): no entry more than 3% above today's open; 4% if volume pace >= 3x normal OR 20s buy imbalance >= +50%; 5% if both. Stage 1 hard reject, every setup. OFF until backtested/approved.",
    "min_stop": {"enabled": true, "style": "k", "k": 0.25, "tiers": [[3.0, 0.6, "low"], [6.0, 1.0, "medium"], [1000000000.0, 1.5, "high"]], "spread_mult": 2.0, "unknown_floor_pct": 1.0},
    "_note_min_stop": "[2026-09-23] Volatility-scaled minimum stop distance (volatility.py). Daily ATR(14)% classifies low/medium/high; floor = max(k x daily ATR or tier % of price, 2x spread); widens the 3xATR stop or the setup_plan stop, never tightens. OFF until backtested.",
    "_note_entry_slope_rule": "[2026-09-23] Enabled per user after backtest (gr 1/4, 3xATR, mes55, 6 slots): 9/18 +322->+467, 9/21 -439->-476, 9/22(partial) -195->-146; 3-day -311->-155; re-buys within 2 min 21->4. Last 5 1-min bars must slope above the ATR-scaled flat band on every read or Stage 1 fails and the 18s hold resets. mode above_zero tested worse (-525)."
  },

  "streaming": {
    "feed": "sip",
    "reconnect_backoff_seconds": [2, 5, 10, 30, 60],
    "max_reconnect_attempts": 20,
    "stale_data_seconds": 120,
    "sub_minute_bucket_seconds": 30,
    "sub_minute_buffer_minutes": 15,
    "_note": "Same shape and same values as sip_bot's streaming config -- stream.py here is a near-verbatim copy of screener/premarket's (it's generic buffering/reconnect logic with no strategy-specific code in it)."
  },

  "setup_plan": {
    "min_reward_risk": 2.0,
    "pivot_bars": 2,
    "ceiling_lookback_bars": 120,
    "ceiling_min_touches": 3,
    "band_cents": 2.0,
    "band_pct": 0.3,
    "breakout_buffer_cents": 1.0,
    "breakout_volume_ratio": 1.5,
    "stop_buffer_cents": 1.0,
    "min_stop_pct": 0.5,
    "min_shares_per_min": 500,
    "pullback_zone_fraction": 0.25,
    "pullback_zone_min_cents": 2.0,
    "time_fit_factor": 1.0,
    "real_level_targets": true,
    "ceiling_target_min_age_min": 15,
    "reclaim_breakouts": false,
    "five_day_levels": {"enabled": false, "max_atr_distance": 3.0, "realistic_target_atr": 1.0},
    "_note_five_day_levels": "[2026-09-25] User's 5-day reference frame. When enabled: the 5-day high becomes a level (breakout, ceiling, target) unless it is more than max_atr_distance daily ATRs above price (spike guard), and the plan target is capped at today's open + realistic_target_atr x daily ATR. Position in the 5-day range and room to the 5-day high are logged in plan metrics either way. OFF until backtested.",
    "_note_dna_fixes": "[2026-09-24] DNA fixes, OFF. real_level_targets: never use high_of_day as the target, and only use a ceiling as a target once its first touch is >= ceiling_target_min_age_min old. reclaim_breakouts: a level closed above earlier, closed back under, and now closing above again on breakout volume with rising lows counts as a breakout.",
    "context_rules": {"enabled": true, "breakout_slope_bars": 5, "pullback_require_reclaim": true, "pullback_max_sell_volume_ratio": 0.7, "pullback_min_imbalance": -0.3, "pullback_require_support": true, "pullback_exempt_lower_highs": true, "swing_lookback_bars": 30},
    "_note_context_rules": "[2026-09-23] User design: slope is context-dependent. Breakout: last 5 bars must slope up (above flat band) + existing 1.5x volume. Pullback: may slope down temporarily if higher low intact, pullback low holds nearest support (VWAP / broken level), red-bar volume <= 0.7x the advance leg, 20s imbalance >= -30%, R:R >= 1.5, and the last bar reclaims (closes above) the prior bar high; Stage 1 lower-highs/lows reject waived while a pullback is live. OFF until compared live (user plans a mid-day switch).",
    "_note": "[2026-09-23] setup_analyzer.py per-tick trade plan (user: replace Stage 3 imbalance + entry_score, min R:R 1.5, plan stop replaces 3xATR). Active only when smart_engine.use_setup_plan is true. min_stop_pct is a placeholder floor until the volatility-scaled floor backtest picks one."
  },
  "exit_v2": {
    "enabled": true,
    "replace_j": true,
    "trail": false,
    "_note_trail": "[2026-09-30, user-approved] trail OFF: on v2's fixed entries (23 days) smooth only +$125.72 (both halves +), smooth+trail +$68.32, trail only +$20.33, J +$140.79.",
    "min_stop_pct": 1.0,
    "atr_mult": 2.0,
    "atr_period": 14,
    "reversal_breakeven_atr": 2.0,
    "smooth": true,
    "smooth_bars": 30,
    "smooth_arm": 0.8,
    "smooth_break": 0.6,
    "_note": "[2026-09-29] user: add the smooth exit and the breakeven stop trail (exit_v2.py). Stop: entry stop (>= 1% below the buy), +R steps (breakeven at +1R), then 2x 1-min ATR trail; first reversal after < 2 ATR run -> buy price. Smooth: armed at R2 0.8 (30 bars), sell under 0.6. replace_j true = the only exits besides the 15:55 sell-all; false = before J's layers. enabled false = J exits only."
  },
  "playbook": {
    "enabled": true,
    "mode": "record",
    "_note_mode": "[2026-09-30 08:5x, user-approved] record only: out-of-sample check (3/2-8/26, 3,670 stock-days) ALLOW +0.00% vs AVOID -0.03% -- no edge; logs every verdict, blocks nothing.",
    "before_group": "wait",
    "group_minute": 45,
    "g1_minute": 50,
    "near_high_pct": 0.66,
    "g2_min_r2": 0.28,
    "g2_max_rvol": 1.48,
    "g3_max_rvol": 2.96,
    "g3_max_vol15x": 2.51,
    "_note": "[2026-09-29] Situation playbook (playbook.py), user-approved to CONTROL buys from 9/30. Each stock gets a group from its first 45 min (10:15 bar): 1 opening drive (ALLOW if still within 0.66% of the high at 10:20), 2 up-pulled-back (ALLOW if R2>0.28 and RVOL<1.48), 3 V recovery (ALLOW if RVOL<2.96 and 15-min vol<2.51x), 4 spike&fade / 5 selling slide / 6 weak drift / 7 flat: AVOID. Before 10:16: WAIT (no buys). ALLOW only lets v2 buy. From reports/open_window/group_study.py, 23 days, in-sample. mode 'record' = log only; enabled false = off."
  },
  "entry_v2": {
    "min_bars": 15,
    "min_rvol": 1.5,
    "max_vol15_x": 5.0,
    "min_room_pct": 1.0,
    "min_buy_share": 0.55,
    "min_r2": 0.7,
    "min_eff": 0.5,
    "max_vwap_pct": 2.0,
    "max_acc5": 1.5,
    "stretch_move_x": 3.0,
    "stretch_vwap_pct": 4.0,
    "confirm_checks": 3,
    "min_stop_pct": 1.0,
    "max_stop_pct": 3.0,
    "max_spread_pct": 1.0,
    "max_above_open_pct": 3.0,
    "breakout_confirm_bars": 2,
    "breakout_recent_min": 30,
    "breakout_max_above_pct": 1.5,
    "breakout_level_age": 15,
    "_note_extension": "[2026-09-29] user: no buy more than 3% over the open unless a confirmed breakout (yesterday's high or today's high as of 15 min ago: last 2 finished bars closed above it, it was broken within the last 30 min, price <= 1.5% over it). null = off.",
    "_note": "[2026-09-28] User's entry rules v2 (dictated): 1 unusual volume (RVOL so far vs 14-day by minute >= 1.5x, last-15-min volume <= 5x normal), 2 room to 14-day resistance >= 1% or above the 14-day high, 3 buy share (buy/(buy+sell), last 15 min) >= 55% and not falling on net selling, 4 steady rise (15-bar smoothness >= 0.7, straightness >= 0.5); price 0..+2% over VWAP; no acceleration spike (5-bar >= 1.5 ATR) or stretched move (>= 3x normal and >= 4% over VWAP); 15 finished bars first; 3 checks in a row. Stop under the nearest 14-day support (1%..3%), else 1%. 14-day references: reference.py (nightly 16:35)."
  },
  "shadow_entry_rules": {"enabled": true, "log_dir": "logs/shadow", "track_outcomes": false, "benchmark_symbols": ["SPY", "IWM"]},
  "_note_shadow_entry_rules": "[2026-09-26] entry_rules.py shadow recorder (user-supplied design, adapted): records volume/order-flow/relative-strength features at decision CHANGES, never trades. SPY/IWM are streamed as benchmarks only. Outcomes are labeled after the close with shadow_outcomes.py.",
  "resistance_stall": {
    "enabled": true,
    "min_touches": 3,
    "band_cents": 2.0,
    "band_pct": 0.3,
    "stall_minutes": 45.0,
    "extension_minutes": 10.0,
    "max_extensions": 1,
    "breakout_buffer_cents": 1.0,
    "breakout_volume_ratio": 1.5,
    "rising_volume_ratio": 1.3,
    "low_ceiling_enabled": false,
    "low_ceiling_minutes": 15,
    "_note_low_ceiling": "[2026-09-25] User rule after EMBC (armed at a 5.845-5.86 ceiling while price sat in a lower 5.81-5.83 box): once armed, find the real ceiling of the last 15 min. No free slot OR position not healthy (exit.py not 'healthy' on the last poll) -> sell when price reaches that lower ceiling. Free slot and healthy -> keep holding for the original ceiling. OFF until tested/approved.",
    "_note": "[2026-09-23] User spec after OPTX: ceiling = highest cluster of >=3 one-minute highs within max(2c, 0.3%), at most one wick above. Clock starts at the ceiling's first touch after entry. Breakout proof = completed bar closing > ceiling + 1c on >=1.5x avg volume (resets the clock). At 60 min: if rising lows or rising test volume, +10 min once; else arm. Armed: market-sell the first poll price >= ceiling bottom, whether above or below entry. exit_resistance_stall.py; runs after giveback-room, before exit.py. OFF until backtested."
  },
  "trading": {
    "max_positions": 5,
    "allow_multiple_entries_same_symbol": true,
    "same_symbol_reentry_cooldown_minutes": 0,
    "account_risk_pct_per_trade": 1.0,
    "max_position_notional_pct_of_equity": 19.0,
    "reentry_after_loss": {"mode": "above_entry", "buffer_cents": 1.0},
    "_note_reentry_after_loss": "[2026-09-23] reentry.py. After a LOSING trade in a symbol: off | block (no more entries in it today) | above_entry (only once price > that trade's entry + buffer). Backtest 09-18..09-23: re-entries after a loss won 21% for -$262; after a win 59% for +$173. OFF until backtested.",
    "min_shares": 1,
    "_note": "max_positions raised 3->6 on 2026-09-18 per user request (originally started smaller than sip_bot's 5 since this was a brand-new, unvalidated engine -- now past that). Position size is risk-based: risk account_risk_pct_per_trade% of equity on the distance between entry and smart_engine's own Stage 3 stop (not a separate risk_manager.py -- that stop is already computed at entry time, reused directly instead of recomputing a second one).",
    "_note_reentry": "[2026-09-18] allow_multiple_entries_same_symbol / same_symbol_reentry_cooldown_minutes are NOT read by any code -- monitor.py._scan_for_entries only gates on is_symbol_open + a free slot, no time-based cooldown ever existed. Left as-is deliberately, not a gap to fix: GLOO on 2026-09-18 exited on the imbalance-decline gate at 4.83, re-entered 96s later at 4.93 once imbalance flipped back positive, and rode it to 5.485+ (+$217 unrealized on the second entry alone, net +$203 combining both GLOO trades that day) -- user explicitly confirmed after seeing this that immediate same-symbol re-entry with no cooldown is the intended behavior, not a bug. These two keys are kept at their now-accurate values (true/0) so they read correctly if anyone wires them up later, but nothing currently consults them."
  },

  "exit": {
    "_note": "[FEATURE 2026-09-16] Deliberately independent of smart_engine.py's entry logic (Stage 1 answers 'should we enter', this answers 'should we remain' -- per the breakout_bot design doc's explicit 'entry and exit must never share one combined decision rule'). Two layers: (1) a hard ATR-based stop, always active, exits immediately no matter what -- pure capital protection, same atr_multiplier_stop smart_engine used to size the stop at entry. (2) a softer 'deterioration' exit that requires a COMBINATION of confirming signals (not just one) to hold true for exit_confirm_reads consecutive polls before firing -- directly answers the 2026-09-16 IIIV lesson: a single flat/quiet read should NOT force an exit on its own (that's what caused the whipsaw), only a genuine multi-signal breakdown, sustained, should.",
    "deterioration_min_signals": 2,
    "deterioration_signals_total": 4,
    "exit_confirm_reads": 3,
    "atr_multiplier_stop": 1.2,
    "_note_signals": "The 4 deterioration signals checked each poll: price lost VWAP, VWAP sloping down, trend classified negative (same ATR-normalized trend check as smart_engine.py's Stage 1), lower-highs/lower-lows structure. deterioration_min_signals=2 of 4 must agree, sustained for exit_confirm_reads=3 consecutive polls, before the soft exit fires -- one blip, or one signal alone, is never enough.",
    "imbalance_window_seconds": 20,
    "imbalance_exit_confirm_reads": 5,
    "_note_imbalance_exit": "[2026-09-17, redesigned 2026-09-18] First-checked signal-based exit gate (right after the hard stop, ahead of the deterioration layer above). Originally checked stream.py's per-completed-1-min-bar imbalance once per bar roll; rebuilt after real 2026-09-17 data (ABSI) showed that version completely missed a real reversal (the bar spanning ABSI's actual price low read the most bullish imbalance of the whole window) while also firing falsely later on pure bar-to-bar noise with no price move behind it. Now reads the same continuous rolling-window imbalance smart_engine.py's Stage 3 uses (stream.get_trade_imbalance(imbalance_window_seconds)) every poll (5s cadence, not once per minute) and fires after imbalance_exit_confirm_reads (5) consecutive polls each read lower than the one before -- roughly a 25s reaction time instead of 5 minutes. Deliberately NOT gated behind price/trend going flat first -- that precondition was tested and would have excluded the one real catch found (trend didn't reclassify until 5 seconds after the fast imbalance streak already completed). Not yet tuned against real exit data -- first cut."
  },

  "giveback_room": {
    "_note": "[FEATURE 2026-09-21] exit_giveback_room.py -- checked in monitor.py BEFORE the exit block above, since it reacts to the position's OWN peak instead of absolute/session-wide readings. Inactive until price actually starts sloping down (same ATR-scaled trend classifier exit.py uses); once sloping down AND the peak has run up at least min_peak_gain_cents, the allowed pullback from peak is peak_gain_cents * giveback_ratio, cents-for-cents, no confirm-read wait. Motivated by the 2026-09-21 peak-vs-exit analysis (17 of 24 trades that day gave back a real chunk of a genuine peak before exit.py's layers ever fired).",
    "min_peak_gain_cents": 15.0,
    "giveback_ratio": 0.25,
    "slope_lookback_bars": 4,
    "min_flat_threshold_pct": 0.05,
    "flat_threshold_atr_fraction": 0.25,
    "atr_period": 14
  },

  "data_storage": {
    "candidates_dir": "data/candidates",
    "positions_dir": "state",
    "trades_dir": "data/trades",
    "decisions_dir": "data/decisions",
    "state_dir": "state",
    "retention_days": 30
  },

  "logging": {
    "log_dir": "logs",
    "level": "INFO",
    "filename_prefix": "trade_bot",
    "retention_days": 30
  }
}
