Project Black-Box — Iron Sight Developer Guide — REACTION-DOJI-V1
This Pine Script identifies high-probability reversal points on any chart. It combines three independent signals that must all line up at the same time before it fires a trade entry. Think of it like a three-step checklist — the trade only executes when every box is ticked.
// @S3 Development | Project Black-Box // Level Reaction & Doji Strategy v1.3 //@version=6 strategy("Level Reaction & Doji Strategy", overlay=true, initial_capital=100000, default_qty_type=strategy.cash, currency=currency.USD)
Tells TradingView this is a backtest-capable strategy (not just an indicator).
overlay=true draws everything directly on the price chart instead of a separate panel.
The strategy() call unlocks strategy.entry() and strategy.exit() —
the commands that simulate (and ultimately trigger) live trade orders through the NIGHTHAWK pipeline.
// Level Settings lookback = input.int(20, "Swing Lookback (Bars)") show_levels = input.bool(true, "Show Support/Resistance Lines") // Doji Settings doji_pct = input.float(10.0, "Doji Body % of Total Range", minval=1.0, maxval=30.0) // Risk Management (Sentinel) tp_ticks = input.int(100, "Take Profit (Ticks)") sl_ticks = input.int(50, "Stop Loss (Ticks)")
Creates a settings panel inside TradingView where you can adjust the strategy without touching the code.
Each input.* call adds a row to the strategy's Settings dialog.
How many bars left and right the script looks when hunting for a swing high or low. 20 means the pivot must be the highest/lowest point over a 41-bar window (20 + 1 + 20).
A candle qualifies as a Doji only if its body is smaller than 10% of its full wick-to-wick range. Lower values = stricter, rarer signals. Higher values = looser, more frequent.
Take Profit distance in ticks. SENTINEL closes the trade automatically once price moves this many ticks in your favour.
Stop Loss distance in ticks. SENTINEL ejects the trade if price moves this many ticks against you.
// Find the highest point that has 'lookback' lower bars on both sides high_level = ta.pivothigh(high, lookback, lookback) low_level = ta.pivotlow(low, lookback, lookback) var float last_res = na // Last known Resistance var float last_sup = na // Last known Support if not na(high_level) last_res := high_level if not na(low_level) last_sup := low_level // Draw the levels on screen plot(show_levels ? last_res : na, "Resistance", color.new(color.red, 40), linewidth=2) plot(show_levels ? last_sup : na, "Support", color.new(color.green, 40), linewidth=2)
ta.pivothigh() scans back lookback bars to the left and right.
If the current bar's high is the tallest in that whole window, it's a confirmed Pivot High = Resistance.
Same logic applies in reverse for ta.pivotlow() = Support.
var float makes the variables persistent — they keep their last value until a new pivot is found.
Markets respect price memory. Institutional traders place large orders at previous swing highs/lows. By detecting these levels algorithmically, the strategy knows exactly where to watch for a reaction — the same areas other traders are watching.
// How big is the candle's body (open vs close)? body_size = math.abs(close - open) // How wide is the candle's full wick-to-wick range? total_range = high - low // Doji = body is tiny relative to the full range is_doji = total_range > 0 and body_size <= (total_range * (doji_pct / 100))
Measures the candle's body size as a percentage of its full range. If the body is smaller than
doji_pct (default 10%), is_doji becomes true for that bar.
The total_range > 0 guard prevents divide-by-zero on flat bars.
A Doji means buyers and sellers were exactly matched during that candle — neither side won. When this happens at a key level, it tells us the level is being respected. The indecision is a warning that a reversal may be loading.
// How far is current price from each key level? (as a % of level price) dist_res = math.abs(high - last_res) / last_res dist_sup = math.abs(low - last_sup) / last_sup // Are we within 0.2% of a level? (close enough to "touch" it) at_resistance = dist_res < 0.002 at_support = dist_sup < 0.002 // Reaction: the candle AFTER the Doji breaks out decisively bear_reaction = is_doji[1] and close < low[1] and close < open // Break below Doji bull_reaction = is_doji[1] and close > high[1] and close > open // Break above Doji
dist_res and dist_sup measure how close price is to each level as a decimal fraction.
If less than 0.2%, we say price is at that level.
The [1] notation means "one bar ago". So is_doji[1] asks: "was the previous bar a Doji?"
The Doji alone is not a signal — it's a setup. The reaction candle is the trigger. Requiring the next candle to close beyond the Doji's range confirms that one side won the battle. We only trade when there is confirmation, not just indecision.
// LONG: Support level + Doji appeared at support + Bull reaction candle long_condition = bull_reaction and at_support[1] // SHORT: Resistance level + Doji appeared at resistance + Bear reaction candle short_condition = bear_reaction and at_resistance[1]
Pine Script's and keyword means ALL conditions must be true simultaneously.
If any one of them is false, the entire expression is false and no trade fires.
This is the core of the confluence model — layering filters to reduce false signals.
bull_reaction = TRUE AND at_support[1] = TRUE = LONG ENTRY FIRES
bull_reaction = FALSE AND at_support[1] = TRUE = NO TRADE — WAIT
log.info("SYSLOG | {0} | TF: {1} | Price: {2} | SR: [{3}/{4}] | Doji: {5} | Near: [S:{6}/R:{7}] | Signal: {8}", syminfo.ticker, timeframe.period, str.tostring(close), str.tostring(math.round(last_sup, 2)), str.tostring(math.round(last_res, 2)), is_doji ? "Y" : "N", at_support ? "Y" : "N", at_resistance ? "Y" : "N", long_condition ? "LONG" : short_condition ? "SHORT" : "NONE")
Writes a structured log line to TradingView's Pine Logs console on every single bar.
You can see this in the Strategy Tester under the "Pine Logs" tab.
The {0}, {1}... placeholders are filled in with live values in order.
SYSLOG | BTCUSD | TF: 5 | Price: 68240.5 | SR: [67800.00/68500.00] | Doji: Y | Near: [S:N/R:Y] | Signal: NONE SYSLOG | BTCUSD | TF: 5 | Price: 67810.2 | SR: [67800.00/68500.00] | Doji: Y | Near: [S:Y/R:N] | Signal: NONE SYSLOG | BTCUSD | TF: 5 | Price: 68120.0 | SR: [67800.00/68500.00] | Doji: N | Near: [S:N/R:N] | Signal: LONG
The third line shows a LONG signal firing — note how the Doji was on the previous bar (now N) but conditions aligned.
if long_condition msg = '{"secret": "tac_com_alpha_9", "model_id": "REACTION-DOJI-V1", ' + '"ticker": "' + syminfo.tickerid + '", "action": "BUY", ' + '"price": ' + str.tostring(close) + ', ' + '"metadata": {"type": "Support_Doji_Reversal", ' + '"level": ' + str.tostring(last_sup) + ', "timestamp": ' + str.tostring(timenow) + '}}' log.error("SIGNAL: Bullish Reversal Detected. Payload: {0}", msg) strategy.entry("DOJI_LONG", strategy.long, alert_message=msg)
When long_condition is true, it builds a JSON string and passes it to
strategy.entry() as the alert_message.
When TradingView fires the strategy alert, this JSON is sent as the HTTP body of a webhook POST request
to our NIGHTHAWK server.
{
"secret": "tac_com_alpha_9", // Auth token — NIGHTHAWK rejects if wrong
"model_id": "REACTION-DOJI-V1", // Which strategy fired
"ticker": "COINBASE:BTCUSD", // TradingView symbol ID
"action": "BUY", // or "SELL"
"price": 68240.5, // Close price at signal bar
"metadata": {
"type": "Support_Doji_Reversal",
"level": 67800.00, // The support level that was respected
"timestamp": 1745000000000 // Unix ms timestamp from TradingView
}
}
strategy.exit("EXIT_LONG", "DOJI_LONG", profit=tp_ticks, loss=sl_ticks, comment="TP/SL") strategy.exit("EXIT_SHORT", "DOJI_SHORT", profit=tp_ticks, loss=sl_ticks, comment="TP/SL")
strategy.exit() attaches automatic closing rules to each open position.
profit=100 means: close the trade if it moves 100 ticks in your favour.
loss=50 means: close it if it moves 50 ticks against you.
Automated exits remove emotion from the equation. The system never hopes a losing trade recovers — it closes it and protects capital. These parameters feed directly into SENTINEL's backtesting P&L calculations.
Take Profit: +100 ticks Stop Loss: -50 ticks Risk-Reward: 2:1
At 2:1 R:R, you only need to win 34% of trades to break even. The goal is to set these values based on historical win rate from the backtester.
var dash = table.new(position.top_right, 2, 6, border_width=1, ...) if barstate.islast // Score each condition independently p1 = at_support or at_resistance or at_support[1] or at_resistance[1] p2 = is_doji or is_doji[1] p3 = bull_reaction or bear_reaction total_score = (p1 ? 1 : 0) + (p2 ? 1 : 0) + (p3 ? 1 : 0) // Score 3/3 → EXECUTE, otherwise → WAIT scoreColor = total_score == 3 ? color.new(color.green, 50) : color.new(color.gray, 50)
Creates a 2-column, 6-row table in the top-right corner of the chart.
It only updates on the most recent bar (barstate.islast).
Each condition is scored independently — you can see how many have aligned even before a signal fires.
At a glance, a trader can see whether the market is setting up (score: 2/3) or already in signal condition (score: 3/3). It replaces manual visual scanning with automated confluence tracking.
Live Dashboard Preview (Score: 3/3)
Dashboard Preview (Score: 1/3 — Waiting)
test_trigger = input.bool(false, "FORCE MANUAL TRIGGER", group=group_sr) if test_trigger and barstate.islast test_msg = '{"secret": "tac_com_alpha_9", "model_id": "REACTION-DOJI-V1", ' + '"action": "TEST", "price": ' + str.tostring(close) + ', ' + '"metadata": {"note": "MANUAL_TRIGGER", ...}}' log.info("TACTICAL: Manual Test Triggered. Payload: {0}", test_msg) alert(test_msg, alert.freq_all)
Adds a toggle checkbox to the strategy settings. When switched ON, it fires a test webhook on the current bar
using alert() — a different mechanism from strategy.entry().
This lets you verify the NIGHTHAWK pipeline is alive without waiting for a real signal to form.
strategy.entry() alerts
Fired by TradingView when a strategy order is triggered. Set alert condition to Order fills.
Used for live production signals.
alert() function alerts
Fired explicitly in code. Set alert condition to Any alert() function call.
Used by the manual test block for pipeline verification.
| Variable | Type | Meaning | Used For |
|---|---|---|---|
last_res |
float | Most recent pivot high price (Resistance) | Level proximity check, chart line |
last_sup |
float | Most recent pivot low price (Support) | Level proximity check, chart line |
is_doji |
bool | True if current bar's body < doji_pct% of range | Setup detection, bgcolor highlight |
at_resistance |
bool | True if price is within 0.2% of last_res | short_condition gate, dashboard row |
at_support |
bool | True if price is within 0.2% of last_sup | long_condition gate, dashboard row |
bull_reaction |
bool | Doji on prev bar, today closes above Doji high and bullish | long_condition trigger |
bear_reaction |
bool | Doji on prev bar, today closes below Doji low and bearish | short_condition trigger |
long_condition |
bool | All 3 bullish conditions aligned | Fires BUY signal to NIGHTHAWK |
short_condition |
bool | All 3 bearish conditions aligned | Fires SELL signal to NIGHTHAWK |