Purpose: First-order logic validation — catches BUGS, not strategy quality.
Full Documentation: /docs/LOGICAL_SANITY_CHECKS.md
- A1: No
close > high,close < low,high < low(unless synthetic bars documented) - A2: No division by zero or
nawithout guards (ATR, volume, prior close, range) - A3: All periods/lengths >= 1 and bounded (no negative, zero, or unbounded lookbacks)
- A4: Percentages bounded 0-100% (unless documented exception)
- A5: Time conversions use named constants (
MILLISECONDS_PER_MINUTE = 60000.0)
- B1:
strategy.entry()uses onlystrategy.longorstrategy.short - B2:
strategy.exit()stop/limit not swapped; nonaprices without guards - B3: Type compatibility (
ta.barssince(bool), no bool/int confusion) - B4:
request.security()has explicitlookahead,gaps, AND data persistence (na fallbacks) - B5: All
vardeclarations have explicit type and initial value - B6: Pyramiding logic matches settings (check
strategy.position_size == 0if pyramiding=0) - B7: Repainting patterns explicit (
calc_on_every_tick,request.securitylookahead) - B8: All
ta.*functions called unconditionally (not insideifblocks or loops)
- C1: Long stops below entry, short stops above entry ← Critical upside-down check
- C2: Long TP above entry, short TP below entry ← Critical upside-down check
- C3: Stop/target values don't become
naduring position (no silent sign flips) - C5: Price references captured at entry (use
strategy.position_avg_priceor capture on fill)
- D1: Every
varhas explicit reset logic (session boundary or flat state) - D2: Session boundaries match intent (RTH close vs midnight)
- D3: Centralized
canTradegate controls all entries - D4: Entry tracking (
entryBar, counters, flags) resets when flat
- E1: Missing external data behavior explicit (fail-open vs fail-closed)
- E2:
request.security()handlesnaand session gaps withbarmerge.gaps_off
- B6: Pyramiding logic coherent (if pyramiding=0, check position before entry; if enabled, document intent)
- B7: Repainting risks documented (
calc_on_every_tickwith bar functions,request.securitylookahead)
- C4: Entry IDs match direction (ID "Long" uses
strategy.long, notstrategy.short)
- E3: All external symbols enumerated in header/metadata
- E4: Symbol requirements documented (required vs optional; fail-open vs fail-closed)
- F1: Long/short entries are mutually exclusive (or explicit sequencing)
- F2: No always-true (
high >= low) or never-true (close > high) conditions - F4: Exit logic reachable given time limits, stops, and session closes
- G1: Commission < ~10% of typical trade value (context-dependent)
- G2: Stop distance reasonable for asset (prefer ATR-normalized:
stopDist > 4*ATR→ warning) - G3: Entry frequency not suspicious (not almost-always or almost-never true)
- G4: Bar Magnifier assumption documented (tight stops/targets validated on lower TF)
- H1: Key states visualized during dev (OR levels, veto flags, session boundaries)
- H2: Veto reasons visible (labels/plots when veto active)
- H3: Debug visuals behind
input.bool(debugMode)toggle - H4: Pre-flight status block (table/logs showing config and filter states)
These are the most frequent logic inversions caught by these checks:
// ❌ BAD: Stop for long is ABOVE entry (exits immediately!)
if strategy.position_size > 0
stop = strategy.position_avg_price + 10 // WRONG!
strategy.exit("Stop", stop=stop)
// ✅ GOOD: Stop for long is BELOW entry
if strategy.position_size > 0
stop = strategy.position_avg_price - 10
strategy.exit("Stop", stop=stop)
// ❌ BAD: ID says "Long" but using short direction
strategy.entry("Long", strategy.short) // WRONG!
// ✅ GOOD: ID matches direction
strategy.entry("Long", strategy.long)
// ❌ BAD: Using current ATR for historical position
if strategy.position_size > 0
stop = close - (2 * ta.atr(14)) // Wrong reference!
// ✅ GOOD: Capture ATR at entry
var float entryAtr = na
if position opened
entryAtr := ta.atr(14)
stop = entryRefPrice - (2 * entryAtr)
// ❌ BAD: Counter never resets
var int tradesToday = 0
if filled
tradesToday += 1 // Accumulates forever!
// ✅ GOOD: Reset at session boundary
var int tradesToday = 0
if isFirstBar
tradesToday := 0
// ❌ BAD: rthClose can be na or zero
gapPct = (open - rthClose) / rthClose // CRASH!
// ✅ GOOD: Guard with check
gapPct = (not na(rthClose) and rthClose != 0) ?
(open - rthClose) / rthClose : na
// ❌ BAD: pyramiding=0 but multiple entry attempts
strategy("Test", overlay=true) // pyramiding=0 by default
if crossover(ma1, ma2)
strategy.entry("Long", strategy.long)
if rsi < 30
strategy.entry("Long", strategy.long) // Silently ignored if already long!
// ✅ GOOD: Check position before entry
if crossover(ma1, ma2) and strategy.position_size == 0
strategy.entry("Long", strategy.long)
// ❌ BAD: request.security without explicit lookahead
dailyClose = request.security(syminfo.tickerid, "D", close)
// Historical: sees future (completed bar)
// Real-time: bar still forming
// ✅ GOOD: Explicit lookahead_off
dailyClose = request.security(syminfo.tickerid, "D", close,
barmerge.gaps_off, barmerge.lookahead_off)
// ❌ BAD: ATR only calculated conditionally
if isRth
float atr = ta.atr(14) // State breaks outside RTH!
// Later: atr is stale or na
// ✅ GOOD: Calculate unconditionally, use conditionally
float atr = ta.atr(14) // Every bar
if isRth
stopDist = 2 * atr // Use the value
// ❌ BAD: No fallback for na data
tickValue = request.security("NYSE:TICK", timeframe.period, close)
if tickValue > 700 // na > 700 = false (silent failure!)
// Veto intended but not applied
// ✅ GOOD: Explicit na handling
tickValue = request.security("NYSE:TICK", timeframe.period, close,
barmerge.gaps_off, barmerge.lookahead_off)
tickValid = not na(tickValue)
tickVeto = tickValid ? (tickValue > 700) : true // Fail-closed
// ⚠️ WARNING: Tight stops on 5-min bars
// If bar range includes both stop AND target,
// backtest assumes target hit first (optimistic!)
// Solution: Validate on 1-min bars or document assumption
// EXECUTION MODEL: Bar Magnifier favorable execution assumed
// Strategy validated on 1m timeframe; 5m used for overview
- Any failures → Block and fix immediately
- Findings → Review and fix, or document as intentional
- Note for user awareness
- Verify intentional
- Implement observability during development
- Disable for production via toggle
| Symbol | Level | Meaning |
|---|---|---|
| 🔴 | CRITICAL | Code is broken; must fix |
| 🟡 | HIGH | Likely bug; should fix or document |
| 🟠 | MEDIUM | May be issue; review recommended |
| 🔵 | WARNING | Suspicious but not error; note for awareness |
Always:
- Before committing code
- After significant logic changes
- When debugging unexpected behavior
Recommended:
- Code review with peers
- Porting strategy to new symbol/timeframe
- Adding new features/filters
Not Required:
- For strategy evaluation (profitability, optimization)
- For parameter tuning
- For backtest analysis (win rate, profit factor, etc.)
These checks validate that your code does what you intend it to do.
They do NOT evaluate whether what you intend is a good trading idea.
Strategy logic and trading decisions remain entirely your domain expertise.
- "Your long stop is above entry price — this will exit immediately" (BUG)
- "You're dividing by
rthClosewhich can bena" (BUG) - "Entry ID is 'Long' but using
strategy.short" (BUG)
- "Your stop is too tight for this volatility" (STRATEGY EVALUATION)
- "Your win rate is low; try different parameters" (STRATEGY EVALUATION)
- "This indicator combination doesn't work well" (STRATEGY EVALUATION)
Full Documentation: /docs/LOGICAL_SANITY_CHECKS.md (1,027 lines with code examples)
Related:
/docs/PINE_SCRIPT_STANDARDS.md— Coding standards (Categories 1-8)/.cursorrules— Complete code review checklist (Categories 1-9)
Version: 1.3 | Last Updated: January 8, 2026