//@version=6 strategy("Options BOS v6 — Valid Pullback Research", overlay=true, pyramiding=0, process_orders_on_close=false, calc_on_every_tick=false, calc_on_order_fills=false, default_qty_type=strategy.fixed, default_qty_value=1, initial_capital=10000, max_lines_count=500, max_labels_count=500, max_bars_back=500) // Developed from the user's Valid BOS Pullback v2-v5. One Pine strategy file. // Underlying structure signals are NOT historical option profits. // Optional Strategy Tester orders are stock-direction proxies, never option orders. // No brokerage connection. Order-fill alerts are disabled. gSignal = "01 · Original BOS structure" window = input.string("1–3 days", "Expiration window", options=["1–3 days", "7–14 days"], group=gSignal) pivotLength = input.int(5, "Strict confirmed swing length", minval=1, maxval=100, group=gSignal) atrLength = input.int(14, "ATR length", minval=1, group=gSignal) minBars = input.int(5, "Minimum bars since impulse extreme", minval=1, group=gSignal) maxBars = input.int(40, "Maximum pullback bars", minval=1, group=gSignal) tolATR = input.float(0.25, "Frozen retest / hold tolerance (ATR)", minval=0, step=0.05, group=gSignal) holdMode = input.string("Close", "Hold rule", options=["Close", "Wick"], group=gSignal) needConfirm = input.bool(true, "Require directional pullback confirmation candle", group=gSignal) breakByClose = input.bool(true, "Confirm BOS by close (off = wick at bar close)", group=gSignal) maxWaitBOS = input.int(0, "Maximum bars after valid pullback (0 = unlimited)", minval=0, group=gSignal) entryMode = input.string("BOS", "Entry research model", options=["Pullback", "BOS"], group=gSignal) stopBufATR = input.float(0.1, "Fixed stop buffer (setup ATR)", minval=0, step=0.05, group=gSignal) rr = input.float(2, "Underlying target R multiple", minval=0.5, step=0.5, group=gSignal) allowCalls = input.bool(true, "Enable purchased Call candidates", group=gSignal) allowPuts = input.bool(true, "Enable purchased Put candidates", group=gSignal) gFilter = "02 · Optional candidate filters" useTrend = input.bool(false, "Enable chart-timeframe EMA filter", group=gFilter) emaLength = input.int(50, "Trend EMA length", minval=1, group=gFilter) volumeMultiple = input.float(0, "Minimum relative volume (0 = off)", minval=0, step=0.1, group=gFilter) regularHours = input.bool(true, "Restrict intraday entries to regular hours", group=gFilter) marketSession = input.session("0930-1600", "New York entry session", group=gFilter) eventBlock = input.bool(false, "Block entries for known earnings / events (manual)", group=gFilter) allowCustom = input.bool(false, "Allow an additional technology stock (user verified)", group=gFilter) // Filters gate entry candidates, not structural success counters. gDisplay = "05 · Research and display" proxyEnabled = input.bool(false, "Enable stock proxy backtest (NOT option returns)", group=gDisplay) maxHoldBars = input.int(20, "Maximum stock proxy holding bars", minval=1, group=gDisplay) infoAlerts = input.bool(false, "Enable informational candidate alerts", group=gDisplay) showStructure = input.bool(true, "Show structure events and reference lines", group=gDisplay) showMA = input.bool(false, "Show chart EMA", group=gDisplay) showRisk = input.bool(true, "Show fixed underlying risk levels", group=gDisplay) displayMode = input.string("History", "Trade display", options=["History", "Latest"], group=gDisplay) retainTrades = input.int(30, "Maximum displayed trade cards", minval=1, maxval=60, group=gDisplay) if barstate.isfirst and maxBars < minBars runtime.error("Maximum pullback bars must be at least minimum pullback bars.") gPlan = "03 · Manual option plan" optionType = input.string("Call", "Purchased option type", options=["Call", "Put"], group=gPlan) strike = input.float(0, "Actual option strike ($)", minval=0, group=gPlan) entryPremium = input.float(0, "Actual entry premium per share ($)", minval=0, step=0.01, group=gPlan) exitPremium = input.float(0, "Current / planned exit premium per share ($)", minval=0, step=0.01, group=gPlan) contracts = input.int(1, "Option contracts (100 shares each)", minval=1, maxval=10000, group=gPlan) entryDate = input.time(0, "Actual option entry date/time", group=gPlan) expiryDate = input.time(0, "Actual option expiration date/time", group=gPlan) includeSell = input.bool(true, "Include a closing sell execution", group=gPlan) premiumTick = input.float(0.01, "Allowed premium price increment ($)", minval=0.01, step=0.01, group=gPlan) planCapital = input.float(10000, "Planning capital ($)", minval=1, group=gPlan) riskPercent = input.float(2, "Maximum premium capital at risk (%)", minval=0.1, maxval=100, step=0.1, group=gPlan) profitTarget = input.float(25, "Target net return on entry cost (%)", minval=0, step=1, group=gPlan) gFees = "04 · Sahm fees (per execution)" vatEnabled = input.bool(true, "Apply Saudi VAT: 15% on fees", group=gFees) orfRate = input.float(0.01171, "Estimated ORF per contract ($)", minval=0, step=0.00001, group=gFees) secRate = input.float(27.8, "Estimated SEC fee per $1 million of sell proceeds", minval=0, group=gFees) tafRate = input.float(0.00279, "Estimated FINRA TAF per sold contract ($)", minval=0, step=0.00001, group=gFees) tafCap = input.float(7.27, "TAF cap per sell execution ($; 0 = no cap)", minval=0, group=gFees) regConfirmed = input.bool(false, "Confirm SEC / TAF inputs (zero only if applicable)", group=gFees) // SEC/TAF defaults are user-supplied estimates, not independently verified current rates. // Full precision is retained; display rounds to cents. Invoice rounding may differ. f_fee(float premium, int qty, bool selling) => bool low = premium <= 0.10 float commission = math.max(qty * (low ? 0.49 : 0.99), low ? 0.99 : 1.99) float settlement = qty * 0.18 float occ = math.min(qty * 0.02, 55.0) float orf = qty * orfRate float sec = selling ? premium * 100 * qty * secRate / 1000000 : 0 float tafRaw = selling ? qty * tafRate : 0 float taf = tafCap > 0 ? math.min(tafRaw, tafCap) : tafRaw float subtotal = commission + settlement + occ + orf + sec + taf float vat = vatEnabled ? subtotal * 0.15 : 0 [commission, settlement, occ, orf, sec, taf, vat, subtotal + vat] f_total(float premium, bool selling) => [commission, settlement, occ, orf, sec, taf, vat, total] = f_fee(premium, contracts, selling) total f_net(float sellPremium) => (sellPremium - entryPremium) * 100 * contracts - f_total(entryPremium, false) - (includeSell ? f_total(sellPremium, true) : 0) // Integer-tick search within each fee tier separately. A single global binary search // would be incorrect across the discontinuity at a premium of $0.10. f_target(float desiredNet) => int lastLow = int(math.floor((0.10 + 0.000000000001) / premiumTick)) int firstHigh = lastLow + 1 bool lowWorks = f_net(lastLow * premiumTick) >= desiredNet int lo = lowWorks ? 0 : firstHigh int hi = lowWorks ? lastLow : math.max(firstHigh, int(math.ceil((entryPremium + 1) / premiumTick))) int expand = 0 while not lowWorks and f_net(hi * premiumTick) < desiredNet and expand < 30 hi *= 2 expand += 1 bool possible = f_net(hi * premiumTick) >= desiredNet int iterations = 0 while possible and lo < hi and iterations < 60 int mid = int(math.floor((lo + hi) / 2.0)) if f_net(mid * premiumTick) >= desiredNet hi := mid else lo := mid + 1 iterations += 1 possible ? lo * premiumTick : na allowed = syminfo.ticker == "AAPL" or syminfo.ticker == "MSFT" or syminfo.ticker == "NVDA" or syminfo.ticker == "AMD" or syminfo.ticker == "GOOGL" or syminfo.ticker == "META" or syminfo.ticker == "AMZN" or syminfo.ticker == "AVGO" or syminfo.ticker == "ORCL" or syminfo.ticker == "CRM" or syminfo.ticker == "ADBE" or syminfo.ticker == "PLTR" supported = chart.is_standard and syminfo.type == "stock" and (allowed or allowCustom) and (timeframe.isintraday or timeframe.isdaily) atr = ta.atr(atrLength) ema = ta.ema(close, emaLength) volAvg = ta.sma(volume, 20) sessionOK = not regularHours or not timeframe.isintraday or not na(time(timeframe.period, marketSession, "America/New_York")) volumeOK = volumeMultiple == 0 or (not na(volume) and not na(volAvg) and volume >= volumeMultiple * volAvg) // Strict pivots avoid ambiguous equal-high/low clusters, and match the offline oracle. f_strict(bool upper) => float candidate = upper ? high[pivotLength] : low[pivotLength] bool valid = bar_index >= 2 * pivotLength for k = 0 to 2 * pivotLength if k != pivotLength valid := valid and (upper ? high[k] < candidate : low[k] > candidate) valid ? candidate : na ph = f_strict(true) pl = f_strict(false) var float lastPH = na var float lastPL = na var int phConfirmed = na var int plConfirmed = na var bool phUsed = true var bool plUsed = true var int state = 0 var int dir = 0 var float ref = na var float leg = na var int legBar = na var int pbBar = na var float pbLow = na var float setupATR = na var float tolerance = na var int pbCount = 0 var int bosCount = 0 var int failCount = 0 bool pbSignal = false bool bosSignal = false bool failed = false float candidateStop = na float candidateInvalidation = na float structureLevel = na int signalDir = 0 if barstate.isconfirmed if not na(ph) lastPH := ph phConfirmed := bar_index phUsed := false if not na(pl) lastPL := pl plConfirmed := bar_index plUsed := false if state == 0 and not na(atr) bool up = not phUsed and not na(lastPH) and phConfirmed < bar_index and close > lastPH bool down = not plUsed and not na(lastPL) and plConfirmed < bar_index and close < lastPL if up or down dir := up ? 1 : -1 if up phUsed := true else plUsed := true ref := up ? lastPH : -lastPL leg := up ? high : -low legBar := bar_index pbLow := leg setupATR := atr tolerance := tolATR * setupATR state := 1 else if state != 0 float nh = dir == 1 ? high : -low float nl = dir == 1 ? low : -high float nc = dir * close float no = dir * open bool failHold = (holdMode == "Close" ? nc : nl) < ref - tolerance bool expired = state == 1 ? bar_index - legBar > maxBars : maxWaitBOS > 0 and bar_index - pbBar > maxWaitBOS // Failure takes priority, including an outside bar that also breaks the impulse. if failHold or expired state := 0 failCount += 1 failed := true if showStructure label.new(bar_index, close, expired ? "Expired" : "Invalidation", color=color.new(color.gray, 60), textcolor=color.white, size=size.tiny) else if state == 1 if nh > leg leg := nh legBar := bar_index pbLow := leg else pbLow := math.min(pbLow, nl) bool confirmed = not needConfirm or (nc > no and nc > ref) if pbLow <= ref + tolerance and bar_index - legBar >= minBars and confirmed state := 2 pbBar := bar_index pbCount += 1 pbSignal := true signalDir := dir structureLevel := dir * ref candidateStop := dir * (pbLow - stopBufATR * setupATR) candidateInvalidation := dir * (ref - tolerance) else float priorLow = pbLow pbLow := math.min(pbLow, nl) if (breakByClose ? nc : nh) > leg bosCount += 1 bosSignal := true signalDir := dir structureLevel := dir * leg // Use only the protective extreme known BEFORE the BOS candle. candidateStop := dir * (priorLow - stopBufATR * setupATR) candidateInvalidation := dir * (ref - tolerance) state := 1 ref := leg leg := nh legBar := bar_index pbLow := leg setupATR := atr tolerance := tolATR * setupATR var array