Three automated ETH long bots recently passed the qualification gate for production trading. Their historical backtests all came back negative. During a live overnight paper-trading session, one of them opened one trade and closed it for +$41.50.
A number like that feels like proof, but it isn’t. It’s still a useful piece of evidence as long as it’s read correctly.
This article goes through the architecture, the signal logic, the execution model and the risk framework shared by all three bots. It covers what happened to each one during the overnight window, why they passed qualification, and why the profitable result should be read with care. All code, formulas and algorithms are written as pseudocode so the logic is easy to follow and doesn’t depend on any language or framework.
The Session at a Glance
The test window ran from 18:00 on September 24, 2026 to 09:30 on September 25, 2026, covering the overnight stretch of CME ETH futures trading. Each bot trades long only and was running in paper mode against live market data.
Bot Overnight Result Status bar_eth_long_20260924_104027 +$41.50 (1 trade, 1 win) ✅ Profitable bar_eth_long_20260914_194814 No trades ⚠️ Data gaps bar_eth_long_20260918_012747 No trades ⚠️ Gateway disconnections
In short, one bot traded and won, one never got clean data, and one kept losing its connection to the broker gateway. That’s one data point for strategy performance and two for infrastructure problems, and the infrastructure findings may matter more.
All of these bots are just samplings of what you expect at our bot marketplace at hftcode.com
Part 1: Bot Architecture Overview
The shared foundation
All three bots inherit from one base class, RedisEventDrivenTradingBot. They have the same skeleton and differ mainly in configuration. That’s a good design choice because a fix to the foundation reaches every bot, and differences in behavior can be traced to parameters rather than code.
The base class supplies five core components:
Redis-based market data streaming. Bars arrive as asynchronous events. The bot doesn’t poll. It waits for a bar-close message and reacts.
Rithmic gateway integration. This is the link to live CME ETH futures data and order routing.
Bar-aggregated technical signals. There’s no tick-by-tick logic. Every decision is made when a bar closes.
Position tracking with active stop and target management. Each open position carries its own protective stop and profit target.
A drawdown guard. A hard cap forces the bot flat once drawdown reaches 15%.
Why event-driven and bar-based?
Bar-based logic gives up speed in exchange for stability. Tick-level strategies react to every print, which makes them sensitive to noise, latency and data-feed problems. Bar-close logic only makes decisions at defined moments, so it is:
Deterministic. The same bars always produce the same decisions, which keeps backtests and live runs comparable.
Cheap to compute. Nothing happens between bar closes.
Tolerant of small latency. A few hundred milliseconds of delay barely matters when decisions happen every few minutes.
The weakness is that a bar-close system depends completely on getting every bar. If a bar goes missing, the indicators built on it are wrong. That’s exactly what hurt Bot #1, as covered below.
The dual-timeframe execution model
The most interesting architectural choice is that signal detection and order execution run on separate timeframes.
Signal timeframe (10m, 15m or 30m bars): decides whether a trade opportunity exists.
Execution timeframe (2m, 3m or 5m bars): decides when to act on it.
The pairs give signal-to-execution ratios of roughly 5:1 or 6:1:
PSEUDOCODE - Timeframe Ratio Pairs
──────────────────────────────────
PAIR A: signal = 10 min, execution = 2 min → ratio = 10 / 2 = 5
PAIR B: signal = 15 min, execution = 3 min → ratio = 15 / 3 = 5
PAIR C: signal = 30 min, execution = 5 min → ratio = 30 / 5 = 6
The reason for this setup is that slower bars produce cleaner signals but respond slowly. If a 30-minute bot could only enter at 30-minute closes, it might see a crossover and then wait up to half an hour to act while price moves on. With a separate execution clock, the slow timeframe answers whether to trade and the fast timeframe answers when, so the bot keeps the cleaner signal without the full delay.
The full data pipeline
Here’s the complete decision loop:
PSEUDOCODE - Market Data Pipeline
─────────────────────────────────
LOOP forever:
1. WAIT for bar-close event from Redis
2. IF event is a SIGNAL bar close:
UPDATE ema_fast (period 9)
UPDATE ema_slow (period 21)
UPDATE momentum_score
3. CHECK gates:
IF signal_bar_count < 20: SKIP (warmup not complete)
IF momentum_score < 40: SKIP (insufficient momentum)
4. IF ema_fast crosses ABOVE ema_slow:
SET pending_long_signal = TRUE
5. IF event is an EXECUTION bar close AND pending_long_signal:
reference_price = (bar.high + bar.low) / 2
PLACE long order at reference_price
SET stop = reference_price - (ATR × 1.0)
SET target = reference_price + (ATR × 6.0)
SET pending_long_signal = FALSE
6. WHILE position is open, on each bar:
IF price >= target: CLOSE position (profit)
IF price <= stop: CLOSE position (loss)
IF drawdown >= 15%: FORCE CLOSE (safety)
7. IF ema_fast crosses BELOW ema_slow AND position is open:
CLOSE position (signal reversal)
The following sections go through each stage.
Part 2: The Signal Engine
Exponential moving averages
The core signal is a crossover between a 9-period and a 21-period exponential moving average. An EMA weights recent prices more heavily than older ones:
PSEUDOCODE - EMA Calculation
────────────────────────────
FUNCTION update_ema(previous_ema, new_close, period):
smoothing = 2 / (period + 1)
RETURN (new_close × smoothing) + (previous_ema × (1 - smoothing))
// Seeding: the first EMA value is typically the simple average
// of the first `period` closes
FUNCTION seed_ema(closes, period):
RETURN SUM(closes[0 .. period-1]) / period
For the 9-period EMA, the smoothing factor is 2/10 = 0.20, so each new bar contributes 20% of the value. For the 21-period EMA it’s 2/22 ≈ 0.091, or about 9%. The fast line follows price closely and the slow line lags behind it.
Detecting the crossover
A crossover is a change in state. It isn’t the same as the fast EMA simply sitting above the slow one. The bot has to compare the current bar with the previous bar:
PSEUDOCODE - Crossover Detection
────────────────────────────────
FUNCTION crossed_above(fast_now, slow_now, fast_prev, slow_prev):
RETURN (fast_prev <= slow_prev) AND (fast_now > slow_now)
FUNCTION crossed_below(fast_now, slow_now, fast_prev, slow_prev):
RETURN (fast_prev >= slow_prev) AND (fast_now < slow_now)
This distinction matters. A bot that tested only fast > slow would re-trigger on every bar of an uptrend and keep trying to enter. Detecting the transition makes each crossover a one-time event.
Gate 1: Warmup
The bot won’t act until it has seen at least 20 signal bars. The reason is that an EMA’s early values are heavily shaped by how it was seeded. A 21-period EMA computed from 5 bars doesn’t mean much yet.
PSEUDOCODE - Warmup Gate
────────────────────────
IF signal_bar_count < 20:
LOG "warming up"
RETURN no_signal
It’s worth noting that 20 bars is slightly fewer than the slow EMA’s 21-bar period, and the effect of the seed takes longer than one period to fade. A stricter warmup of two to three times the slow period would give more reliable values. It’s a small detail but it becomes important after restarts, which the gateway-disconnection bot probably went through many times.
The warmup period also varies a lot by timeframe:
PSEUDOCODE - Warmup Duration by Timeframe
─────────────────────────────────────────
warmup_minutes = 20 × signal_timeframe_minutes
10-min signals → 200 minutes (~3.3 hours)
15-min signals → 300 minutes (5 hours)
30-min signals → 600 minutes (10 hours)
A 30-minute bot that restarts at 18:00 wouldn’t be able to trade until about 04:00. For an overnight session that’s most of the window. Warmup is a hidden cost of every restart. This is one reason the disconnection problems on Bot #2 may have done more harm than they first appear to.
Gate 2: Momentum score
The second gate requires a momentum score of at least 40. The score works as a quality filter. Crossovers happen often in choppy, directionless markets, and most of them fail. The momentum gate tries to keep only crossovers that occur when price is actually moving.
The exact scoring formula belongs to the base framework. As an illustration, a typical composite momentum score on a 0 to 100 scale might look like this:
PSEUDOCODE - Illustrative Momentum Score (0–100 scale)
──────────────────────────────────────────────────────
FUNCTION momentum_score(bars):
// Component 1: EMA separation, normalized by volatility
separation = (ema_fast - ema_slow) / ATR
sep_points = CLAMP(separation × 25, 0, 40)
// Component 2: slope of the slow EMA
slope = (ema_slow_now - ema_slow_5_bars_ago) / ATR
slope_points = CLAMP(slope × 20, 0, 30)
// Component 3: recent directional consistency
up_closes = COUNT(bars in last 10 where close > open)
consistency_points = (up_closes / 10) × 30
RETURN sep_points + slope_points + consistency_points
Whatever the real weights are, the idea is the same: don’t trust a crossover unless the market behind it is moving. A threshold of 40 on a 0 to 100 scale is fairly permissive. It removes dead markets but still lets many moderate setups through.
Part 3: The Execution Engine
The pending-signal handoff
When the signal timeframe finds a valid crossover, the bot doesn’t trade right away. It raises a flag:
PSEUDOCODE - Signal-to-Execution Handoff
────────────────────────────────────────
ON signal_bar_close:
IF all_gates_pass AND crossed_above(...):
pending_long_signal = TRUE
signal_timestamp = NOW
ON execution_bar_close:
IF pending_long_signal AND no_open_position:
EXECUTE entry
pending_long_signal = FALSE
This design has a subtle risk: stale signals. If the flag gets set but execution is delayed by a data gap, a disconnection or an error, the bot could enter long after the conditions that justified the trade have gone. A strong implementation gives the flag an expiry:
PSEUDOCODE - Signal Expiry Guard (recommended)
──────────────────────────────────────────────
IF pending_long_signal AND (NOW - signal_timestamp) > max_signal_age:
pending_long_signal = FALSE
LOG "signal expired before execution"
A reasonable max_signal_age is one signal bar. A crossover older than the next signal bar should be re-checked before anyone acts on it.
The mid-price reference
The entry reference is the midpoint of the execution bar:
PSEUDOCODE - Entry Reference Price
──────────────────────────────────
reference_price = (execution_bar.high + execution_bar.low) / 2
This is a reasonable estimate of the “typical” price during the bar. It’s important to understand what it isn’t, though. It isn’t a price the bot is guaranteed to get. By the time the execution bar has closed, the market is at the close, not the midpoint. In a rising bar, the close sits above the mid-price. A market order will fill near the current price, and a limit order at the mid-price may never fill if the move continues.
This gap matters most in paper trading. If the simulator assumes fills at the mid-price, it’s systematically generous on long entries in rising markets, which is exactly the situation a bullish crossover produces. Any paper result, including the +$41.50, should be checked against a more conservative fill assumption:
PSEUDOCODE - Conservative Fill Model (for validation)
─────────────────────────────────────────────────────
assumed_fill = MAX(reference_price, execution_bar.close) + slippage_ticks × tick_size
If the trade is still profitable under this model, the result holds up better.
Part 4: The Exit Engine
Asymmetric stops and targets
This is the most important feature of all three bots:
PSEUDOCODE - Stop and Target Placement
──────────────────────────────────────
stop_price = entry_price - (ATR × 1.0)
target_price = entry_price + (ATR × 6.0)
risk_per_unit = entry_price - stop_price // = 1 ATR
reward_per_unit = target_price - entry_price // = 6 ATR
reward_to_risk = reward_per_unit / risk_per_unit // = 6.0
A 6:1 reward-to-risk ratio is extreme, and it shapes everything about how these bots behave.
Average True Range
Both levels scale with ATR, a standard measure of volatility:
PSEUDOCODE - Average True Range
───────────────────────────────
FUNCTION true_range(bar, previous_close):
RETURN MAX(
bar.high - bar.low,
ABS(bar.high - previous_close),
ABS(bar.low - previous_close)
)
FUNCTION atr(bars, period = 14):
// Wilder smoothing
atr_value = AVERAGE(true_range of first `period` bars)
FOR each subsequent bar:
atr_value = ((atr_value × (period - 1)) + true_range(bar)) / period
RETURN atr_value
Scaling by ATR means stops widen in volatile markets and tighten in quiet ones. The stop is always “one typical bar of movement” away, whatever the regime.
The math of a 6:1 system
With a 1 ATR stop and a 6 ATR target, the breakeven win rate before costs is:
PSEUDOCODE - Breakeven Win Rate
───────────────────────────────
// Expected value per trade, in units of risk (R):
expected_value = (win_rate × reward_R) - ((1 - win_rate) × risk_R)
// Set expected_value = 0 and solve:
breakeven_win_rate = risk_R / (risk_R + reward_R)
= 1 / (1 + 6)
≈ 0.143 → about 14.3%
The bot only has to win about one trade in seven to break even before costs. It will also lose often. A 1 ATR stop is tight, and normal price noise will knock out many positions before a 6 ATR move has time to happen.
This profile creates specific expectations:
Long losing streaks are normal. At a 20% win rate, the chance of 10 straight losses is 0.8^10 ≈ 10.7%. Over hundreds of trades this will almost certainly happen.
Profitability depends on the rare large winner. A few trades carry the whole equity curve.
Small samples say almost nothing. Any single trade, win or loss, is mostly noise.
Three ways out
A position can close in three ways, in this order of priority:
PSEUDOCODE - Exit Priority
──────────────────────────
ON each bar while position_open:
// Priority 1: account safety
IF current_drawdown >= 0.15:
FORCE CLOSE at market
HALT new entries
RETURN
// Priority 2: price-based exits
IF bar.low <= stop_price:
CLOSE at stop_price → loss of ~1R
RETURN
IF bar.high >= target_price:
CLOSE at target_price → gain of ~6R
RETURN
// Priority 3: signal reversal
IF crossed_below(ema_fast, ema_slow, ...):
CLOSE at market → partial win or partial loss
One implementation detail matters here. When a single bar touches both the stop and the target, the code has to decide which one hit first. Bar data can’t tell you. A conservative simulator assumes the stop was hit first. An optimistic one assumes the target. On wide-range bars this choice alone can change backtest results a lot, and it’s one of the first things to audit when backtest and live results disagree.
The reversal exit
The bearish crossover exit keeps a trade from sitting in limbo. If momentum has turned against the position before either the stop or the target is reached, the bot leaves. In practice many trades in this system will end this way, at a small gain or a small loss, rather than at the full +6R or −1R. That lowers the effective reward-to-risk below 6:1 and raises the real breakeven win rate. It’s worth measuring directly:
PSEUDOCODE - Realized R-Multiple Distribution
─────────────────────────────────────────────
FOR each closed trade:
r_multiple = (exit_price - entry_price) / (entry_price - initial_stop)
RECORD r_multiple, exit_reason
REPORT:
count and average R by exit_reason IN {target, stop, reversal, drawdown}
Part 5: The Three Bots, Individually
Bot #1: bar_eth_long_20260914_194814 (gen2)
Overnight status: not active, because of data gaps.
This bot didn’t trade during the window, and the cause was upstream of the strategy: missing bars. For a bar-close system, data gaps do more than cost trading opportunities. They corrupt state:
PSEUDOCODE - How Data Gaps Corrupt Signals
──────────────────────────────────────────
Expected bar sequence: B1, B2, B3, B4, B5
Received sequence: B1, B2, B5
Consequences:
- EMA updates once for B5 instead of three times → values drift from truth
- ATR misses true ranges of B3, B4 → stop/target distances misestimated
- Crossover may occur "inside" the gap → never detected
- Bar counter may under-count → warmup may never complete
The right defensive response is to detect gaps explicitly and either backfill them or reset:
PSEUDOCODE - Gap Detection and Handling
───────────────────────────────────────
ON bar_close(bar):
expected_time = last_bar_time + bar_interval
IF bar.timestamp > expected_time:
missing = (bar.timestamp - expected_time) / bar_interval
LOG WARNING "gap detected: {missing} bars"
IF backfill_available:
REQUEST historical bars for gap
REPLAY them through indicators in order
ELSE:
RESET indicators
SET signal_bar_count = 0 // force fresh warmup
CANCEL any pending_long_signal
Takeaway: Bot #1’s overnight result says nothing about its strategy. It’s a data-integrity finding, and it has to be fixed before this bot can produce meaningful live evidence.
Bot #2: bar_eth_long_20260918_012747 (gen2)
Overnight status: not active, because of Rithmic gateway disconnections.
Gateway disconnections are a different kind of failure. The data may be fine, but the bot can’t see it or can’t act on it. Disconnections carry several risks:
Missed bars during the outage, which cause the same problems as Bot #1.
Warmup resets on reconnect, if the bot restarts cold. As calculated above, that can cost hours.
Orphaned positions. If the bot disconnects while holding a position, its stop and target may exist only in local memory. They won’t be enforced if the process can’t reach the exchange.
The third risk is the dangerous one. It would have mattered even in paper trading and would be critical in production. The fix is to rest protective orders at the broker rather than simulate them locally, and to reconcile state after every reconnect:
PSEUDOCODE - Reconnect Reconciliation
─────────────────────────────────────
ON gateway_reconnect:
broker_positions = QUERY broker for open positions
broker_orders = QUERY broker for working orders
IF broker_positions != local_positions:
LOG CRITICAL "position mismatch"
ADOPT broker state as truth
FOR each open position:
IF no working stop order exists at broker:
SUBMIT stop order immediately
BACKFILL missed bars
REBUILD indicators from backfilled history
RESUME normal loop
Takeaway: like Bot #1, Bot #2 produced no strategy evidence. What it did show is a production-blocking resilience gap. A bot that loses its connection overnight isn’t ready for real capital, however good its logic is.
Bot #3: bar_eth_long_20260924_104027 (gen2)
Overnight status: one trade, one win, +$41.50.
This is the only bot that ran the full pipeline from start to finish during the window: warmup completed, gates passed, a crossover fired, the execution bar confirmed the entry, and the position closed at a profit.
The trade-by-trade record for the window would show whether the exit came from the target or from a reversal. The difference matters. A target hit means the bot captured the full 6 ATR move its design aims for. A reversal exit at a profit means it caught part of a move and left when momentum faded. Both count as wins, but only the first confirms the core thesis of the system.
Takeaway: the bot worked mechanically end to end, which is valuable in itself. Whether the strategy works is still an open question.
Part 6: Why These Bots Qualified
All three bots passed strict qualification criteria even though their historical backtests were negative. That seems contradictory, so it needs a closer look.
Qualification gates usually combine several kinds of checks:
PSEUDOCODE - Typical Qualification Framework
────────────────────────────────────────────
FUNCTION qualifies(bot):
checks = [
bot.max_drawdown <= drawdown_cap,
bot.risk_controls == properly_configured,
bot.code_health == passes_tests,
bot.recent_live_result >= threshold,
bot.backtest_metrics >= minimum_bar
]
RETURN ALL(checks) // or a weighted score, depending on design
If negative backtests were allowed through, then either the backtest threshold was loose or recent live performance was weighted heavily enough to outweigh it. There are real arguments for doing that:
Backtests can be wrong in the pessimistic direction. Fill assumptions, fee models, or the handling of same-bar stop/target collisions can make a strategy look worse than it is.
Market regimes change. A trend-following long bot can backtest poorly over a choppy stretch and still work in a trending one.
Structural soundness counts. All three bots have tight risk controls: a 1 ATR stop, a 15% drawdown cap and a clear exit on reversal. Even if they have no edge, they’re built to lose slowly.
That last point is probably the strongest case for qualification. These bots qualified as controlled experiments, not as proven earners. Framed that way, the decision makes sense. Framed as “these bots are profitable,” it doesn’t.
Part 7: Reading the +$41.50 Honestly
This section matters most.
One trade isn’t a sample
A 6:1 system with a win rate of 15 to 25% produces a winner on any given trade somewhere between one time in seven and one time in four. Even a strategy with zero or negative edge will produce a winning trade that often. Seeing one win in one trade is completely consistent with:
a strategy that has a real edge,
a strategy that breaks even, and
a strategy that loses money over time, which is what the backtest suggests.
One observation can’t tell these three apart. Statistically, the +$41.50 carries almost no information about edge.
How many trades would it take?
For an asymmetric system, a rough guide to the sample size needed for confidence:
PSEUDOCODE - Rough Sample Size for Win-Rate Confidence
──────────────────────────────────────────────────────
// Standard error of an observed win rate p over n trades:
standard_error = SQRT(p × (1 - p) / n)
// To distinguish a true 20% win rate from the 14.3% breakeven
// with reasonable confidence, the gap (≈ 0.057) should be
// about 2 standard errors:
required: 2 × SQRT(0.2 × 0.8 / n) <= 0.057
solve: n >= (2 / 0.057)^2 × 0.16
n ≈ 197 trades
That’s roughly 200 trades just to tell a modest edge apart from breakeven, and more once fees and slippage are counted. At one trade per night, it would take months of live data.
What the result does show
This isn’t meant to dismiss the session. It showed several real things:
The full pipeline works live on Bot #3. Data ingestion, indicator computation, gating, the signal handoff, order placement and exit management all ran correctly against real market data.
Live behavior matched the design. The bot entered on a crossover and closed at a profit, as its logic intends.
Infrastructure failures were found before capital was at risk. Finding data gaps and gateway fragility in paper trading is exactly what paper trading is for.
Two of those three are infrastructure wins, not strategy wins, and they may be the most valuable outcome of the night.
Part 8: Comparative Trading Logic
Side by side, the three bots are close to identical in logic and differ in configuration and operational health:
Dimension Bot #1 (0914) Bot #2 (0918) Bot #3 (0924) Base class RedisEventDriven RedisEventDriven RedisEventDriven Signal logic EMA 9/21 cross EMA 9/21 cross EMA 9/21 cross Gates Warmup 20, momentum ≥ 40 Warmup 20, momentum ≥ 40 Warmup 20, momentum ≥ 40 Stop / target 1 ATR / 6 ATR 1 ATR / 6 ATR 1 ATR / 6 ATR Drawdown cap 15% 15% 15% Overnight health Data gaps Disconnects Clean Overnight result No trades No trades +$41.50
Because the logic is shared, the difference in outcomes came entirely from operations, not strategy. Bot #3 didn’t win because its logic was better. It won because it was the only one that got to trade at all.
The most likely strategic difference between the bots is the timeframe pairing (10/2, 15/3 or 30/5). Faster pairings produce more signals, more trades and faster statistical feedback, but also more noise and higher cost drag. Slower pairings trade less, filter more, and take longer to produce a meaningful sample. When choosing which bot to scale first, the speed of feedback is itself a reason to prefer the faster configurations.
Part 9: The Risk Management Framework
The risk structure has three layers, each covering a different time horizon:
PSEUDOCODE - Layered Risk Model
───────────────────────────────
LAYER 1 — Per-trade (seconds to hours):
Initial stop at 1 ATR below entry
Max loss per trade ≈ 1R + slippage
LAYER 2 — Per-thesis (hours):
Exit on bearish EMA crossover
Prevents holding after the entry rationale is invalidated
LAYER 3 — Account-level (days to weeks):
IF drawdown from equity peak >= 15%:
FORCE CLOSE all positions
HALT trading pending review
The drawdown guard is calculated from the peak:
PSEUDOCODE - Drawdown Guard
───────────────────────────
ON each equity update:
equity_peak = MAX(equity_peak, current_equity)
drawdown = (equity_peak - current_equity) / equity_peak
IF drawdown >= 0.15:
TRIGGER emergency_flatten()
SET trading_halted = TRUE
Recommended additions
The framework is sound but leaves gaps. The overnight session suggests these additions:
A data-health gate. Don’t open new positions if bars have been missing recently.
A connection-health gate. Don’t open new positions within N minutes of a reconnect, and never hold a position without a resting broker-side stop.
Signal expiry. Drop pending signals older than one signal bar.
A position-sizing rule tied to R. Risk a fixed fraction of equity per trade, so a streak of 1R losses can’t approach the 15% cap too quickly.
PSEUDOCODE - Fixed-Fractional Sizing
────────────────────────────────────
risk_fraction = 0.01 // risk 1% of equity per trade
dollar_risk = equity × risk_fraction
risk_per_contract = (entry_price - stop_price) × contract_multiplier
contracts = FLOOR(dollar_risk / risk_per_contract)
IF contracts < 1: SKIP trade
At 1% risk per trade, it would take about 16 consecutive full losses to reach a 15% drawdown, which is a rare but possible streak for a system with a 20% win rate. Sizing should be set so the drawdown guard catches real strategy failure and not ordinary variance.
Part 10: Key Trading Rules, Summarized
Here is the complete rulebook in one place:
PSEUDOCODE - Complete Rule Set
──────────────────────────────
ENTRY RULES
R1. Require >= 20 completed signal bars (warmup)
R2. Require momentum_score >= 40
R3. Require EMA(9) to cross ABOVE EMA(21) on signal bar close
R4. Enter on next execution bar close at reference = (high + low) / 2
R5. Only one position at a time; long only
EXIT RULES
R6. Stop loss at entry - 1.0 × ATR
R7. Profit target at entry + 6.0 × ATR
R8. Exit on EMA(9) crossing BELOW EMA(21)
R9. Force-close everything at 15% drawdown from equity peak
RECOMMENDED ADDITIONS
R10. Expire pending signals after one signal bar
R11. Block entries during/after data gaps until indicators rebuilt
R12. Block entries after reconnect until state reconciled
R13. Rest stop orders at the broker, not in local memory
R14. Size positions by fixed fractional risk
Conclusion: What Comes Next
The overnight session produced one profitable trade and two infrastructure failures. It’s tempting to lead with the +$41.50, but the more honest reading is:
The strategy logic is unproven. Backtests are negative and the live sample is one trade. For a 6:1 asymmetric system, that’s statistically meaningless.
The architecture is sound. Shared base classes, dual timeframes, ATR-scaled exits and layered risk controls are all good engineering choices.
Operational resilience is the real bottleneck. Two of three bots couldn’t trade because of data and connectivity problems. Those need fixing before any strategy question can be answered.
A disciplined plan from here would look like this:
PSEUDOCODE - Validation Roadmap
───────────────────────────────
PHASE 1 — Harden infrastructure
Implement gap detection, backfill, reconnect reconciliation
Verify: 7 consecutive sessions with zero missed bars on all bots
PHASE 2 — Audit the backtest
Apply conservative fill model and stop-first collision rule
Compare backtest trades against paper trades bar-for-bar
Identify why backtest and live results diverge
PHASE 3 — Accumulate live evidence
Run all three bots in paper mode until each has >= 100 trades
Track win rate, average R, R distribution by exit reason
PHASE 4 — Decide
IF observed expectancy > 0 after costs AND consistent across bots:
Promote to minimum-size live trading
ELSE:
Retire or redesign
The most useful thing a trading bot can tell you early on isn’t whether it made money. It’s whether the rest of the system can be trusted when it does. On that front, the session gave clear answers: one bot is ready to collect evidence and two need repairs first. The +$41.50 is a good start but not proof of anything yet.
This article describes paper-trading research and is not investment advice. Automated futures trading carries substantial risk of loss.



