import { useState } from “react”;
// ─── Full Pine Script v5 source ───────────────────────────────────────────────
const PINE = `// ============================================================
// 9:30 CANDLE BOT v2 · Pine Script v5
// Strategy & Indicator for TradingView
//
// RULES IMPLEMENTED:
// 1. 9:30 AM opening candle → box HIGH / LOW (the “zone”)
// 2. Break candle: closes above HIGH (LONG) or below LOW (SHORT)
// — skips doji (body < 28% range) and inside bars
// — requires above-average volume (1.1× 20-bar avg)
// 3. Retest: price pulls back to zone, wick enters ≤ 40%
// of setup candle range, closes outside the zone
// 4. Confirm candle: body ≥ 45% range, closes beyond
// retest candle’s extreme in the breakout direction
// 5. Confluence score ≥ 65 required to enter
// (trend alignment, volume, body quality, retest depth,
// ATR ratio, momentum beyond break candle)
// 6. SL = 1 × ATR(14) | TP = 2R (score<80) or 2.5R (score≥80)
// 7. Only trade 09:30 – 10:45 ET | EMA(20) trend filter
// 8. Skip if setup candle range > 2.1 × ATR(14)
// ============================================================
//@version=5
strategy(
title = “9:30 Candle Bot v2”,
shorttitle = “930v2”,
overlay = true,
initial_capital = 10000,
default_qty_type = strategy.percent_of_equity,
default_qty_value= 2,
commission_type = strategy.commission.percent,
commission_value = 0.05,
slippage = 1
)
// ─── INPUTS ──────────────────────────────────────────────────────────────────
grp1 = “⏰ Time Gate”
sessionStart = input.session(“0930-1045”, “Trade Window (ET)”, group=grp1)
openCandleMins = input.int(5, “Opening Candle Length (min)”, minval=1, maxval=15, group=grp1)
grp2 = “📊 Filters”
atrPeriod = input.int(14, “ATR Period”, minval=5, group=grp2)
atrSLMult = input.float(1.0,“SL Distance (× ATR)”, minval=0.5,step=0.1, group=grp2)
atrSkipMult = input.float(2.1,“Skip if candle > (× ATR)”, minval=1.5,step=0.1, group=grp2)
emaPeriod = input.int(20, “EMA Trend Period”, minval=5, group=grp2)
minScore = input.int(65, “Min Confluence Score (0-100)”,minval=40,maxval=100,group=grp2)
volLookback = input.int(20, “Volume Avg Lookback”, minval=5, group=grp2)
minBodyPctConf = input.float(0.45,“Confirm Candle Min Body %”, minval=0.2,step=0.05,group=grp2)
minBodyPctBrk = input.float(0.28,“Break Candle Min Body %”, minval=0.1,step=0.05,group=grp2)
maxRetestDepth = input.float(0.40,“Max Retest Depth (% zone)”, minval=0.1,step=0.05,group=grp2)
grp3 = “🎯 Trade Management”
rrLow = input.float(2.0, “R:R Ratio (score < 80)”, minval=1.0,step=0.5,group=grp3)
rrHigh = input.float(2.5, “R:R Ratio (score ≥ 80)”, minval=1.0,step=0.5,group=grp3)
grp4 = “🎨 Display”
showZone = input.bool(true, “Show 9:30 Zone Box”, group=grp4)
showLabels = input.bool(true, “Show Signal Labels”, group=grp4)
showScore = input.bool(true, “Show Confluence Score”,group=grp4)
showBreakEven = input.bool(true, “Show Break-Even Line”, group=grp4)
// ─── CORE INDICATORS ─────────────────────────────────────────────────────────
atr = ta.atr(atrPeriod)
ema20 = ta.ema(close, emaPeriod)
avgVol = ta.sma(volume, volLookback)
// ─── SESSION & TIME LOGIC ─────────────────────────────────────────────────────
// In session = within 09:30–10:45 ET window
inSession = not na(time(timeframe.period, sessionStart, “America/New_York”))
// Detect the very first bar of the trading day at 09:30
// (works on 1-min, 2-min, 3-min, 5-min charts)
isMarketOpen = hour(time, “America/New_York”) == 9 and
minute(time, “America/New_York”) == 30
// ─── OPENING RANGE CANDLE (9:30 AM CANDLE) ───────────────────────────────────
// We track the FIRST candle of the day that starts at 09:30
var float setupHigh = na
var float setupLow = na
var bool setupDone = false
var int setupBar = na
var float setupRange = na
// Reset at midnight (new day)
if dayofweek != dayofweek[1]
setupHigh := na
setupLow := na
setupDone := false
setupBar := na
setupRange := na
// Capture the 9:30 opening candle on its CLOSE
if isMarketOpen and not setupDone
setupHigh := high
setupLow := low
setupDone := true
setupBar := bar_index
setupRange := high - low
// Skip if opening candle is oversized vs ATR
setupOversized = setupDone and setupRange > atr * atrSkipMult
// ─── DRAW ZONE BOX ────────────────────────────────────────────────────────────
var box zoneBox = na
if showZone and setupDone and bar_index == setupBar
zoneBox := box.new(
left = setupBar,
top = setupHigh,
right = setupBar + 50,
bottom = setupLow,
border_color = color.new(color.orange, 40),
border_width = 1,
bgcolor = color.new(color.orange, 88)
)
// ─── BREAK DETECTION ─────────────────────────────────────────────────────────
// Break candle = first bar after setup that closes decisively outside the zone
// Requirements: not a doji, not an inside bar, volume above average
bodySize = math.abs(close - open)
candleRange = high - low
bodyPct = candleRange > 0 ? bodySize / candleRange : 0
isDoji = bodyPct < minBodyPctBrk
isInsideBar = high <= setupHigh and low >= setupLow
isHighVolume = volume > avgVol * 1.1
// A break candle fires once, on the first qualifying bar
var bool breakDetected = false
var bool breakIsLong = false
var float breakBarHigh = na
var float breakBarLow = na
var int breakBar = na
if dayofweek != dayofweek[1]
breakDetected := false
breakIsLong := false
breakBarHigh := na
breakBarLow := na
breakBar := na
longBreak = setupDone and not setupOversized and inSession and
not breakDetected and
close > setupHigh and not isDoji and not isInsideBar and isHighVolume
shortBreak = setupDone and not setupOversized and inSession and
not breakDetected and
close < setupLow and not isDoji and not isInsideBar and isHighVolume
if longBreak or shortBreak
breakDetected := true
breakIsLong := longBreak
breakBarHigh := high
breakBarLow := low
breakBar := bar_index
// ─── RETEST DETECTION ────────────────────────────────────────────────────────
// After break: price pulls back to the broken level
// Candle must wick into zone but CLOSE outside
// Penetration ≤ maxRetestDepth of setup candle range
var bool retestDetected = false
var float retestLow = na
var float retestHigh = na
var int retestBar = na
if dayofweek != dayofweek[1]
retestDetected := false
retestLow := na
retestHigh := na
retestBar := na
if breakDetected and not retestDetected and inSession and bar_index > breakBar
if breakIsLong
retestDepthPts = setupHigh - low
depthPct = setupRange > 0 ? retestDepthPts / setupRange : 1.0
if low <= setupHigh and close > setupHigh and depthPct <= maxRetestDepth
retestDetected := true
retestLow := low
retestHigh := high
retestBar := bar_index
else
retestDepthPts = high - setupLow
depthPct = setupRange > 0 ? retestDepthPts / setupRange : 1.0
if high >= setupLow and close < setupLow and depthPct <= maxRetestDepth
retestDetected := true
retestLow := low
retestHigh := high
retestBar := bar_index
// ─── CONFIRMATION CANDLE ─────────────────────────────────────────────────────
// Strong body candle in breakout direction, closing beyond retest extreme
var bool entryFired = false
var float entryPrice = na
var float entrySL = na
var float entryTP = na
var int entryBar = na
var float entryScore = na
if dayofweek != dayofweek[1]
entryFired := false
entryPrice := na
entrySL := na
entryTP := na
entryBar := na
entryScore := na
if retestDetected and not entryFired and inSession and bar_index > retestBar
confirmBodyPct = candleRange > 0 ? bodySize / candleRange : 0
longConfirm = breakIsLong and close > open and
close > retestHigh and confirmBodyPct >= minBodyPctConf
shortConfirm = not breakIsLong and close < open and
close < retestLow and confirmBodyPct >= minBodyPctConf
if longConfirm or shortConfirm
// ── CONFLUENCE SCORING ──────────────────────────────────────────
score = 40.0
// 1. Trend alignment (EMA-20)
trendUp = close > ema20
trendDown = close < ema20
if (longConfirm and trendUp) or (shortConfirm and trendDown)
score += 20
else if math.abs(close - ema20) / close < 0.001 // near neutral
score += 8
else
score -= 10
// 2. Break candle volume
if volume[bar_index - breakBar] > avgVol * 1.4
score += 10
else if volume[bar_index - breakBar] > avgVol * 1.1
score += 5
// 3. Break candle body quality
breakBodyPct = breakBarHigh - breakBarLow > 0 ?
math.abs(close[bar_index - breakBar] - open[bar_index - breakBar]) /
(breakBarHigh - breakBarLow) : 0
if breakBodyPct >= 0.65
score += 10
else if breakBodyPct >= 0.50
score += 5
else
score -= 5
// 4. Retest depth bonus
rtDepth = breakIsLong ? (setupHigh - retestLow) : (retestHigh - setupLow)
rtDepthPct = setupRange > 0 ? rtDepth / setupRange : 1.0
if rtDepthPct < 0.25
score += 10
else if rtDepthPct < 0.40
score += 5
else
score -= 8
// 5. Confirm candle body quality
if confirmBodyPct >= 0.65
score += 10
else if confirmBodyPct >= 0.50
score += 4
else
score -= 6
// 6. Setup candle ATR ratio
atrRatio = atr > 0 ? setupRange / atr : 1.0
if atrRatio >= 0.8 and atrRatio <= 1.8
score += 5
else if atrRatio > 2.0
score -= 10
// 7. Momentum: confirm closes beyond break candle extreme
if longConfirm and close > breakBarHigh
score += 5
if shortConfirm and close < breakBarLow
score += 5
score := math.max(0, math.min(100, score))
// ── ENTRY (only if score ≥ minScore) ───────────────────────────
if score >= minScore
slDist = atr * atrSLMult
rrMult = score >= 80 ? rrHigh : rrLow
ePrice = close
eSL = longConfirm ? ePrice - slDist : ePrice + slDist
eTP = longConfirm ? ePrice + slDist * rrMult : ePrice - slDist * rrMult
entryFired := true
entryPrice := ePrice
entrySL := eSL
entryTP := eTP
entryBar := bar_index
entryScore := score
if longConfirm
strategy.entry("LONG", strategy.long)
strategy.exit("LONG-X", "LONG",
stop = eSL,
limit = eTP,
comment= "SL/TP")
else
strategy.entry("SHORT", strategy.short)
strategy.exit("SHORT-X","SHORT",
stop = eSL,
limit = eTP,
comment= "SL/TP")
// ─── CLOSE ANY OPEN POSITION AT SESSION END ───────────────────────────────────
sessionEnd = hour(time, “America/New_York”) == 10 and minute(time, “America/New_York”) >= 45
if sessionEnd and strategy.position_size != 0
strategy.close_all(comment=“Session End”)
// ─── VISUALS ──────────────────────────────────────────────────────────────────
// 9:30 Candle highlight
barcolor(bar_index == setupBar and setupDone ? color.new(color.orange, 60) : na)
// Break bar triangle
plotshape(longBreak and showLabels, title=“Long Break”, style=shape.triangleup,
location=location.belowbar, color=color.new(color.aqua, 20), size=size.tiny, text=“BRK”)
plotshape(shortBreak and showLabels, title=“Short Break”, style=shape.triangledown,
location=location.abovebar, color=color.new(color.red, 20), size=size.tiny, text=“BRK”)
// Retest dot
plotshape(retestDetected and bar_index == retestBar and showLabels,
title=“Retest”, style=shape.circle,
location=breakIsLong ? location.belowbar : location.abovebar,
color=color.new(color.purple, 20), size=size.small, text=“RT”)
// Entry arrow + score label
plotshape(entryFired and bar_index == entryBar and breakIsLong and showLabels,
title=“Long Entry”, style=shape.labelup, location=location.belowbar,
color=color.new(color.green, 10), textcolor=color.white,
text=“▲ LONG”, size=size.normal)
plotshape(entryFired and bar_index == entryBar and not breakIsLong and showLabels,
title=“Short Entry”, style=shape.labeldown, location=location.abovebar,
color=color.new(color.red, 10), textcolor=color.white,
text=“▼ SHORT”, size=size.normal)
// Confluence score label
if entryFired and bar_index == entryBar and showScore
scoreGrade = entryScore >= 85 ? “A+” : entryScore >= 75 ? “A” : entryScore >= 65 ? “B” : “C”
scoreCol = entryScore >= 85 ? color.lime : entryScore >= 75 ? color.green :
entryScore >= 65 ? color.teal : color.yellow
label.new(
x = bar_index,
y = breakIsLong ? low - atr * 2.2 : high + atr * 2.2,
text = “Score: “ + str.tostring(math.round(entryScore)) + “ Grade: “ + scoreGrade,
style = label.style_label_center,
color = color.new(scoreCol, 75),
textcolor = scoreCol,
size = size.small
)
// SL / TP horizontal lines (drawn at entry, extend 30 bars)
if entryFired and bar_index == entryBar
line.new(entryBar, entrySL, entryBar + 30, entrySL,
color=color.new(color.red, 30), width=1, style=line.style_dashed)
line.new(entryBar, entryTP, entryBar + 30, entryTP,
color=color.new(color.green, 30), width=1, style=line.style_dashed)
// Break-even line at midpoint
if showBreakEven
beMid = breakIsLong ? entryPrice + (entryTP - entryPrice) * 0.5
: entryPrice - (entryPrice - entryTP) * 0.5
line.new(entryBar, entryPrice, entryBar + 30, entryPrice,
color=color.new(color.orange, 50), width=1, style=line.style_dotted)
// EMA-20 trend line
plot(ema20, title=“EMA 20”, color=color.new(color.yellow, 60), linewidth=1)
// ─── INFO TABLE ───────────────────────────────────────────────────────────────
var table infoTable = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 70), border_width=1)
if barstate.islast
table.cell(infoTable, 0, 0, “9:30 CANDLE BOT v2”, text_color=color.orange, text_size=size.small)
table.cell(infoTable, 1, 0, “Status”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 0, 1, “Zone High”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 1, na(setupHigh) ? “—” : str.tostring(setupHigh, “#.00”), text_color=color.orange, text_size=size.small)
table.cell(infoTable, 0, 2, “Zone Low”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 2, na(setupLow) ? “—” : str.tostring(setupLow, “#.00”), text_color=color.orange, text_size=size.small)
table.cell(infoTable, 0, 3, “Break”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 3, breakDetected ? (breakIsLong ? “LONG ▲” : “SHORT ▼”) : “—”,
text_color=breakDetected ? (breakIsLong ? color.aqua : color.red) : color.gray, text_size=size.small)
table.cell(infoTable, 0, 4, “Retest”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 4, retestDetected ? “✓” : “—”,text_color=retestDetected ? color.purple : color.gray, text_size=size.small)
table.cell(infoTable, 0, 5, “Entry”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 5, entryFired ? str.tostring(entryPrice, “#.00”) : “—”,
text_color=entryFired ? color.green : color.gray, text_size=size.small)
table.cell(infoTable, 0, 6, “Score”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 6, entryFired ? str.tostring(math.round(entryScore)) + “/100” : “—”,
text_color=entryFired ? color.lime : color.gray, text_size=size.small)
table.cell(infoTable, 0, 7, “ATR-14”, text_color=color.gray, text_size=size.small)
table.cell(infoTable, 1, 7, str.tostring(atr, “#.000”), text_color=color.teal, text_size=size.small)
`;
// ─── Python/algo code (Alpaca paper trading skeleton) ───────────────────────
const PYTHON = `# ============================================================
9:30 CANDLE BOT v2 · Python Execution Engine
Paper-trading skeleton using Alpaca Markets API
pip install alpaca-trade-api pandas numpy
Set env vars:
APCA_API_KEY_ID = your key
APCA_API_SECRET_KEY = your secret
============================================================
import os, time, logging
from datetime import datetime, time as dtime
import pandas as pd
import numpy as np
import alpaca_trade_api as tradeapi
from alpaca_trade_api.rest import TimeFrame
logging.basicConfig(level=logging.INFO, format=”%(asctime)s %(levelname)s %(message)s”)
log = logging.getLogger(“930Bot”)
── CONFIG ────────────────────────────────────────────────────────────────────
SYMBOL = “SPY” # Change to NQ1!, ES1!, QQQ, etc.
TIMEFRAME = TimeFrame.Minute
BAR_MINS = 1 # 1-min bars (change to 5 for 5-min)
RISK_PCT = 0.01 # 1% of account per trade
ATR_PERIOD = 14
ATR_SL_MULT = 1.0
ATR_SKIP_MULT = 2.1
EMA_PERIOD = 20
MIN_SCORE = 65
VOL_LOOKBACK = 20
MIN_BODY_CONF = 0.45
MIN_BODY_BRK = 0.28
MAX_RETEST_DEPT = 0.40
RR_LOW = 2.0
RR_HIGH = 2.5
SCORE_RR_THRESH = 80
SESSION_START = dtime(9, 30)
SESSION_END = dtime(10, 45)
── CONNECT ───────────────────────────────────────────────────────────────────
api = tradeapi.REST(
key_id = os.environ[“APCA_API_KEY_ID”],
secret_key = os.environ[“APCA_API_SECRET_KEY”],
base_url = os.environ.get(“APCA_API_BASE_URL”, “https://paper-api.alpaca.markets”),
)
── INDICATOR HELPERS ─────────────────────────────────────────────────────────
def calc_atr(df: pd.DataFrame, period: int = 14) -> pd.Series:
“”“Wilder-smoothed ATR.”””
hl = df[“high”] - df[“low”]
hcp = (df[“high”] - df[“close”].shift()).abs()
lcp = (df[“low”] - df[“close”].shift()).abs()
tr = pd.concat([hl, hcp, lcp], axis=1).max(axis=1)
atr = tr.ewm(alpha=1 / period, adjust=False).mean()
return atr
def calc_ema(series: pd.Series, period: int) -> pd.Series:
return series.ewm(span=period, adjust=False).mean()
def body_pct(row) -> float:
rng = row[“high”] - row[“low”]
return abs(row[“close”] - row[“open”]) / rng if rng > 0 else 0.0
── CONFLUENCE SCORER ─────────────────────────────────────────────────────────
def score_setup(setup, brk, retest, confirm, direction, atr_val, ema_val, avg_vol) -> int:
score = 40
# 1. Trend alignment
close = confirm["close"]
if (direction == "LONG" and close > ema_val) or (direction == "SHORT" and close < ema_val):
score += 20
elif abs(close - ema_val) / close < 0.001:
score += 8
else:
score -= 10
# 2. Break candle volume
bvol = brk["volume"]
if bvol > avg_vol * 1.4: score += 10
elif bvol > avg_vol * 1.1: score += 5
# 3. Break candle body quality
bp = body_pct(brk)
if bp >= 0.65: score += 10
elif bp >= 0.50: score += 5
else: score -= 5
# 4. Retest depth
zone_rng = setup["high"] - setup["low"]
if zone_rng > 0:
if direction == "LONG":
depth_pct = (setup["high"] - retest["low"]) / zone_rng
else:
depth_pct = (retest["high"] - setup["low"]) / zone_rng
if depth_pct < 0.25: score += 10
elif depth_pct < 0.40: score += 5
else: score -= 8
# 5. Confirm candle body quality
cp = body_pct(confirm)
if cp >= 0.65: score += 10
elif cp >= 0.50: score += 4
else: score -= 6
# 6. Setup candle ATR ratio
setup_rng = setup["high"] - setup["low"]
ratio = setup_rng / atr_val if atr_val > 0 else 1.0
if 0.8 <= ratio <= 1.8: score += 5
elif ratio > 2.0: score -= 10
# 7. Momentum: confirm closes beyond break extreme
if direction == "LONG" and confirm["close"] > brk["high"]: score += 5
if direction == "SHORT" and confirm["close"] < brk["low"]: score += 5
return max(0, min(100, score))
── MAIN STATE MACHINE ────────────────────────────────────────────────────────
class Bot930:
def init(self):
self.reset_day()
def reset_day(self):
self.setup_candle = None # 9:30 bar
self.break_candle = None
self.break_dir = None # "LONG" | "SHORT"
self.retest_candle = None
self.in_trade = False
self.entry = None
self.sl = None
self.tp = None
self.order_ids = {}
log.info("State machine reset for new day.")
def fetch_bars(self, lookback_mins: int = 60) -> pd.DataFrame:
bars = api.get_bars(
SYMBOL, TIMEFRAME,
limit = lookback_mins,
adjustment = "raw"
).df
bars.index = pd.to_datetime(bars.index, utc=True).tz_convert("America/New_York")
return bars
def get_account_equity(self) -> float:
return float(api.get_account().equity)
def in_session(self) -> bool:
now = datetime.now().astimezone().time()
return SESSION_START <= now <= SESSION_END
def cancel_all(self):
try: api.cancel_all_orders()
except Exception as e: log.warning(f"Cancel error: {e}")
def close_position(self):
try:
api.close_position(SYMBOL)
log.info("Position closed.")
except Exception as e:
log.warning(f"Close error: {e}")
def run_bar(self, bars: pd.DataFrame, current: pd.Series):
now = datetime.now().astimezone()
is_930 = now.hour == 9 and now.minute == 30
# Fresh day reset
if now.hour == 9 and now.minute == 28:
self.reset_day()
atr_series = calc_atr(bars, ATR_PERIOD)
atr_val = atr_series.iloc[-1]
ema_val = calc_ema(bars["close"], EMA_PERIOD).iloc[-1]
avg_vol = bars["volume"].tail(VOL_LOOKBACK).mean()
# ── PHASE 1: Capture the 9:30 candle ─────────────────────────────
if is_930 and self.setup_candle is None:
zone_rng = current["high"] - current["low"]
if zone_rng > atr_val * ATR_SKIP_MULT:
log.info(f"SKIP: Opening candle oversized ({zone_rng:.3f} > {atr_val*ATR_SKIP_MULT:.3f})")
return
self.setup_candle = current.to_dict()
log.info(f"9:30 candle captured | H:{current['high']:.2f} L:{current['low']:.2f} Range:{zone_rng:.3f}")
return
if not self.setup_candle or self.in_trade:
return
setup = self.setup_candle
# ── PHASE 2: Detect Break ─────────────────────────────────────────
if self.break_candle is None:
bp = body_pct(current)
is_inside = current["high"] <= setup["high"] and current["low"] >= setup["low"]
is_doji = bp < MIN_BODY_BRK
hi_vol = current["volume"] > avg_vol * 1.1
long_brk = current["close"] > setup["high"] and not is_doji and not is_inside and hi_vol
short_brk = current["close"] < setup["low"] and not is_doji and not is_inside and hi_vol
if long_brk:
self.break_candle = current.to_dict()
self.break_dir = "LONG"
log.info(f"BREAK LONG @ {current['close']:.2f} body%={bp:.2f} vol={current['volume']:.0f}")
elif short_brk:
self.break_candle = current.to_dict()
self.break_dir = "SHORT"
log.info(f"BREAK SHORT @ {current['close']:.2f} body%={bp:.2f} vol={current['volume']:.0f}")
return
brk = self.break_candle
zone_rng = setup["high"] - setup["low"]
# ── PHASE 3: Detect Retest ────────────────────────────────────────
if self.retest_candle is None:
if self.break_dir == "LONG":
depth = setup["high"] - current["low"]
depth_pct = depth / zone_rng if zone_rng > 0 else 1.0
retested = (current["low"] <= setup["high"] and
current["close"] > setup["high"] and
depth_pct <= MAX_RETEST_DEPT)
else:
depth = current["high"] - setup["low"]
depth_pct = depth / zone_rng if zone_rng > 0 else 1.0
retested = (current["high"] >= setup["low"] and
current["close"] < setup["low"] and
depth_pct <= MAX_RETEST_DEPT)
if retested:
self.retest_candle = current.to_dict()
log.info(f"RETEST confirmed depth={depth_pct:.2%}")
return
retest = self.retest_candle
# ── PHASE 4: Confirmation Candle → Entry ──────────────────────────
cp = body_pct(current)
long_conf = (self.break_dir == "LONG" and
current["close"] > current["open"] and
current["close"] > retest["high"] and cp >= MIN_BODY_CONF)
short_conf = (self.break_dir == "SHORT" and
current["close"] < current["open"] and
current["close"] < retest["low"] and cp >= MIN_BODY_CONF)
if not (long_conf or short_conf):
return
score = score_setup(setup, brk, retest, current.to_dict(),
self.break_dir, atr_val, ema_val, avg_vol)
log.info(f"CONFIRM candle dir={self.break_dir} body%={cp:.2f} score={score}/100")
if score < MIN_SCORE:
log.info(f"SKIP: Score {score} < minimum {MIN_SCORE}")
return
# ── PLACE TRADE ───────────────────────────────────────────────────
equity = self.get_account_equity()
sl_dist = atr_val * ATR_SL_MULT
rr_mult = RR_HIGH if score >= SCORE_RR_THRESH else RR_LOW
entry_px = current["close"]
if self.break_dir == "LONG":
sl_px = entry_px - sl_dist
tp_px = entry_px + sl_dist * rr_mult
else:
sl_px = entry_px + sl_dist
tp_px = entry_px - sl_dist * rr_mult
# Position sizing: risk RISK_PCT of equity / sl_dist per share
shares = max(1, int((equity * RISK_PCT) / sl_dist))
side = "buy" if self.break_dir == "LONG" else "sell"
log.info(f"ENTRY {self.break_dir} {SYMBOL} px={entry_px:.2f} sl={sl_px:.2f} "
f"tp={tp_px:.2f} qty={shares} score={score} rr=1:{rr_mult}")
try:
# Market entry
order = api.submit_order(
symbol = SYMBOL,
qty = shares,
side = side,
type = "market",
time_in_force = "day"
)
self.order_ids["entry"] = order.id
self.in_trade = True
self.entry = entry_px
self.sl = sl_px
self.tp = tp_px
# Bracket stop + limit (OCO)
time.sleep(1) # let entry fill
exit_side = "sell" if self.break_dir == "LONG" else "buy"
api.submit_order(
symbol = SYMBOL,
qty = shares,
side = exit_side,
type = "oco",
time_in_force = "gtc",
order_class = "oco",
stop_price = round(sl_px, 2),
limit_price = round(tp_px, 2)
)
log.info("OCO bracket order submitted.")
except Exception as e:
log.error(f"Order error: {e}")
self.in_trade = False
def run(self):
log.info("9:30 Candle Bot v2 starting…")
while True:
try:
now = datetime.now().astimezone()
# Hard stop: close any open position after 10:45
if now.time() > SESSION_END and self.in_trade:
log.info("Session end — closing position.")
self.cancel_all()
self.close_position()
self.in_trade = False
if not self.in_session():
time.sleep(15)
continue
bars = self.fetch_bars(lookback_mins=60)
current = bars.iloc[-1]
self.run_bar(bars.iloc[:-1], current)
except Exception as e:
log.error(f"Loop error: {e}")
time.sleep(60) # wait for next 1-min bar close
if name == “main”:
Bot930().run()
`;
// ─── Syntax highlighter (minimal, regex-based) ───────────────────────────────
function highlight(code, lang) {
const escape = s => s.replace(/&/g,”&”).replace(/</g,”<”).replace(/>/g,”>”);
let s = escape(code);
if (lang === “pine”) {
s = s
.replace(/(//[^\n])/g, ‘$1’)
.replace(/\b(strategy|input|ta|math|str|color|label|line|box|table|barstate|bar_index|timeframe|dayofweek|hour|minute|time|plot|plotshape|barcolor|na|true|false)\b/g, ‘$1’)
.replace(/\b(var|if|else|and|or|not|for|while|to|by|import|export|type|enum|method|switch|return|continue|break)\b/g, ‘$1’)
.replace(/(”(?:[^”]|.)”)/g, ‘$1’)
.replace(/\b(\d+.?\d*)\b/g, ‘$1’)
.replace(/@version=5/g, ‘@version=5’);
} else {
s = s
.replace(/(#[^\n])/g, ‘$1’)
.replace(/\b(def|class|import|from|as|return|if|elif|else|for|while|in|not|and|or|True|False|None|pass|try|except|raise|with|lambda)\b/g, ‘$1’)
.replace(/\b(self|log|api|pd|np|os|time|datetime|dtime|tradeapi|TimeFrame)\b/g, ‘$1’)
.replace(/(”(?:[^”]|.)”|’(?:[^’]|.)’|f”(?:[^”]|.)”)/g, ‘$1’)
.replace(/\b(\d+.?\d*)\b/g, ‘$1’);
}
return s;
}
// ─── Code block component ────────────────────────────────────────────────────
function CodeBlock({ code, lang, title, badge }) {
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(code).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
};
return (
);
}
// ─── Sidebar nav ─────────────────────────────────────────────────────────────
const SECTIONS = [
{ id: “overview”, label: “Overview”, icon: “◈” },
{ id: “pine”, label: “Pine Script v5”, icon: “📈” },
{ id: “python”, label: “Python Engine”, icon: “🐍” },
{ id: “setup”, label: “Setup Guide”, icon: “⚙” },
{ id: “rules”, label: “Strategy Rules”, icon: “📋” },
];
const RULES = [
{ n:“1”, color:”#f59e0b”, title:“Box the 9:30AM Candle”,
desc:“At open, mark the HIGH and LOW of the first candle. This is the zone. Skip if range > 2.1× ATR(14).” },
{ n:“2”, color:”#38bdf8”, title:“Break Candle — Volume + Body”,
desc:“Next candle closes above HIGH (LONG) or below LOW (SHORT). Must have body ≥ 28% and volume > 1.1× avg. No doji, no inside bar.” },
{ n:“3”, color:”#a78bfa”, title:“Retest ≤ 40% Zone Depth”,
desc:“Price wicks back into the zone ≤ 40% of its range, then closes back outside. Confirms the level as support/resistance.” },
{ n:“4”, color:”#34d399”, title:“Confirmation Candle”,
desc:“Strong body candle (≥ 45%) in breakout direction, closing beyond retest extreme. Confluence score ≥ 65 required.” },
{ n:“5”, color:”#f472b6”, title:“ATR Stop + Dynamic R:R”,
desc:“SL = 1× ATR(14). TP = 2R if score < 80, 2.5R if score ≥ 80. Close all positions at 10:45 AM.” },
];
// ─── Main App ─────────────────────────────────────────────────────────────────
export default function App() {
const [activeSection, setActiveSection] = useState(“overview”);
return (
{`
@import url(‘https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Cabinet+Grotesk:wght@400;500;700;800;900&display=swap’);
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #050d18;
--surf1: #081524;
--surf2: #0c1e32;
--surf3: #102440;
--border: #132a44;
--border2: #1a3755;
--text: #c8dff5;
--muted: #3d607f;
--dim: #1a3350;
--green: #34d399;
--blue: #38bdf8;
--purple: #a78bfa;
--amber: #f59e0b;
--pink: #f472b6;
--red: #f87171;
--mono: 'JetBrains Mono', monospace;
--sans: 'Cabinet Grotesk', sans-serif;
}
body { background: var(--bg); color: var(--text); font-family: var(--sans); }
.root {
display: flex;
min-height: 100vh;
background: var(--bg);
background-image:
radial-gradient(ellipse 60% 40% at 10% 0%, rgba(56,189,248,.07) 0%, transparent 55%),
radial-gradient(ellipse 50% 40% at 90% 100%, rgba(167,139,250,.04) 0%, transparent 55%);
}
/* ─ SIDEBAR ─ */
.sidebar {
width: 210px;
flex-shrink: 0;
background: var(--surf1);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 18px 0;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.sidebar-logo {
padding: 0 18px 20px;
border-bottom: 1px solid var(--border);
margin-bottom: 14px;
}
.sidebar-logo-mark {
width: 36px; height: 36px;
border-radius: 8px;
background: linear-gradient(135deg, #38bdf820, #38bdf845);
border: 1px solid #38bdf830;
display: flex; align-items: center; justify-content: center;
font-size: 18px; margin-bottom: 10px;
}
.sidebar-logo-title {
font-family: var(--sans);
font-weight: 900;
font-size: .82rem;
letter-spacing: .1em;
text-transform: uppercase;
color: #e2e8f0;
line-height: 1.3;
}
.sidebar-logo-sub {
font-family: var(--mono);
font-size: .56rem;
color: var(--muted);
letter-spacing: .08em;
margin-top: 2px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 18px;
cursor: pointer;
font-family: var(--sans);
font-weight: 700;
font-size: .76rem;
letter-spacing: .06em;
color: var(--muted);
border-right: 2px solid transparent;
transition: all .15s;
user-select: none;
}
.nav-item.active {
color: var(--blue);
background: rgba(56,189,248,.07);
border-right-color: var(--blue);
}
.nav-item:hover:not(.active) { color: var(--text); background: rgba(255,255,255,.03); }
.nav-icon { font-size: .9rem; width: 18px; text-align: center; }
/* ─ MAIN ─ */
.main {
flex: 1;
overflow-y: auto;
padding: 28px 32px;
max-width: 900px;
}
.page-title {
font-family: var(--sans);
font-weight: 900;
font-size: 1.6rem;
letter-spacing: -.01em;
color: #e2e8f0;
margin-bottom: 4px;
}
.page-sub {
font-family: var(--mono);
font-size: .68rem;
color: var(--muted);
letter-spacing: .08em;
margin-bottom: 24px;
}
/* ─ CODE CARD ─ */
.code-card {
background: var(--surf1);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: var(--surf2);
border-bottom: 1px solid var(--border);
}
.code-header-left {
display: flex;
align-items: center;
gap: 10px;
}
.code-dot {
width: 8px; height: 8px;
border-radius: 50%;
}
.code-title {
font-family: var(--sans);
font-weight: 700;
font-size: .75rem;
color: #e2e8f0;
letter-spacing: .06em;
}
.code-badge {
font-family: var(--mono);
font-size: .6rem;
padding: 2px 8px;
border-radius: 4px;
border: 1px solid;
letter-spacing: .06em;
}
.copy-btn {
font-family: var(--sans);
font-weight: 700;
font-size: .68rem;
letter-spacing: .08em;
text-transform: uppercase;
padding: 5px 12px;
border-radius: 5px;
border: 1px solid var(--border2);
background: transparent;
color: var(--muted);
cursor: pointer;
transition: all .15s;
}
.copy-btn:hover { color: var(--text); border-color: var(--blue); }
.copy-btn.copied { color: var(--green); border-color: var(--green); }
.code-body {
font-family: var(--mono);
font-size: .7rem;
line-height: 1.65;
padding: 18px 20px;
overflow-x: auto;
color: #8bafc8;
background: var(--surf1);
max-height: 520px;
overflow-y: auto;
white-space: pre;
}
/* Syntax colors */
.code-body .cm { color: #2d5070; font-style: italic; } /* comment */
.code-body .kw { color: #38bdf8; } /* keyword/builtin */
.code-body .cf { color: #a78bfa; } /* control flow */
.code-body .st { color: #86efac; } /* string */
.code-body .nm { color: #fb923c; } /* number */
.code-body .dk { color: #f59e0b; } /* decorator */
/* ─ OVERVIEW CARDS ─ */
.card-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 20px; }
.info-card {
background: var(--surf1);
border: 1px solid var(--border);
border-radius: 9px;
padding: 14px 16px;
}
.info-card-label {
font-family: var(--mono);
font-size: .58rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .1em;
margin-bottom: 5px;
}
.info-card-value {
font-family: var(--sans);
font-weight: 800;
font-size: .9rem;
color: #e2e8f0;
line-height: 1.4;
}
/* ─ RULE CARDS ─ */
.rule-card {
display: flex;
gap: 14px;
padding: 14px 16px;
background: var(--surf1);
border: 1px solid var(--border);
border-radius: 9px;
margin-bottom: 10px;
}
.rule-num {
min-width: 28px; height: 28px;
border-radius: 6px;
font-family: var(--sans);
font-weight: 900;
font-size: .8rem;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
color: #050d18;
}
.rule-title {
font-family: var(--sans);
font-weight: 700;
font-size: .8rem;
color: #e2e8f0;
margin-bottom: 4px;
}
.rule-desc {
font-family: var(--mono);
font-size: .68rem;
color: var(--muted);
line-height: 1.6;
}
/* ─ SETUP STEPS ─ */
.setup-step {
display: flex;
gap: 14px;
align-items: flex-start;
padding: 13px 0;
border-bottom: 1px solid var(--border);
}
.setup-step:last-child { border-bottom: none; }
.step-icon {
width: 28px; height: 28px;
border-radius: 6px;
background: var(--surf2);
border: 1px solid var(--border2);
display: flex; align-items: center; justify-content: center;
font-size: .85rem;
flex-shrink: 0;
}
.step-title {
font-family: var(--sans);
font-weight: 700;
font-size: .78rem;
color: #e2e8f0;
margin-bottom: 3px;
}
.step-desc {
font-family: var(--mono);
font-size: .66rem;
color: var(--muted);
line-height: 1.65;
}
.code-inline {
font-family: var(--mono);
font-size: .65rem;
background: var(--surf3);
border: 1px solid var(--border2);
border-radius: 3px;
padding: 1px 6px;
color: var(--blue);
}
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: var(--surf1); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }
`}</style>
{/* ── SIDEBAR ── */}
<nav className="sidebar">
<div className="sidebar-logo">
<div className="sidebar-logo-mark">📈</div>
<div className="sidebar-logo-title">9:30 Candle<br/>Bot v2</div>
<div className="sidebar-logo-sub">CODE REFERENCE</div>
</div>
{SECTIONS.map(s => (
<div
key={s.id}
className={`nav-item ${activeSection === s.id ? "active" : ""}`}
onClick={() => setActiveSection(s.id)}
>
<span className="nav-icon">{s.icon}</span>
{s.label}
</div>
))}
</nav>
{/* ── MAIN CONTENT ── */}
<main className="main">
{/* ─ OVERVIEW ─ */}
{activeSection === "overview" && (<>
<div className="page-title">9:30 Candle Bot v2</div>
<div className="page-sub">PINE SCRIPT v5 · PYTHON EXECUTION ENGINE · CONFLUENCE FILTERED</div>
<div className="card-grid">
{[
{ l:"Strategy Type", v:"Opening Range Breakout + Retest" },
{ l:"Timeframe", v:"1-min, 2-min or 5-min bars" },
{ l:"Trade Window", v:"09:30 – 10:45 AM ET" },
{ l:"Stop Loss", v:"1× ATR(14) from entry" },
{ l:"Take Profit", v:"2R (score<80) or 2.5R (score≥80)" },
{ l:"Min Confluence", v:"Score ≥ 65 / 100" },
{ l:"Volume Filter", v:"Break candle > 1.1× avg volume" },
{ l:"Skip Condition", v:"Setup candle > 2.1× ATR(14)" },
].map(({ l, v }) => (
<div className="info-card" key={l}>
<div className="info-card-label">{l}</div>
<div className="info-card-value">{v}</div>
</div>
))}
</div>
<div style={{ background:"var(--surf1)", border:"1px solid var(--border)", borderLeft:"3px solid var(--blue)", borderRadius:9, padding:"14px 18px", marginBottom:20 }}>
<div style={{ fontFamily:"var(--sans)", fontWeight:700, fontSize:".82rem", color:"#e2e8f0", marginBottom:8 }}>Two files delivered</div>
<div style={{ fontFamily:"var(--mono)", fontSize:".68rem", color:"var(--muted)", lineHeight:1.7 }}>
<strong style={{ color:"var(--blue)" }}>Pine Script v5</strong> — paste directly into TradingView's Pine Editor. Runs as a full <code className="code-inline">strategy()</code> with built-in backtesting, visual zone boxes, signal labels, SL/TP lines, and an info table.<br/><br/>
<strong style={{ color:"var(--amber)" }}>Python Engine</strong> — live paper-trading skeleton using Alpaca Markets API. Implements the same 4-phase state machine (setup → break → retest → confirm) with ATR sizing, OCO bracket orders, and session close management.
</div>
</div>
{/* File list */}
<div style={{ display:"flex", flexDirection:"column", gap:8 }}>
{[
{ name:"930_candle_bot_v2.pine", lang:"Pine Script v5", size:"~8 KB", color:"var(--blue)", icon:"📈" },
{ name:"930_candle_bot_v2.py", lang:"Python 3.10+", size:"~7 KB", color:"var(--amber)", icon:"🐍" },
].map(f => (
<div key={f.name} style={{ display:"flex", alignItems:"center", gap:14, padding:"12px 16px", background:"var(--surf2)", border:"1px solid var(--border)", borderRadius:8 }}>
<span style={{ fontSize:"1.2rem" }}>{f.icon}</span>
<div style={{ flex:1 }}>
<div style={{ fontFamily:"var(--mono)", fontSize:".75rem", color:f.color }}>{f.name}</div>
<div style={{ fontFamily:"var(--mono)", fontSize:".62rem", color:"var(--muted)", marginTop:2 }}>{f.lang} · {f.size}</div>
</div>
</div>
))}
</div>
</>)}
{/* ─ PINE SCRIPT ─ */}
{activeSection === "pine" && (<>
<div className="page-title">Pine Script v5</div>
<div className="page-sub">TRADINGVIEW STRATEGY · PASTE INTO PINE EDITOR · WORKS ON 1-5 MIN CHARTS</div>
<CodeBlock
code={PINE}
lang="pine"
title="930_candle_bot_v2.pine"
badge="Pine Script v5"
/>
<div style={{ background:"var(--surf1)", border:"1px solid rgba(56,189,248,.2)", borderRadius:9, padding:"14px 18px" }}>
<div style={{ fontFamily:"var(--sans)", fontWeight:700, fontSize:".78rem", color:"var(--blue)", marginBottom:8 }}>How to use in TradingView</div>
{[
"Open TradingView → Pine Editor (bottom panel)",
"Delete any existing code and paste the full script above",
"Click Save → Add to chart",
"Set chart to 1-min or 5-min on SPY, QQQ, NQ, ES, etc.",
"Adjust inputs: ATR period, min score, time window, R:R in the Settings panel",
"Use the Strategy Tester tab to review backtested performance",
].map((s, i) => (
<div key={i} style={{ display:"flex", gap:10, marginBottom:5, fontFamily:"var(--mono)", fontSize:".68rem", color:"var(--muted)" }}>
<span style={{ color:"var(--blue)", minWidth:16 }}>{i+1}.</span>{s}
</div>
))}
</div>
</>)}
{/* ─ PYTHON ─ */}
{activeSection === "python" && (<>
<div className="page-title">Python Engine</div>
<div className="page-sub">ALPACA MARKETS API · PAPER TRADING SKELETON · SAME 4-PHASE STATE MACHINE</div>
<CodeBlock
code={PYTHON}
lang="python"
title="930_candle_bot_v2.py"
badge="Python 3.10+"
/>
<div style={{ background:"var(--surf1)", border:"1px solid rgba(245,158,11,.2)", borderRadius:9, padding:"14px 18px" }}>
<div style={{ fontFamily:"var(--sans)", fontWeight:700, fontSize:".78rem", color:"var(--amber)", marginBottom:8 }}>Installation & Setup</div>
{[
["Install deps", "pip install alpaca-trade-api pandas numpy"],
["Set env vars", "export APCA_API_KEY_ID=your_key\nexport APCA_API_SECRET_KEY=your_secret\nexport APCA_API_BASE_URL=https://paper-api.alpaca.markets"],
["Run", "python 930_candle_bot_v2.py"],
["Change symbol", "Edit SYMBOL = 'SPY' at the top of the file"],
["Switch to live", "Change base URL to https://api.alpaca.markets (REAL MONEY — test thoroughly first)"],
].map(([title, code], i) => (
<div key={i} style={{ marginBottom:10 }}>
<div style={{ fontFamily:"var(--sans)", fontWeight:700, fontSize:".72rem", color:"var(--amber)", marginBottom:3 }}>{i+1}. {title}</div>
<pre style={{ fontFamily:"var(--mono)", fontSize:".65rem", color:"var(--muted)", background:"var(--surf2)", border:"1px solid var(--border)", borderRadius:5, padding:"7px 10px", overflowX:"auto" }}>{code}</pre>
</div>
))}
</div>
</>)}
{/* ─ SETUP GUIDE ─ */}
{activeSection === "setup" && (<>
<div className="page-title">Setup Guide</div>
<div className="page-sub">TRADINGVIEW + ALPACA · STEP-BY-STEP</div>
<div style={{ background:"var(--surf1)", border:"1px solid var(--border)", borderRadius:10, padding:"16px 18px", marginBottom:20 }}>
{[
{ icon:"📈", title:"TradingView — Pine Script",
desc:<>Open TradingView, go to <strong>Pine Editor</strong>, paste the Pine Script, click <strong>Add to chart</strong>. Best on <code className="code-inline">1-min</code> or <code className="code-inline">5-min</code> SPY/QQQ/NQ1!/ES1!. Use the <strong>Strategy Tester</strong> to backtest before using it live.</> },
{ icon:"🐍", title:"Python — Install Alpaca",
desc:<>Run <code className="code-inline">pip install alpaca-trade-api pandas numpy</code>. Create a free Alpaca account at alpaca.markets and get your <strong>paper trading API keys</strong>. Set the three environment variables shown in the Python file header.</> },
{ icon:"⚙", title:"Configure the Bot",
desc:<>Edit the CONFIG block at the top of the Python file: set <code className="code-inline">SYMBOL</code>, <code className="code-inline">BAR_MINS</code> (1 or 5), <code className="code-inline">RISK_PCT</code> (recommend 0.01 = 1%), and <code className="code-inline">MIN_SCORE</code> (65 minimum). Start with paper trading for at least 2 weeks.</> },
{ icon:"🕐", title:"Time Zone",
desc:<>Both files are coded for <strong>America/New_York (ET)</strong>. The trade window is 09:30–10:45 AM. Outside this window no new trades are taken. All positions are force-closed at 10:45 to avoid lunch-hour chop.</> },
{ icon:"⚠", title:"Risk Warning",
desc:<>This is educational code. Never run the live (non-paper) API without thorough backtesting and forward-testing first. Position sizing defaults to 1% risk per trade. Do not increase this until you have 50+ paper trades of consistent results.</> },
].map((s, i) => (
<div key={i} className="setup-step">
<div className="step-icon">{s.icon}</div>
<div>
<div className="step-title">{s.title}</div>
<div className="step-desc">{s.desc}</div>
</div>
</div>
))}
</div>
</>)}
{/* ─ RULES ─ */}
{activeSection === "rules" && (<>
<div className="page-title">Strategy Rules</div>
<div className="page-sub">ALL 5 PHASES · SAME LOGIC IN BOTH PINE SCRIPT AND PYTHON</div>
{RULES.map(r => (
<div key={r.n} className="rule-card">
<div className="rule-num" style={{ background: r.color }}>{r.n}</div>
<div>
<div className="rule-title">{r.title}</div>
<div className="rule-desc">{r.desc}</div>
</div>
</div>
))}
<div style={{ background:"var(--surf1)", border:"1px solid var(--border)", borderRadius:9, padding:"14px 16px", marginTop:8 }}>
<div style={{ fontFamily:"var(--sans)", fontWeight:700, fontSize:".78rem", color:"#e2e8f0", marginBottom:10 }}>Confluence Scoring Breakdown</div>
{[
["+20","Trend aligned with EMA-20 (same direction)"],
["+8", "EMA-20 neutral (within 0.1% of price)"],
["−10","Counter-trend trade"],
["+10","Break candle volume > 1.4× avg"],
["+5", "Break candle volume > 1.1× avg"],
["+10","Break candle body ≥ 65%"],
["+5", "Break candle body ≥ 50%"],
["−5", "Break candle body < 50%"],
["+10","Retest depth < 25% of zone"],
["+5", "Retest depth 25–40% of zone"],
["−8", "Retest depth > 40% of zone"],
["+10","Confirm candle body ≥ 65%"],
["+5", "Setup ATR ratio 0.8–1.8×"],
["−10","Setup ATR ratio > 2.0×"],
["+5", "Confirm closes beyond break candle extreme"],
].map(([pts, desc]) => (
<div key={desc} style={{ display:"flex", gap:12, padding:"4px 0", borderBottom:"1px solid var(--border)", fontFamily:"var(--mono)", fontSize:".66rem" }}>
<span style={{ minWidth:32, color: pts.startsWith("+") ? "var(--green)" : "var(--red)", fontWeight:600 }}>{pts}</span>
<span style={{ color:"var(--muted)" }}>{desc}</span>
</div>
))}
<div style={{ fontFamily:"var(--mono)", fontSize:".66rem", color:"var(--blue)", marginTop:8 }}>
Base score: 40 · Max: 100 · Entry threshold: ≥ 65 · High-grade: ≥ 80 (→ 2.5R target)
</div>
</div>
</>)}
</main>
</div>
);
}
11.05.2026 г., 00:02:30 ч.