IdiotProof — Project Bible
Single source of truth for what IdiotProof IS, is NOT, and the rules that keep it coherent. README says how to build/run; this says how to think about the system.
1. The one sentence
IdiotProof turns a plain-English trade idea into a runnable DSL strategy that the Monitor,
running 24/7 inside the IdiotProof Hub, evaluates against live market data, fires only when
every condition matches, an LLM voter panel approves, and the Risk Guardian clears it — then
places the order through the Hub's transaction gateway and manages the position to its exit.
A person (or an assistant on their behalf) drives all of it through the idiotproof CLI, its
MCP tools or the web app. The flagship flow is the Gapper
(§4.4): buy the premarket gap at 4AM, sell it off before the 9:30 bell once momentum
rolls over.
2. The product promise
- Describe, don't code. A trader writes prose ("if NVDA pulls back to the 9 EMA in an
uptrend with volume confirmation, go long with a 1% stop"); Claude (via MindAttic.Legion)
translates it into IdiotScript, the project's fluent DSL. The verb catalog is produced by
reflecting on the real
StrategyBuilder+Conditionstypes, so a model can never invent syntax that does not compile; generated scripts are parse-checked and the user is told when a verb did not parse. - Set and forget.
IdiotProof.Monitoris an unattended console host that re-reads every active strategy from SQL each tick (default 1s,IDIOTPROOF_MONITOR_INTERVALoverrides — UI edits apply to the running console automatically), evaluates against live Alpaca data (real-time SIP websocket stream + REST, Mock fallback), reports per-condition progress (4/5 — waiting on OnReclaim(9)), places gated entries, and manages open positions to their exit. - The Gapper, done well. Queue up to 3 tickers on the
/gappertab, each with a dialable profile (gap %, volume, price band, entry window, stops, peak-giveback, sell-by). All gappers are not the same — every value is per-ticker adjustable; the tuned result is written into the strategy's canonical JSON so what you dialed is exactly what runs. A past day can be replayed with the same brain and its hindsight-tuned dials applied back. - Three gates before money moves. All strategy conditions match → LLM voter quorum approves → Risk Guardian clears stop/daily-loss/per-trade-risk. Any gate blocks the fire and records the reasoning.
- Paper by default, live by explicit opt-in. The Sandbox broker is always the safe fallback; live trading requires explicit configuration plus a confirmation modal.
- Research finds the news for you. A scheduled scanner sweeps SEC filings, Alpaca news and
Federal Register notices across the tracked ticker universe and ranks what it finds by
significance on the
/researchtab — no search box required. - Options without mental math. The manual
/optionssection shows premium, breakeven and the intrinsic ("real") vs extrinsic ("hype") split for every contract, so the trade is "buy the idea, sell the hype".
3. What it is NOT
- Not a charting terminal. It draws static ASCII charts (
market chart,account history) and a web live chart, but it is not a streaming tick-chart workstation for discretionary trading; the ghost-overlay work is planned, not built. - Not multi-broker today. The build is Alpaca-only (plus the simulated Sandbox).
IBrokerClientis the seam a future broker would implement; no other adapter exists. - Not a direct-to-vendor LLM client. No feature code calls an Anthropic/OpenAI SDK directly; all LLM traffic routes through MindAttic.Legion and all keys resolve through MindAttic.Vault.
- Not a gate-bypassing autotrader. Every order, automated or by hand, goes through the
Hub's
TransactionGateway(IP-LAW-12); automated entries clear all three gates, never around the Risk Guardian. Exit orders are risk-reducing: they skip the LLM panel by design but are always audit-logged. - Not an agent that moves real money on its own. An assistant drives IdiotProof through the CLI or MCP, but a Live action only previews and returns a confirmation code; it happens only when the person approves and the code comes back (IP-LAW-13).
- Not an options autotrader. Options orders are manual and user-initiated (
/options, oroptions buy|sell|spread|close|exercisefrom the CLI/MCP); the DSL, Monitor,RiskGuardianandConditionscatalog know nothing about options. - Not a desktop app. Its front doors are the
idiotproofCLI, theidiotproofMCP server and theIdiotProof.Blazorweb app, all over the one Hub (§4); the Hub runs headless as a console window or a Windows Service. - Not a research daemon.
IdiotProof.ResearchScanner(andresearch scan) runs one pass and exits; it is fired by a scheduler job and never shares the Monitor's trading loop. The one long-running research worker, the catalyst intake (§4.4), writes research rows only, is off by default, and holds no broker client. - Not a trained predictor. Nothing in the trading path reads a catalyst row or a formation snapshot yet; the gapper study (§4.4) decides whether anything ever will, and its pre-registered prior is that the honest answer may be "catalyst latency is the edge".
4. Architecture canon
One long-running process, the IdiotProof Hub, owns everything that touches money or state.
Every front door is a thin client over the same reflected operation catalog; nothing places an
order except through the Hub's TransactionGateway (IP-LAW-12).
person ─► Claude / any agent ─┬─► idiotproof (CLI, IdiotProof.Cli) ─┐
└─► idiotproof MCP (IdiotProof.Mcp) ──┤ HTTP 127.0.0.1:5910
person ─► browser ─► IdiotProof.Blazor (same Core services) ────────┤ X-IdiotProof-Key
▼
┌───────────────────────────── IdiotProof.Hub ──────────────────────────────────────┐
│ /api/op ─► OperationDispatcher ─► [Operation] handlers (IdiotProof.Operations) │
│ owner resolution · CommandLedger (redacted) · exit codes 0/1/2/3/4 │
│ MonitorWorker (SupervisedLoop, SQL leader lease) + scanners ── IP-LAW-10 │
│ TransactionGateway ─► shape → route → Mock guard → RiskGuardian → LLM panel │
│ → Live confirm (ConfirmationStore codes) → place → audit │
│ KnowledgeIndex (BIBLE, stories, tickets, XML docs, catalogs) for `docs *` │
└──────────────┬──────────────────────────────┬─────────────────────────────────────┘
│ IdiotProof.Core (AddIdiotProofCore: AppDbContext, repositories,
│ services, UserBrokerResolver, AlpacaApiFactory, Vault, DataProtection)
┌─────────▼─────────┐ ┌────────────────▼───────┐ ┌──────────────────────────┐
│ SQL Server │ │ IdiotProof.Brokers │ │ IdiotProof.DataFeeds │
│ Strategies, │ │ AlpacaApi (raw REST) · │ │ Alpaca REST + stream · │
│ ConditionProgress,│ │ AlpacaBrokerClient · │ │ Mock · Switchable │
│ TradeDiary, Audit,│ │ Sandbox · BrokerRouter │ └──────────────────────────┘
│ CommandLedger … │ └─────────────────────────┘
└───────────────────┘
Scripting (IdiotScript, StrategyJson, StrategyStamp) · Strategies (DslStrategy, backtesters)
Indicators · Shared (RiskGuardian, options math) · Models · Engine (SupervisedLoop, AppSettings)
4.1 Projects (in IdiotProof.slnx)
| Project | Role |
|---|---|
IdiotProof.Hub |
The one long-running process (ASP.NET Core minimal API on 127.0.0.1:5910, IDIOTPROOF_HUB_PORT). Hosts the Monitor (MonitorWorker on SupervisedLoop with the sp_getapplock leader lease; IDIOTPROOF_HUB_MONITOR=0 serves operations without the trading loop) and its scanners, and serves /api/health, /api/catalog, /api/op (dispatch by argv or by name + JSON args), /api/logs and /api/shutdown behind the shared X-IdiotProof-Key (Vault Tokens bucket idiotproof-hub, or IDIOTPROOF_HUB_KEY). Runs in its own visible window that echoes every command, or as a Windows Service installed to run as the user (hub install-service, tools/install-hub-service.ps1) because LocalDB and the Data Protection ring are per-user. |
IdiotProof.Core |
The one service graph every host shares (AddIdiotProofCore): AppDbContext + migrations, repositories, every non-UI service (strategies, gapper, research, options, LLM voting, risk), UserBrokerResolver, AlpacaApiFactory, the TransactionGateway + ConfirmationStore + OrderIntent (IdiotProof.Core/Trading), CommandLedgerRepository + SecretRedactor (IdiotProof.Core/Ledger), MonitorWorker and the replay/scan/dataset engines (IdiotProof.Core/Monitoring), and the static JSON catalogs (IdiotProof.Core/Catalogs). |
IdiotProof.Operations |
Every capability as one [Operation("noun verb")] class with a typed args record ([Argument]/[Option]); OperationCatalog reflects them, OperationDispatcher binds argv or JSON, resolves the owner and maps the result to an exit code. The CLI flags, the MCP tool schemas and docs/CLI_COMMANDS.md / docs/MCP_TOOLS.md are all generated from it. Also the render kit (Rendering/TextTable, KeyValueBlock, Fmt, ET via EasternClock) and the knowledge index (IdiotProof.Operations/Docs). Runs only inside the Hub. |
IdiotProof.Cli |
idiotproof thin client: parses noun verb --flags, POSTs to the Hub, prints the result, exits 0 ok · 1 failed · 2 usage · 3 blocked · 4 confirmation required. Runs help, version, agent bootstrap, agent export, hub status/start/stop/logs/version and users use/whoami itself, so they work with the Hub down. idiotproof.cmd shim at the repo root. |
IdiotProof.Mcp |
idiotproof-mcp stdio MCP server: one tool per operation (noun_verb, schema from the args record), forwarded to /api/op; money tools take a confirmation_code. Resources: the BIBLE, user stories, agent protocol and live catalog. |
IdiotProof.Blazor |
Blazor Server web front door over the same IdiotProof.Core services — Strategies, Strategy Builder (Guided/Script/Describe), Gapper, Research, Options, Learning Center, Backtest, Activity Log, Settings, API Keys, Live chart. MindAttic.Authentication 6.0.0 (Argon2id + pepper, sessions, MFA scaffolding, self-service reset at /forgot-password → /account/reset). New work is CLI-first; the web keeps working (HOUSE-LAW-6). |
IdiotProof.Monitor |
idiotproof-monitor: a backward-compatible shim that starts the Hub (the Monitor lives there now); its operator subcommands forward to the matching Hub operations (--local keeps the old in-process path for replay/scan). |
IdiotProof.Engine |
AppSettings overlay chain, SupervisedLoop, AuditLogger, WorkspaceManager (UI layout state only). |
IdiotProof.Scripting |
The IdiotScript DSL: Stock.Ticker(...), StrategyBuilder, the Conditions catalog, ScriptParser, StrategyJson + StrategyLoader (canonical JSON), StrategyStamp (the ASCII stamp), StrategyHtml, branching algebra, EmaPeriodCollector, GapperProfile + GapperScriptFactory, MarketTime / TradingSchedule (ET clock, trading-day gate). |
IdiotProof.Strategies |
IStrategy + DslStrategy adapter + IndicatorSnapshotBuilder + GapperExitEvaluator (sell-off brain, long and short, calling the shared VelocityExitRule) + StrategyBacktester/BacktestReport (replays sequences) + GapperDayBacktester. |
IdiotProof.Evidence |
Pure statistics and shared market math; references IdiotProof.Models only, so Strategies, Core and Operations can all call it. Gappers/ is the one gapper feature and exit brain (GapperFormationFeatures, TimeOfDayProfile, VelocityExitRule, MinuteBarMath, GapperVolumeRatio) used by the live Monitor, gapper backtest-day and the study (IP-LAW-10); Evaluation/, Search/ (BH, DSR, SPA), EventStudy/, Economics/TradingCosts and the rest back the evidence * operations and the research harnesses (tools/EvidenceSweep, tools/EvidenceRun, tools/GapperStudy). No I/O, no clocks. |
IdiotProof.Indicators |
Pure indicator math: ADX, ATR, Bollinger, CCI, EMA, MACD, OBV, RSI, SMA, Stochastic, VWAP, WilliamsR, CandlestickPatterns. |
IdiotProof.DataFeeds |
IMarketDataFeed (+ GetPreviousCloseAsync): AlpacaDataFeed (REST, sip default with iex fallback), AlpacaStreamingClient (websocket trades + minute bars), MockDataFeed, SwitchableMarketDataFeed. |
IdiotProof.Brokers |
AlpacaApi (raw Trading + Market Data REST: paging, 429 retry, AlpacaApiException), IBrokerClient + AlpacaBrokerClient + SandboxBrokerClient + BrokerRouter + AlpacaOAuthClient. Options-aware (contracts, snapshots, single-leg and multi-leg orders, exercise). |
IdiotProof.Models |
Domain DTOs/enums (the nouns, see 4.2) — incl. AssetClass (Equity, Option, Crypto), OptionContract (OCC), OptionQuote. |
IdiotProof.Shared |
RiskGuardian + RiskGuardianConfig/Result, IndicatorSnapshot, LogMessage, SettingsMetadata, and Options/ (pure math: IntrinsicValueCalculator, BlackScholesCalculator, SellSignalEvaluator, OptionsTradingLevel). |
IdiotProof.UI |
Shared Razor Class Library for the Options components. Presentational only. |
IdiotProof.Settings |
The settings tab as a portable Razor class library, hostable by the web app or MindAttic.Vault.Desktop. |
IdiotProof.ResearchScanner |
One-shot research pass (EDGAR, Alpaca, Federal Register) that writes claims and exits; also reachable as idiotproof research scan. |
Test projects: IdiotProof.Engine.Tests, IdiotProof.Indicators.Tests, IdiotProof.Strategies.Tests,
IdiotProof.Evidence.Tests, IdiotProof.Brokers.Tests, IdiotProof.Core.Tests, IdiotProof.Operations.Tests, IdiotProof.Cli.Tests,
IdiotProof.Mcp.Tests, IdiotProof.Blazor.Tests, IdiotProof.UI.Tests, IdiotProof.Monitor.Tests
(see §6); Cypress E2E lives in tests/IdiotProof.Cypress/.
4.2 Domain model — the NOUNS (IdiotProof.Models, IdiotProof.Shared, IdiotProof.Core/Data)
Candle— one OHLCV bar.TradeSignal— output ofIStrategy.Evaluate; a candidate to fire, carrying the full take-profit ladder.TradeSetup/RiskLimits— decimal-priced inputs theRiskGuardianvalidates.OrderRequest/OrderResult/Position— broker-facing order lifecycle; carryAssetClass+Option?(equity by default).OptionContract(OCC symbol, underlying, expiration, strike, right, multiplier 100),OptionQuote/OptionGreeks(IV/Greeks null when the broker omits them).StrategyDefinition(inIdiotProof.Scripting) — the semantic model: phases + conditions + branches; persisted as canonical JSON (IP-LAW-8). The root definition is step 0 of a sequence;NextStepsholds steps 1..N-1 and is empty for every single-step strategy (IP-LAW-14).GapperFormationSnapshot(inIdiotProof.Evidence/Gappers) — one forming-gapper feature row at one decision instant (gap, premarket volume, time-of-day volume ratio, 1/3/5/15-minute velocity, acceleration, volume rate-of-change, session VWAP, distance to the premarket high, run-up, days since reverse split / 424B / S-1, latest catalyst), plus anExtradictionary for registered literature features.- SQL entities (
IdiotProof.Core/Data, namespaceIdiotProof.Core.Data):CommandLedgerEntry(one row per CLI/MCP call),PendingConfirmation(Live codes),Strategy(owner, author,BrokerMode,ScriptJsoncanon +ScriptTextview,OriginTranscript, open-position bookkeeping),ConditionProgress,TradeDiaryEntry,AuditLog,UserApiKeys(encrypted at rest),UserPreferences,ReplayRun/ReplayTrade/ReplayBar,ResearchClaim,InsiderTransaction,TrackedTicker,BlockedEmailDomain,SettingsKv,Workspaces,CatalystEvent/CatalystEventSymbol(research rows: one news item or filing withPublishedUtcandReceivedUtc, pre-class, LLM fields, score;IdiotProof.Core/Data/CatalystRows.cs). - Enums:
TradeDirection,TradingSession,OrderType,OrderSide,PriceType,ConfidenceGrade,BrokerType {Alpaca, Sandbox},StrategyType {Iti, BreakoutPullback, LowHigh, FluentDsl, Custom},AssetClass {Equity, Option},OptionRight {Call, Put},WorkspaceState.
4.3 Key services — the VERBS
Stock.Ticker(symbol)→StrategyBuilder(IdiotProof.Scripting) — entry point to author IdiotScript.StrategyLoader.Load(scriptJson, scriptText)— the one materialization path: canon first; present-but-rejected canon quarantines; only canon-less legacy rows touchScriptParser.IStrategy.Evaluate(symbol, candles, context)→IReadOnlyList<TradeSignal>(IdiotProof.Strategies).DslStrategy— adapts a parsedStrategyDefinitioninto anIStrategy.IndicatorSnapshotBuilder.Build(...)→IndicatorSnapshot(indicators, previous close/gap, window high/low, pivot higher-low/lower-high) consumed by condition evaluation.StrategyBacktester.Run(...)→BacktestReportincl. the per-candleConditionTable(IdiotProof.Strategies/Backtesting);GapperDayBacktesterreplays a gapper day.RiskGuardian.ValidateTrade(setup, ...)→RiskGuardianResult(the final gate,IdiotProof.Shared/Risk);RecordTradePnL(realized)feeds every exit into the daily circuit breaker;UpdateConfigswaps limits without resetting the daily-loss counter.SupervisedLoop.RunAsync(options, ct)— fault-tolerant tick loop with backoff + heartbeat file.UserBrokerResolver.ResolveAsync(userId, brokerMode)— per-strategy broker routing (§4.4).LlmVotingService.VoteOnSignalAsync(...)— the LLM gate on MindAttic.Legion's voter panel;UserClaudeKeyResolver.ResolveAsync(ownerId)supplies the owner's Claude key (§4.4).DirectionalOrders(IdiotProof.Core/Monitoring) — long/short entry and exit order shapes, held-share reconciliation and direction-aware realized P&L for the Monitor andtrade flatten.TransactionGateway.SubmitAsync(OrderIntent)(IdiotProof.Core/Trading) — the only caller ofIBrokerClient.PlaceOrderAsync/ replace / cancel (IP-LAW-12);ConfirmationStoreissues and redeems Live codes (IP-LAW-13).IBrokerClientviaBrokerRouter(Sandbox is the always-registered fallback; constructed once inTradingServiceRegistration).OperationDispatcher.DispatchAsync(...)(IdiotProof.Operations) — binds a call to its[Operation], resolves the owner, runs it, ledgers it, maps the result to an exit code.AlpacaApiFactory.ForOwnerAsync(owner, mode)/ForDataAsync(owner)— raw Alpaca REST with the owner's own keys, for read operations and the gateway's broker calls.StrategyStamp.ToStamp(definition)/.Decode(text)— the ASCII stamp and its fail-closed paste-back.KnowledgeIndexBuilder.Build()→KnowledgeIndex— thedocs *operations' index.GapperScriptFactory.ToScript(symbol, profile)— tuned profile → round-trip-safe IdiotScript.GapperExitEvaluator.Evaluate/.EvaluateShort— sell-by / stops / take-profit / velocity-exhaustion / peak-giveback verdict for a held position (pure, clock-free, unit-tested).VelocityExitRule.Evaluate(barsSinceEntry, entry, now, ...)→VelocityExitVerdict(IdiotProof.Evidence/Gappers) — "sell when the run's velocity dies"; fails closed (missing input = hold); mirrored for shorts. The one velocity exit for live,gapper backtest-dayand the study.GapperFormationFeatures.Compute(GapperFeatureInputs)→GapperFormationSnapshot(IdiotProof.Evidence/Gappers) — the one no-lookahead feature computation for a forming gapper: trims every input toEndUtc <= titself, forward-fills missing minutes as zero-volume bars so windows are wall-clock (GapperFormationTestsappends post-t bars and asserts nothing changes).CatalystIngestor.IngestAsync(CatalystIntake)(IdiotProof.Core/Catalysts) — one news item or filing → dedupe on (Source, ExternalId) →CatalystPreClassifier.Classify→ novelty count →CatalystScorer.Score→CatalystEventrow; live intake and backfill both come through it.IMarketDataFeed.*— Alpaca (REST + websocket stream), Mock, Switchable;GetPreviousCloseAsyncsupplies gap math's reference close.StrategyRepository— the only writer of strategy rows; owns the mutation guards (IP-LAW-11).- Research subsystem (
IdiotProof.Core/Services):TickerUniverseService,EdgarService,Form4Parser,CorporateActionDetector,RegulatoryScanner,IndexEventScanner,CatalystExtractor,OutcomeBackfillService,SignificanceScorer,ResearchService(§4.4).
4.4 Subsystems
The Hub and its front doors. The Hub is the only process that opens the database for
trading, holds broker clients and places orders. The CLI and the MCP server hold no state: they
send {argv | name + args, owner, output, confirmation code} to /api/op with the shared key and
print or return the envelope (status, exit_code, message, data, confirmation). The owner
comes from --user, else the CLI's saved default (users use), else IDIOTPROOF_OWNER, else the
only user; it is never guessed among several. Every call writes one CommandLedger row (caller,
argv with secrets redacted by SecretRedactor, outcome, duration) that the gateway's AuditLog
rows link to. Operations are discovered by reflection, so adding an [Operation] adds a CLI
command, an MCP tool and a docs entry at once; CatalogCompletenessTests proves each one is
documented, has examples that bind and survives a JSON and an argv round trip.
idiotproof agent bootstrap (runs without the Hub) tells a fresh agent the Hub's health, its
owner, the catalog and the etiquette in docs/agent/IDIOTPROOF_PROTOCOL.md;
idiotproof agent export regenerates docs/CLI_COMMANDS.md, docs/MCP_TOOLS.md and
docs/agent/operation-catalog.json.
The transaction gateway (the money path). TransactionGateway.SubmitAsync(OrderIntent) is
the one path to a broker for every order-like action: Monitor entries and exits, CLI/MCP orders,
replaces, cancels, position closes and flattens, options and crypto orders, the web Options
ticket (IP-LAW-12). Gates in order, failing closed: shape → route by BrokerMode
(Sandbox default, Live never downgrades, IP-LAW-3) → Mock-feed guard
(IP-LAW-9) → Risk Guardian for entries (IP-LAW-2) → LLM panel for
automated entries (IP-LAW-1) → Live confirmation → place → audit (+ TradeDiary).
A Live action from the CLI or MCP returns exit 4 with a preview and a 6-character
ConfirmationStore code bound to the SHA-256 of the exact intent, single-use, 120 s, same owner
(IP-LAW-13); the web confirms with the 5-minute password elevation instead, and the
Monitor needs none (activating a Live strategy was the confirmed act, and itself needs a code from
the CLI). Non-order Live mutations that move money (crypto withdrawals, whitelist changes) use the
same codes; Live account-config and strategy-mode changes too. Raw Alpaca access for read
operations goes through AlpacaApiFactory.ForOwnerAsync(owner, mode) / ForDataAsync(owner)
(the owner's own keys from Vault); docs/ALPACA_COVERAGE.md maps every
Alpaca endpoint to its operation.
The strategy stamp. StrategyStamp (IdiotProof.Scripting/StrategyStamp.cs) renders a
strategy as a deterministic 72-column card generated only from its canonical JSON
(IP-LAW-8): header (symbol, side, size, session, name, short id IP-XXXX-XXXX from
the canon's SHA-256), the six-phase flow with branches, the exit ladder, a drunken-bishop
fingerprint, and a BEGIN IDIOTPROOF STRATEGY armored payload (deflate + base64url + CRC-32).
strategy stamp <id> prints it; strategy import reads the armor back out of pasted text,
fails closed on any damage, and creates the strategy inactive in Sandbox through the repository
(IP-LAW-11). Same canon → byte-identical stamp. A sequenced strategy renders one
STEP k/N header per step with a THEN WHEN connector card between steps; a single-step
strategy's stamp is unchanged (StrategyStamp.cs, StrategyHtml.cs).
IdiotScript Steps (sequenced strategies, IP-T445). One strategy can be written as ordered
Steps, each with its own direction, entry conditions, risk and exits, so "short NVDA into
earnings; once covered and price reclaims, go long" is ONE strategy, not two rows and an external
arm/disarm. The root StrategyDefinition is step 0; Step("name") right after the setup verbs
(Ticker, Name, Session, Quantity*) names it, and any later Step("name") closes the
current step and opens the next, seeded with the root's ticker and session
(IdiotScript.cs, StrategyBuilder.Step). ThenWhen(condition) is the transition out of a step:
evaluated only while flat after that step's exit; when every ThenWhen condition holds the next
step is armed; a step with no ThenWhen arms the next step as soon as it is flat. Within(days)
is the deadline: if the transition has not fired within that many days of the step's exit the
sequence ends. Repeat / Repeat(n) is root-only (the parser treats one inside a later step as
the root's, diagnostic IPS022): a cycle is one full run of the sequence, and the sequence restarts
at step 0 when its last step exits or a Within expires. A step never nests steps. In the canon
the keys stepName, thenWhen, thenWithinDays and nextSteps are written only when used
(nextSteps only on the root; StrategyJson.RequireOnly rejects it inside a step), so every
pre-existing strategy's JSON, stamp and script stay byte-identical — goldens for the seven samples
plus the gapper, velocity, branching and looped canons are asserted in DslSequenceTests
(IP-LAW-8, IP-LAW-14). ScriptDiagnostics adds IPS020 (ThenWhen
without a readable condition), IPS021 (Step inside If/While), IPS022 (Repeat in a later
step, warning), IPS023 (duplicate step name), IPS024 (a later step without Long()/Short()),
IPS025 (ThenWhen/Within with no following Step). Two worked examples
(DslSequenceTests.cs):
Ticker("NVDA").Step("short-into-earnings").Session(IS.RTH).Quantity(50)
.IsBelowVwap().IsRsiBearishDivergence().Short().StopLossPercent(3).TakeProfitPercent(6).SellBy("15:55")
.ThenWhen(HigherLows(3).And(RequireEmaStack(9, 21)).And(OnVwapReclaim())).Within(7)
.Step("buy-back").Quantity(50).IsAboveVwap().IsEmaAbove(9).Long().StopLossPercent(2).TakeProfitPercent(5)
Ticker("GAPT").Session(IS.PREMARKET).Step("ride")
.RequireEntryWindow("04:00", "09:00").IsGapBetween(5, 20).IsVolumeAbove(2).IsPriceBetween(1, 50)
.Long().QuantityNotional(1000).StopLossPercent(5).PeakGiveback(25, "09:35").SellBy("11:00")
.ThenWhen(IsLowerHigh().And(IsBelowVwap())).Within(1)
.Step("fade").RequireEntryWindow("09:35", "11:30").IsBelowVwap().VelocityBelow(0.2, 3)
.Short().QuantityNotional(1000).StopLossPercent(4).TakeProfitPercent(8).SellBy("15:55").Repeat(2)
Shipped: the scripting model, builder verbs and parser (StepReader beside BlockReader in
ScriptChain.cs), the canonical JSON, diagnostics, stamp/HTML/strategy explain, EmaPeriodCollector
recursion, StrategyBranchResolver, and the backtester (StrategyBacktester.cs: per-step condition
latches, the flat transition wait, Within expiry, Repeat restarting at step 0, BacktestTrade.Step
and report.StepTransitions, across multi-day candle lists; StrategyBacktesterSequenceTests).
In progress (IP-T445): the Monitor half — Strategy row step state (StepIndex,
AwaitingTransition, StepArmedUtc), StrategyRepository advance/reset, the per-tick transition
block in MonitorWorker, strategy show's sequence: step k/N row, strategy step <id> --reset,
and one builder card group per step. Until it lands the Monitor evaluates a sequence's step 0 as a
plain strategy and never arms a later step; only the backtester runs a sequence end to end.
Knowledge. The Hub indexes the BIBLE (by section and law), AMENDMENTS, README, the agent
docs, every user story and ticket (the USER_STORIES table plus tickets ticket list --json,
read-only), every library's XML doc comments with their source path:line, and the static
catalogs (gapper profiles, index events, the Legion panel, AppSettings). docs search, docs law, docs section, docs story, docs explain, docs ticket and docs dsl answer from it;
the MCP server also serves the BIBLE, stories, protocol and catalog as resources.
The Monitor tick (one pipeline, SQL as the bus). Strategy rows are the only strategy
runtime state; the Gapper and Strategies pages write them, the Monitor reads them each tick.
Per tick: trading-schedule gate (TradingSchedule.Classify; weekends and outside-session
moments never enter) → re-read active rows → candles from a rolling per-symbol cache (REST,
topped up by the websocket stream; empty windows and missing previous closes negative-cached
for 30s) → load canon → resolve branches → manage an open position or walk entry conditions →
upsert ConditionProgress → on a full pass, LLM gate then Risk gate → place the entry. Premarket
and after-hours entries are limit + DAY + extended_hours; regular-hours entries are a
marketable limit. Longs and shorts both trade: a long buys at +0.2% and exits with a
sell_to_close at -0.5%; a short opens with a sell_to_open at -0.2%, is managed by
GapperExitEvaluator.EvaluateShort, covers with a buy_to_close at +0.5%, and its realized P&L
is inverted. On the wire (AlpacaBrokerClient) a short entry is side: sell +
position_intent: sell_to_open and a cover is side: buy + position_intent: buy_to_close, with a
positive qty (the side carries the direction); null fields are left out of the body, so a long
entry sends no intent. Alpaca opens shorts only in shortable, easy-to-borrow names on a margin
account with shorting enabled (equity of at least $2,000); any other account gets
403 40310000 "account is not allowed to short", which the Monitor sees as a failed entry. Before any exit order the Monitor reconciles its bookkeeping with the broker's
position on the strategy's own side (a short is a negative broker quantity; no broker position → phantom bookkeeping cleared, no order; after a 90s grace for a
still-working entry); when several of a user's strategies share a symbol, aggregate
reconciliation is skipped and per-strategy bookkeeping is trusted. A sell-by position that
outlived its entry's ET day flattens at the first evaluated instant; exit orders outside a
weekday 04:00–20:00 ET window defer visibly. Every buy/sell writes a TradeDiary row
(denormalized, FK-free, log-and-continue).
The LLM gate. LlmVotingService puts the candidate to MindAttic.Legion's voter panel
(VoteWithProfilesAsync): every legion.json voter (claude, openai, gemini, deepseek) that has a
key, each through a rotating trading lens (Risk Manager, Momentum Trader, Technical Analyst),
casts an Approve/Reject/Abstain ballot; Legion snaps answers to the ballot and refills failed
seats from providers that answered. The Claude voter uses the strategy owner's key from the API
Keys page (UserApiKeys, decrypted through UserKeyService), falling back to the host key
(AppSettings.ClaudeApiKey: Vault own-scoped idiotproof-claude, then shared claude, then
env/config); the other voters use the shared MindAttic LLM keyring. An owner row that cannot be
read falls back to the host key, never to another user's. A key saved on the API Keys page is
stored only on that user's row; no user page writes the host key, which changes only through
Vault or configuration (the app has no admin path for it). The interactive Claude features use
the same rule for the signed-in user: the Describe tab (StrategyScriptGenerator), the Gapper
transcript reader (GapperInterpreter) and the Research page's analysis (ResearchService →
CatalystExtractor, ClaimVectorService) get the key from IClaudeKeyContext
(OwnerClaudeKeyContext over the signed-in owner in the web app, over the operation's owner in the
Hub; resolved on every call, never cached) and pass it per call; with neither a user nor a
host key they show UserClaudeKeyResolver.NoKeyMessage. The scheduled research scan has no
signed-in user and uses the host key. Voting is on when the host
(LlmVotingEnabled) or the owner enabled it. Approve needs LlmConsensusThreshold (default
0.66) of the counted votes, each weighted equally.
Broker routing. Each strategy declares BrokerMode (Paper | Live | Sandbox).
UserBrokerResolver sends Paper/Live to the owner's own Alpaca account only when the owner has
opted in on the API Keys page (DefaultBroker = "alpaca") and a key pair for that mode exists
(MindAttic.Vault Brokers bucket first, then the DB-encrypted pair); otherwise, and always for
Sandbox, it returns the global router's active broker (Sandbox by default). Clients are cached
per (user, mode) for 5 minutes and rebuilt when the key fingerprint changes. The Monitor and the
Blazor host share one Data Protection key ring (app name "IdiotProof"; dev
%APPDATA%\MindAttic\DataProtection\IdiotProof; prod either Azure Blob + Key Vault via
DataProtection:AzureBlobUri + DataProtection:KeyVaultKeyUri, or a durable
DataProtection:KeyRingPath; production with neither fails closed) so the console can decrypt
the keys the UI writes. Market data is one global feed (sip by default,
IDIOTPROOF_ALPACA_FEED=iex for the free tier).
The Gapper. Profiles are a static JSON catalog
(IdiotProof.Core/Catalogs/gapper-profiles.json, IP-LAW-7) of templates:
screen (gap %, volume ratio, price band), ET entry window (default 04:00–09:00), stop and
trailing stop, peak giveback + arm time (default 09:15), hard sell-by (default 09:28), notional.
DSL verbs: RequireEntryWindow/EntryWindow, IsGapUp/IsGapBetween (fail closed without a
previous close), PeakGiveback(pct, arm), SellBy(time),
VelocityExhaustion(pctPerMin, windowMin = 3, armAtEt?) with the refinements
VelocityExhaustionMinRun(pct), VelocityExhaustionRequireLowerHigh(),
VelocityExhaustionRequireVwapLoss() and VelocityExhaustionExtensionSwitch(pctAboveOpen);
entry conditions VelocityBelow(pctPerMin, windowMin), VelocityAbove(...), Decelerating(),
VolumeRocBelow(ratio) read the Velocity1m/3m/5m, Acceleration and VolumeRoc fields
IndicatorSnapshotBuilder fills from MinuteBarMath (window 1, 3 or 5 clock minutes; null =
fail closed). Momentum-rollover exit: track the post-entry peak; once armed, sell when price
gives back N% of the entry→peak run; SellBy always flattens before the bell. The exit ladder
(GapperExitEvaluator.Evaluate, first match wins): SellBy → hard stop → trailing stop →
take-profit targets, prior HOD, rolling highs/lows → VelocityExhaustion (the shared
VelocityExitRule: the favorable velocity over the window fell under the configured %/min, after
the optional arm time, minimum run, lower-high and VWAP-loss gates; above the extension-from-open
switch it stands down and the trail takes over) → PeakGiveback; EvaluateShort is the mirror
(a higher low instead of a lower high, a VWAP reclaim instead of a loss), in the same order. The
time-of-day volume ratio (IndicatorSnapshot.VolumeRatioTod, GapperVolumeRatio.Compute over a
TimeOfDayProfile) is opt-in: without a profile it is null and IsVolumeAbove keeps its 20-bar
baseline; AutoGapperScanner still screens on cumulative volume / ADV until the study ships
data/tod-profile.json (VelocityExhaustionTests, GapperFormationTests). IdiotProof.Evidence/Gappers
is the one code path: the live Monitor, gapper backtest-day and the study compute velocity,
features and the velocity exit from the same classes (IP-LAW-10). At most 3
active gappers per user and one active gapper per
symbol, enforced in SQL. The "From a transcript" panel (GapperInterpreter, via Legion) turns
natural language into candidate cards that are re-validated fail-closed and queued only by a
human click. "Backtest a day" (GapperDayBacktester) walks the same condition list and the same
GapperExitEvaluator as live, reports MFE/MAE, a giveback grid and hindsight suggestions, and
offers a tuned profile the user applies manually.
Replay, scan and dataset (replay *, scan movers, dataset export). replay walks a past ET session's Alpaca bars
through the live evaluator and exit brain (shared MarketTime.IsInsideSession gate) and reports
each round-trip; tickers without a saved strategy use a gapper profile or a built-in family
(momentum, reversal, emabreak, rthdrive, rsireversal, swingreversal, shortfade).
Each run is a ReplayRun row with normalized ReplayTrade/ReplayBar feature rows; the HTML
archive is a view regenerated from SQL (replay-regen); scan pulls Alpaca's movers and replays
each gapper; replay-export writes ML-ready CSVs from the feature tables. Replay never places
orders. The old idiotproof-monitor operator commands are operations now (monitor status,
keys set, strategy from-watchlist, users create, trade flatten, strategy resync-canon,
gapper auto *, gapper premarket-fade); the shim forwards to them.
Research. IdiotProof.ResearchScanner runs one pass: watchlist tickers plus a rotating
batch of the tracked universe (TrackedTicker, refreshed daily from Alpaca's asset list); real
Form 4 transactions (InsiderTransaction); 8-K item-code triage with real document fetch only
for 1.01/2.01/3.02/3.03/5.03; Federal Register SRO notices triaged by an LLM into macro claims
(IsMacro = true, affected tickers only when resolvable — never fabricated); S&P index
add/remove events from the hand-maintained IdiotProof.Core/Catalogs/sp-index-events.json
(ClaimType = "IndexEvent", Pending until effective). Claims are deduped by (ticker, source
URL); display sentences are composed deterministically from structured fields
("{Summary}. Affects {Ticker} because {Mechanism}. Expected impact: {ExpectedTimeline}.");
OutcomeBackfillService marks old-enough claims Realized/Disproven from real price history
before SignificanceScorer ranks. The recurring pass runs as a MindAttic.Scheduler (WatchDog)
job (M.SCHD-T8); IdiotProof itself registers no scheduled task.
Catalyst intake (IP-T443, in progress: formation recorder, voter context). The Hub hosts a
leader-only CatalystIntakeWorker (IdiotProof.Core/Catalysts, registered beside the Monitor's
workers in MonitorServiceRegistration.cs, in the shape of ExhaustionScanWorker) that turns a
published catalyst into a timestamped, scored SQL row the Monitor can read per tick — SQL is the
bus (IP-LAW-10); it is not the batch scanner above and never runs inside
MonitorWorker.TickAsync. When on, it keeps one Alpaca news websocket open (v1beta1/news,
["*"], the owner's data keys, AlpacaStreamSession now in IdiotProof.DataFeeds/Streaming) and,
with catalyst.intake.edgarPolling, polls EDGAR's latest-filings feed every 60 s for 8-K/4/S-1/424B
with the acceptance time from each filing's index headers, through a 120 ms EdgarRateGate and a
daily-cached CIK→ticker map. Every CatalystEvent carries PublishedUtc (the source's time) and
ReceivedUtc (this process's time), so catalyst latency is measurable; a backfilled row has
ReceivedUtc == PublishedUtc and Backfilled = true. CatalystPreClassifier is deterministic
and LLM-free, driven by IdiotProof.Core/Catalogs/catalyst-preclass.json (IP-LAW-7):
offering form prefixes → 8-K item codes and combos → ordered keyword rules → a promotional-wire
test, into {Fda, ContractAward, ReverseSplit, OfferingDilution, Uplisting, Earnings, MA, PromoPr,
Other} with a direction (-1/0/+1), a confidence and the rule that fired; SourceTier is 1
EDGAR, 2 editorial, 3 wire/promotional. CatalystScorer is pure:
clamp((TypePrior × Magnitude × SourceWeight + NoveltyBonus) × Freshness(ageMinutes), 0, 100),
direction carried beside the score (a dilution scores high and bearish), weights in
catalyst-scoring.json as placeholders until the study sets them, Explain for the breakdown.
CatalystClassifier is the optional LLM half: a bounded channel of event ids that Haiku via
MindAttic.Legion (CatalystExtractor) classifies in the background, writing the Llm* columns
and re-scoring; headline and body travel in the user message only, never the system prompt;
gated by catalyst.intake.llmClassify. CatalystBackfillService replays Alpaca news (paged, by
date range) and EDGAR filings by date range through the same ingestor, pre-classifier and scorer,
so history and live rows agree. Operations: catalyst list|show|score|intake-status|backfill.
Everything is off by default behind catalyst.intake.enabled|ownerUserId|edgarPolling|llmClassify
(SettingsKv, CatalystIntakeSettings), needs an owner GUID with Alpaca data keys, audits
catalyst-intake-start|stop|error, and backs off 5 minutes after a refusal. The worker holds no
broker client and places nothing. Nothing in the trading path reads these rows yet: the voter
panel still sees only candles. Not built: the live GapperFormationWorker (04:00–09:45 ET
snapshot recorder), the formation-trigger.json arming rule, and the CATALYST/MOMENTUM facts in
LlmVotingService.BuildSignalContext.
Gapper study (IP-T442, in progress). Pre-registered 2026-11-06, before any bar, article or
filing was pulled: docs/research/gapper-study-2026-11/protocol.md (frozen), hypotheses.json
(H1–H17, 12 primary, frozenUtc set), params.json (every frozen number) and literature.md
(the ~70-source review). It asks three questions on one sample of sub-$20 names with ADV20 ≥ $300k
(candidate vs Tier20/50/100 gapper days, seeded-hash controls): what separates the gappers from the
near-misses, whether a gapper can be recognized while it is forming (premarket volume against a
time-of-day baseline, velocity, a catalyst flag, at the 13 decision times from D−1 16:00 to 09:45
ET), and when velocity dies relative to the high of the day (H4/H5/H13/H14 on exit timing,
descriptive only, never "significant"). H11 is the catalyst-latency benchmark: the feature
detector must beat "a news item was published within 60 minutes" by ≥ 0.05 AUC on validation or
strategy work stops and the write-up says "catalyst latency is the edge" — the honest prior,
stated in the protocol so it cannot be softened later. Families F1–F5 with BH/Holm/SPA/DSR
corrections and a frozen cost model (Corwin-Schultz spread × a once-calibrated premarket
multiplier, square-root impact, 0.5% floor). Splits: train 2021-11-01..2024-10-31, validation
2024-11-03..2025-10-31, holdout 2025-11-03..2026-10-30, opened once: HoldoutLock
(IdiotProof.Core/Research/Gappers) refuses holdout labels unless holdout-unlock.txt holds the
hash of the commit that froze hypotheses.json/params.json, writes holdout-run.json before the
first holdout row, and refuses a second run; no force flag. Live gate: a rule trades real money
only after it nets ≥ +0.5%/trade on the holdout with the day-clustered lower bound > 0 and 20
paper-shadow trading days whose live snapshots agree with the backtest's (same code, so drift is
data, not logic); the short leg is studied but not built until the long exit holds up. Features
and labels come from the shared IdiotProof.Evidence/Gappers code; the stages
(research gappers build-universe|pull|catalysts|snapshot|label|status, harness
tools/GapperStudy, minute bars in a FileBackedBarStore under D:\IdiotProofData\gapper-study,
panel tables in a _Research_ database the harness refuses to run without) are landing under
IP-T442. The prior evidence sweep (IP-T432/T437, docs/research/evidence-sweep-2026-11.md)
falsified intraday timing signals on 20 already-volatile names and never studied gap events; this
study reuses its discipline (pre-declared sample, one stage per process, research-DB guard,
verdict rule fixed in advance).
Permanent market history (IP-T454). Everything fetched is kept so nothing is fetched twice:
IdiotProof_History is its own database on the local SQL Server Developer instance (LocalDB's 10 GB
cap is far too small for years of minute bars). Minute bars sit in an ordered clustered columnstore;
daily bars, splits, news (with every tagged symbol) and SEC filings in rowstore tables; Coverage
and CoverageRanges record what was fetched, empty days included. The schema is versioned SQL in
HistoryDatabase.Steps. HistoricalBarStore reads through IBarCache: FallbackBarCache puts the
history database first and the app database's own cache second whenever the history server does not
answer (IDIOTPROOF_HISTORY_STORE=app opts out). HistoryBackfillService queues every ticker the app
meets (strategies, tracked tickers, gapper candidates) for five years of daily and 1-minute
extended-hours bars plus its news, runs jobs one at a time month by month through the same
read-through store, and queues the last session nightly for every held symbol; the Hub's
HistoryBackfillWorker runs it when history.backfill.enabled is true (leader-only). Ops:
history status, history import --research --minutes --app-cache, history backfill --symbols|--discover [--run N]. Backups: IP-T449.
How to research a feature. docs/RESEARCH_PLAYBOOK.md records this
study's method as the template for any feature that rests on a market claim: survey, pre-register,
locked holdout, look-ahead audit, shared live/backtest code, counted corrections, amendments before
results, train-only exploration with a trial ledger, honest reporting, paper shadow before live.
Study amendments (registered before any test on study data; frozen files never edited). Each
amendment-NN.json/.md beside the protocol is committed before its tests run, loaded in order by
GapperAmendment.LoadAll, and corrected as its own set so no frozen q or Holm p moves.
Amendment 01 (IP-T448) fixes a selection problem in the frozen design: H1, H9, H11 and H12 are
asked only on candidate days, which are chosen by the day's own outcome, so they overstate what a
live 08:00 scan achieves. A seeded 10% SHA-256 sample of non-candidate universe days (weight 10)
gets the candidates' data tiers, and H1p/H9p/H11p/H12p re-ask the same questions with the same
thresholds over the whole universe (weighted primitives in IdiotProof.Evidence/Evaluation/WeightedPopulation.cs).
Sample-only days are snapshotted only at the three decision times those hypotheses read, to fit
LocalDB's 10 GB cap. Promotion needs the frozen conditions and H11p/H12p. Amendment 02
(IP-T450) tests the catalyst-latency edge as a trade: H18 buys the first realtime premarket news item
second-hand (1 or 5 minutes after publication) and exits by 09:15, 09:28 or 09:45 through the
frozen simulator and cost model; H19 measures how much of a news-driven gapper's hour comes before
the headline (does someone trade ahead of it); H20 measures how fast the second-hand edge decays.
H18 is not stopped by the H11 kill criterion, because it is how that edge would be traded, but it
reaches paper trading only through the same validation, holdout and paper-shadow gate.
Options (manual). /options (Options.razor + OptionsTradingService) offers single-leg
calls and puts on Sandbox, Paper or Live, defaulting to Sandbox unless Alpaca routing is opted
in and keyed. The chain spans today to AlpacaBrokerClient.ChainHorizon (3 years). Pricing math
is pure (IdiotProof.Shared/Options): intrinsic/extrinsic/breakeven/DTE, Black-Scholes value +
implied-vol solver (European exercise, no dividends), and the informational
SellSignalEvaluator (extrinsic within 5% of its observed high after ≥3 samples and a
Bullish claim on the underlying in the last 7 days). Orders are whole contracts, DAY, no
extended hours, Market/Limit; the ticket locks per action by OptionsTradingLevel
(0 none · 1 covered only · 2 long calls/puts · 3 spreads) and Live needs the 5-minute password
elevation. Jargon has one source, OptionsGlossary. A user-initiated options order is outside
the Monitor, so the three gates do not apply to it (IP-LAW-1).
Accounts. Registration and the CLI create-account reject malformed and disposable email
domains (BlockedEmailDomain, seeded at startup). Self-service password reset is the
MindAttic.Authentication token flow: /forgot-password (MaForgotPassword) emails a single-use
link to MindAttic:Auth:Reset:PublicBaseUrl + /account/reset (MaResetPassword); the base URL
is https://localhost:65025 in appsettings.Development.json and the web app's own
azurewebsites.net origin in infra/main.bicep. The username listing exists in Development
only. /login forwards only same-site return URLs. The SignalR
TradingHub requires authentication. AlpacaOAuthClient + /connect/alpaca store a scoped
token, which is not yet used for order routing.
5. The Laws
This bible inherits the org-wide laws in
MindAttic.HouseRules.md by reference — they are not restated
here. Applicable house laws: whole-number versioning [see HOUSE-LAW-1], soft-disable over
hard-delete [see HOUSE-LAW-2], credentials via MindAttic.Vault [see HOUSE-LAW-3],
provider-agnostic LLMs via MindAttic.Legion [see HOUSE-LAW-4], one engine / many front doors
[see HOUSE-LAW-6], verified definition of done [see HOUSE-LAW-8], psst only on explicit
request [see HOUSE-LAW-9].
Project-specific laws below.
Three gates, in order, before any automated fire
A candidate signal fires only if: (1) every strategy condition matches, (2) the LLM voter
quorum explicitly approves, (3) the RiskGuardian clears it. Any gate blocks the fire and the
reason is recorded to the audit trail. Every gate fails closed: a condition whose inputs are
absent or whose type is unrecognized blocks; zero votes, abstain-only, unparseable votes or a
below-threshold split block (a vote defaults to Abstain). The LLM gate is skipped only when
voting is off for both the host and the strategy owner, or no Claude key resolves (the owner's,
then the host's). Exits are risk-reducing and skip the LLM
panel but are audit-logged and honor the Risk Guardian kill-switch. User-initiated manual orders
(the Options section) are not automated fires; they are governed by the Paper/Live consent rule
and Live password elevation instead. (Risk gate: RiskGuardian* tests; LLM gate:
LlmVotingServiceTests, UserClaudeKeyResolverTests; condition layer: ConditionFailClosedTests.)
Risk Guardian holds the final veto
No order is placed without a stop loss on the correct side, with risk within MaxLossPerTrade,
sized so the worst case cannot exceed the limit, within MinStopLossPercent/MaxStopLossPercent
(default max 10%), within MaxAccountRiskPercent, and under the daily-loss circuit breaker. It
can veto regardless of strategy or LLM consensus. Limits are user-editable on the Settings page;
config refreshes on a 2-minute TTL without resetting the daily-loss counter.
(IdiotProof.Shared/Risk/RiskGuardian.cs.)
Sandbox is the always-safe default broker
BrokerRouter is seeded with BrokerType.Sandbox as the active broker and falls back to
Sandbox rather than throwing or silently routing to a live broker. Routing to a real account
needs the owner's explicit opt-in plus a key pair; a missing or undecryptable key falls through
to Sandbox, never into another user's account. Live trading is an explicit opt-in with a
red-outline confirmation. (IdiotProof.Brokers/BrokerRouter.cs,
IdiotProof.Core/Services/UserBrokerResolver.cs.)
The verb catalog is reflected, never hand-listed
StrategyScriptGenerator builds the LLM system prompt — and the Learning Center renders its
phase/verb reference — by reflecting on the real StrategyBuilder + Conditions types so the
documented/prompted DSL can never drift from the code that actually compiles.
(IdiotProof.Core/Services/StrategyScriptGenerator.cs.)
The Monitor loop survives its own failures
SupervisedLoop catches per-tick exceptions, applies capped exponential backoff, resets on the
next success, writes a heartbeat file each tick, and exits cleanly only on cancellation — the
unattended evaluator never dies on a single bad evaluation. (IdiotProof.Engine/SupervisedLoop.cs.)
No underscore-prefixed private fields
Private fields use camelCase with no leading underscore (project code-style convention).
JSON for static data, SQL Server for runtime state
Static catalogs (gapper profiles, index events, indicator/strategy config) are JSON; runtime state (strategies, preferences, audit logs, condition progress, trade diary, replays, research claims, workspaces) is SQL Server. No Python, no YAML.
The canonical strategy is strict JSON; script text is a view
The semantic model (StrategyDefinition) serialized as versioned, STRICT JSON
(Strategy.ScriptJson, written by IdiotProof.Scripting/StrategyJson.cs) is what evaluators
run. Reads fail closed: unknown schema version, condition type, property, wrong-kind or
unrepresentable value → StrategyJsonException and the strategy is quarantined (visible
reason in ConditionProgress; escalated loudly when it holds a position), never partially
evaluated. IdiotScript text is the human view — generated from the model for display (always
InvariantCulture), and parsed (tolerantly, for now) only for hand-typed input and legacy rows
with no canon. LLM boundaries emit structured JSON against a schema, never DSL text.
Attribution metadata (Author, OriginTranscript) is not part of the canon. ("Parse, don't
validate"; no shotgun parsing on the money path. Verified by StrategyJsonTests.)
Synthetic data never drives a real order
When the market-data feed is Mock, every non-Sandbox entry is refused — Monitor fires, manual
orders, web options tickets and replaces that raise an entry's size alike; Mock data pairs only
with the Sandbox broker. Exits (risk-reducing) are still allowed. The guard is a gate inside the
TransactionGateway (IP-LAW-12), so every order path and any future broker
inherits it. (IdiotProof.Core/Trading/TransactionGateway.cs.)
One evaluator; SQL is the bus
The Monitor is the only live evaluator and order placer for strategies. UI changes reach it only
through SQL rows, read each tick — no restart, no second evaluation path. Backtest and replay
surfaces call the same evaluator and exit brain (IndicatorSnapshotBuilder, conditions,
GapperExitEvaluator, EmaPeriodCollector) so a backtest cannot run different code than live.
A SQL leader lease keeps at most one Monitor trading per database; the Monitor re-confirms it at
the top of every tick and the TransactionGateway re-confirms it again immediately before each
Monitor order reaches a broker (GatewayOptions.LeaderCheck), refusing the order if it is lost.
The repository owns strategy-mutation invariants
StrategyRepository enforces ownership (NotOwner), refuses to deactivate, delete or change the
symbol of a strategy holding a position (PositionOpen), writes only editor-owned columns on
update (never the Monitor's position bookkeeping), enforces one active gapper per symbol and the
3-gapper cap against SQL on every activation path — single, bulk, editor save — with a post-write
recheck (SymbolAlreadyActive / GapperCapReached; auto-armed gappers obey their own per-run
limit instead of the cap), refuses a Live activation the caller has not confirmed
(LiveNotConfirmed: the web asks for the password per activation, as the CLI asks for a code),
and deletes a strategy's ConditionProgress with it. No page writes strategy rows around it.
(StrategyRepositoryGuardTests.)
Every order goes through the TransactionGateway
Every order-like action — Monitor entries and exits, CLI/MCP orders, replaces, cancels,
cancel-alls, position closes and flattens, crypto orders, the web Options ticket, the operator
flatten/test-order — reaches a broker only through TransactionGateway. Its gates run in a
fixed order and fail closed: well-formed order → route (Live never quietly downgrades, IP-LAW-3)
→ synthetic-data guard (IP-LAW-9) → Risk Guardian for entries and for a replace that raises an
entry's size (IP-LAW-2) → LLM panel for automated entries (IP-LAW-1) → Live confirmation
(IP-LAW-13) → place → audit. Every outcome but a dry run writes an AuditLog row
(linked to the Hub's CommandLedger row); a person's accepted order also opens or closes a
TradeDiary row. No other production code calls a broker's place / replace / cancel /
cancel-all, and only the gateway can mark a Live action confirmed.
(IdiotProof.Core/Trading/TransactionGateway.cs; MoneyPathLawTests,
TransactionGatewayTests, MonitorGatewayParityTests.)
Live from the CLI or MCP needs a confirm code
A person's Live action from the CLI or MCP is previewed and held: the gateway stores the exact
intent and returns a 6-character code that works once, for the same owner, within 120 seconds,
and replays exactly what was previewed — an edited, swapped, expired or reused code places
nothing. Several Live actions (close-all) share one code bound to exactly that set. The web
confirms Live with the owner's password elevation (re-checked on the server) instead of a code;
the Monitor needs none, because activating a strategy in Live was the confirmed act. Non-order
Live mutations that move money (funding, transfers) use the same ConfirmationStore codes.
(IdiotProof.Core/Trading/ConfirmationStore.cs; MoneyPathLawTests,
TransactionGatewayTests.)
A sequence is one strategy
A sequenced strategy is one Strategy row and one canon: the root definition is step 0, later
steps live in the root's nextSteps inside the same canonical JSON, a step never nests steps,
and the sequence keys are written only when used, so a strategy with no later steps serializes,
stamps and scripts byte-identically to a strategy written before steps existed. Runtime sequence
state (which step is armed, since when) belongs on that same row, never on a second row or a
settings key, so the repository's guards (IP-LAW-11) and the gapper cap see one
strategy. (IdiotProof.Scripting/StrategyJson.cs — nextSteps allowed on the root only;
DslSequenceTests goldens.)
6. Verified state
Build/test evidence (recorded 2026-10-03, .NET 10 SDK): dotnet test IdiotProof.slnx -c Debug
→ build succeeded, all green, 0 failed: Engine 85 · Indicators 18 · Strategies 34,681
(dominated by generated parameter cases) · Brokers 38 (+3 [Explicit] real-paper tests) · Blazor
217 · UI 62 · Monitor 16 (+2 [Explicit] real-paper tests).
Real Alpaca paper account, 2026-10-03 (market closed, run by name): the 3 options tests pass
(level 3, BE chain + snapshots, a $0.01 buy-to-open placed and cancelled). The short probe
(AlpacaPaperShortIntegrationTests) sent the Monitor's DirectionalOrders.Entry short (1 AAPL,
shortable + ETB, sell limit at about 2× the market) through AlpacaBrokerClient. Alpaca answered 403 40310000 "account is not allowed to short" because the paper account has shorting_enabled=false and equity
under $2,000, so the test reported Inconclusive. The same side + position_intent shape on an
equity is proven by the sell_to_close probe: the Monitor's long exit for 1 held share was
accepted with position_intent=sell_to_close, qty=1, then canceled. Not yet proven: an
accepted sell_to_open, a filled short and a buy_to_close cover against Alpaca; those need a
short-enabled paper account.
Test projects and what they pin:
IdiotProof.Engine.Tests— RiskGuardian gate incl.RecordTradePnLday rollover andUpdateConfigpreserving the daily loss; SupervisedLoop resilience; WorkspaceManager cache hydration; options pricing (OptionsPricingTests); AppSettings credential overlay.IdiotProof.Indicators.Tests— RSI/EMA/ATR/MACD/VWAP math + ADX Wilder-seed regression.IdiotProof.Strategies.Tests— DSL round-trip, backtester (ET time exits, trailing/giveback replay), gapper factory/conditions/exits (GapperTests), mock-gap-day lifecycle (GapperLifecycleTests), canonical JSON (StrategyJsonTests), day replay (GapperDayBacktesterTests), EMA collector, scale-out ladder (DslStrategySignalTests), trading-day gate (MarketTimeTests), fail-closed conditions (ConditionFailClosedTests), window-scoped latches (WindowScopedConditionTests), culture-safe text (ScriptTextRoundTripTests), the velocity exit's ladder position and the new conditions (VelocityExhaustionTests), sequences and the non-sequence byte-identity goldens (DslSequenceTests,StrategyBacktesterSequenceTests).IdiotProof.Evidence.Tests—GapperFormationTests: no-lookahead (post-t bars change nothing), velocity math over gaps and zero-volume minutes,TimeOfDayProfileround trip and version guard,GapperVolumeRatiowith and without a profile.IdiotProof.Brokers.Tests— BrokerRouter Sandbox default, sandbox fills, Alpaca extended-hours contract, options wire format + Sandbox chain/basis (OptionsBrokerTests), equity short wire format, the can't-short 403 and negative short positions (AlpacaEquityShortWireTests); the opt-in[Explicit]AlpacaPaperOptionsIntegrationTestsruns only by name against the real paper account.IdiotProof.Blazor.Tests— verb-catalog reflection, the LLM gate on Legion's voter panel over a fake transport (LlmVotingServiceTests), per-owner Claude keys and their isolation (UserClaudeKeyResolverTests,ClaudeKeyIsolationTests,UserClaudeKeyRoutingTests), self-service password reset (PasswordResetFlowTests),ConditionProgressRepository(SQL Server LocalDB),StrategyRepositoryGuardTests,UserBrokerResolverTests,GapperInterpreterTests,LegionProviderContractTests, research (EdgarServiceTests,Form4ParserTests,CorporateActionDetectorTests,RegulatoryScannerTests,SignificanceScorerTests,OutcomeBackfillServiceTests,IndexEventScannerTests,TickerUniverseServiceTests),RiskGuardianServiceTests,UserPreferencesServiceTests.IdiotProof.UI.Tests— Options presenter / position view / glossary logic.IdiotProof.Monitor.Tests— long/short order shapes and a short round trip on the Sandbox broker (DirectionalOrdersTests);PremarketFadeScannerandBeBexDecayScannermath; the opt-in[Explicit]AlpacaPaperShortIntegrationTests(keys from thealpaca-paperkeyring entry, skipped without it, refuses any host butpaper-api.alpaca.markets) sends a never-fillable 1-share short and a never-fillablesell_to_close, reads each back and cancels it.
Hub, CLI and MCP (Epic W, 2026-10-30/31). Each ticket landed with its own tests, run per
ticket (dotnet test <project> --filter ...) plus CatalogCompletenessTests; the whole solution
has not been re-run since the 2026-10-03 record above.
IdiotProof.Core.Tests— the money path:TransactionGatewayTests(gate order, Live hold and confirm, audit),MoneyPathLawTests(no production broker call outside the gateway, IP-LAW-12/IP-LAW-13),ConfirmationStoreTests(single use, 120 s, owner- and intent-bound),MonitorGatewayParityTests,OptionsGatewayTests,GatewayDiaryTests,MonitorHostingTests,MonitorPauseFlagTests,OwnerResolverTests,SecretRedactorTests,CoreServiceRegistrationTests, plus the services moved from Blazor (research, LLM voting, Claude keys, repositories,UserBrokerResolver*Tests); the catalyst intake (CatalystPreClassifierTestsfixtures for FDA, 424B, 5.03 reverse split and promo wire,CatalystScorerTestsmonotonic freshness decay,CatalystIngestorTests,CatalystIntakeParsingTests,AlpacaNewsServiceRangeTests).IdiotProof.Operations.Tests— the operation framework (OperationCatalogTests,ArgsBinderTests,OperationDispatcherTests,OperationContractTests,CatalogCompletenessTests: every operation documented, examples bind, JSON and argv round-trip), one fixture per operation family run against scripted AlpacaHttpMessageHandlers or in-memory services (account, orders, positions, options, crypto, market, reference, strategies, gapper, replay, risk, LLM, research, diary, audit, ledger, keys, settings, users, monitor, DSL), the knowledge index anddocs *(Docs/*Tests), andAlpacaCoverageTests(every endpoint in docs/ALPACA_COVERAGE.md has an existing operation, every called path is inventoried).IdiotProof.Cli.Tests— exit codes, argv/global flags, stdin-values, help with the Hub up or down, local commands, owner defaults,AgentExportTests, andPublicDocsPrivacyTests(no key-shaped string or account id in README.md or docs/).IdiotProof.Mcp.Tests— one tool per operation with the catalog's schema, confirmation input on money tools, risk hints, Hub-down errors (McpToolTests), and the doc resources (DocResourceTests).
Not proven by the solution test run: the Blazor UI flows and the live LLM voting round-trip; a
Live CLI/MCP order confirmed end to end against a real Live account (the confirm path is proven
against scripted brokers only, by design: no test touches real money).
The Cypress suite (tests/IdiotProof.Cypress/cypress/e2e/, specs 01–09) runs deterministically
with IDIOTPROOF_FAKE_LLM=1 (the Development-only FakeLlmHandler intercepts Legion calls
server-side) but needs a live server run to graduate the E-stories to ✅; 08_options.cy.ts has
been run green. MonitorWorker itself has no host-level harness test.
7. Active frontier
- Options, phase 2 — the fill-and-close half of a real paper round-trip during market hours
(IP-US-U10); extract the duplicated Live elevation modal into one shared component; then option
legs in the strict-JSON strategy schema (v2), IV/Greeks conditions in the
Conditionscatalog, and a non-linearRiskGuardianmodel (max loss = premium for long options) before the Monitor may ever fire an options order. Multi-leg spreads (order_class: "mleg") after that. - Shorts on a real account — enable shorting on the paper account (margin, equity ≥ $2,000),
then re-run
AlpacaPaperShortIntegrationTeststo prove an acceptedsell_to_open, and during market hours a filled short covered bybuy_to_close(IP-US-K16). - Gapper hardening (Epic K tail) — host-level MonitorWorker test (queue → 4AM fire → hold →
rollover sell),
/gapperCypress spec, fill-price reconciliation against the broker's actual fill (entry is recorded at the limit price), full order-state tracking (pending orders as first-class rows). - Gapper program (IP-T442–T445) — the study pipeline (universe, tiered pull, snapshots,
labels,
test H1..H17, report) and its verdict; the Monitor half of Steps (row step state, transition block,strategy step --reset, builder cards); the live formation recorder and theformation-trigger.jsonarming rule (all thresholds null = never arms); catalyst and momentum facts for the voter panel; the time-of-day profile replacing the 20-barIsVolumeAbovebaseline oncedata/tod-profile.jsonexists; the short leg only after the long exit survives the 20-day paper shadow (§4.4). - Known debts — DSL generation is single-shot and the Describe tab still emits text, not
model JSON; the unused
UserPreferences.OpenStrategyTabscolumn awaits removal in a migration; the Azure infra (tools/azure-provision.md) is not provisioned; the OAuth token is not yet wired into order placement (needs a registered Alpaca OAuth app + paper testing). - Replay tests (Epic R) — the replay/scan/export/family commands ship without NUnit coverage.
- Adaptive auto-strategy generation (Epic S) — standardize the
auto-gapperseed into a wait-for-enough-information generator across sessions. - Strategy ghost overlay + branching visualization — chart integration, simulator timeline, branch fork rendering. (Epic G in USER_STORIES.md.)
- Roslyn-based IdiotScript parser — replace the tolerant regex parser with exact line/col diagnostics. (IP-US-H1.)
- Cypress CI run — run the suite against a live server with
IDIOTPROOF_FAKE_LLM=1to graduate IP-US-E1–E6, the Learning Center (Epic I) and Backtest (Epic J) stories.
8. Quality bar
A feature is done (✅) only when: it builds clean in IdiotProof.slnx; it has a green
automated test (NUnit for backend, Cypress for UI) that is named in
USER_STORIES.md; user-facing changes have an e2e or lifecycle assertion;
and it respects the laws in §5 (gates in order, Sandbox default, Vault/Legion routing, no
underscore fields). Anything not proven by a test is 🟡/⬜. (Inherits [HOUSE-LAW-8].)
9. Glossary
- IdiotScript — the fluent C# DSL (
Stock.Ticker("NVDA").RequireAdxAbove(20)...Build()) that expresses a strategy as six lifecycle phases; the human view of the canonical JSON. - Phase — one of the six fixed stages every strategy walks: Setup, Filters, Entry, Order, Risk, Exit. The parser rejects verbs used in the wrong phase.
- Condition — a single boolean check (
IsAboveVwap(),OnReclaim(9)) composed with.And()/.Or()/.Not(). Latch verbs (Breakout,Pullback,HoldsAbove) evaluate live against the snapshot's window high/low. - Gate — one of the three pre-fire checks: condition match → LLM voter quorum → Risk Guardian.
- Risk Guardian —
IdiotProof.Shared.Risk.RiskGuardian, the final pre-trade veto. - Monitor —
IdiotProof.Monitor, the unattended 24/7 console evaluator and executor. - SupervisedLoop — the fault-tolerant tick loop the Monitor runs.
- Voter panel / Legion — the multi-LLM quorum (configured in
legion.json, shipped with both hosts) that approves or rejects a Claude-generated script / a candidate fire, via MindAttic.Legion. - ConditionProgress — the SQL row (
N/M, first failing verb) the Monitor upserts per tick and the Strategies page polls for live badges. - Quarantine — a strategy whose canon cannot be fully understood; never evaluated, reason shown.
- Sandbox broker — the always-registered simulated broker (instant fills into an in-memory
position book) that is the safe default in
BrokerRouter. - BrokerMode — a strategy's own routing choice: Paper, Live or Sandbox.
- Trade diary — the
TradeDiarytable: one row per trade lifecycle (entry, risk plan, exit, realized P&L, paper/live), a permanent record that survives strategy deletion. - Gapper — a stock gapping up in premarket vs the previous close; the flagship trade: buy in the 4AM window, sell before the 9:30 bell.
- Gapper profile — the dialable template (gap %, volume ratio, price band, entry window,
stops, giveback, arm/sell-by times, notional) in
IdiotProof.Core/Catalogs/gapper-profiles.json; cloned and tuned per ticker on the/gappertab. - Peak giveback — the momentum-rollover exit: sell once price gives back N% of the run from entry to the post-entry peak; armed from a configured ET time ("the last 15 minutes").
- Velocity exhaustion — the exit that fires when the favorable price velocity over a 1/3/5
minute window falls under N %/min after a run (
VelocityExitRule); sits after the take-profit targets and before peak giveback in the exit ladder, mirrored for shorts. - Step / sequence — one ordered stage of a sequenced strategy, with its own direction,
conditions and exits; the root definition is step 0,
ThenWhenis the flat-only transition to the next step andWithin(days)its deadline. One row, one canon (IP-LAW-14). - Catalyst event — one
CatalystEventrow: a news item or SEC filing as the intake saw it, withPublishedUtcandReceivedUtc, a deterministic pre-class and direction, optional LLM fields and a 0-100 catalyst score. Research data; nothing in trading reads it yet. - Time-of-day profile —
TimeOfDayProfile: the median fraction of a day's 04:00–20:00 volume printed by each clock minute, per ADV decile, fit on the study's training controls; the baseline forVolumeRatioTod. Not shipped yet; without it volume ratios are plain cumVol / ADV. - Previous close — the prior trading day's official close; the reference for gap %. Gap conditions fail closed without it.
- Replay — re-running a strategy over a past session's bars with the live evaluator; stored
as a
ReplayRunwithReplayTrade/ReplayBarfeature rows. - Strategy family — a built-in replay template (
momentum,reversal,emabreak,rthdrive,rsireversal,swingreversal,shortfade) used when a ticker has no saved strategy. - Research claim — one
ResearchClaimrow: a catalyst or portent extracted from a filing, news article, or regulatory notice, with sentiment/magnitude/timing and a significance score. - Macro claim — a
ResearchClaimwithIsMacro = true: a regulatory/exchange-rule event that isn't about one company (Tickerblank; affected tickers, when resolvable, live inAffectedTickersJson). - Significance score — the 0-100 value
SignificanceScorercomputes per claim (magnitude × confidence, historical correlation strength, source trust, recency, watchlist boost); the Research tab's ranked feed sorts by it. - Tracked ticker — a cached row in
TrackedTicker(symbol, exchange, latest price) forming the research scanner's ticker universe; refreshed daily from Alpaca's asset list. - OCC symbol — the exchange-standard option identifier (
BE251219C00038000= BE, 2025-12-19, Call, $38.00): root +YYMMDD+C|P+ strike×1000 zero-padded to 8. Self-describing; decoded byOptionContract.ParseOcc. - Intrinsic value — what an option is worth if exercised right now:
max(0, S−K)for a call,max(0, K−S)for a put. The "real" part of the premium. - Extrinsic value (hype / time value) — premium minus intrinsic: what the market pays for the possibility of a move. Inflates when the stock runs, decays to zero by expiration. Selling the contract while it is high is the trade; it is what evaporates if you wait for reality.
- Breakeven — the underlying price at expiration where the trade nets zero (
K + premiumfor a call,K − premiumfor a put). Only matters if you hold to expiration. - DTE — calendar days to expiration.
- IV (implied volatility) — the volatility that makes the Black-Scholes price equal the live
premium; supplied by Alpaca's snapshots when available, otherwise solved locally and badged
Model. - Options trading level — Alpaca's per-account permission (
options_trading_level): 0 none, 1 covered calls / cash-secured puts, 2 long calls/puts, 3 spreads. - Sell signal —
SellSignalEvaluator's informational nudge on an open long option: extrinsic value within 5% of its observed high (after at least 3 samples) and a Bullish research claim on the underlying in the last 7 days → "consider taking profit". Never places anything. - Index event — a
ResearchClaimwithClaimType = "IndexEvent": an announced S&P 500/100 addition or deletion logged inIdiotProof.Core/Catalogs/sp-index-events.json, Pending until effective.