| Overall Statistics |
|
Total Orders 200 Average Win 0.05% Average Loss -0.05% Compounding Annual Return 0.638% Drawdown 0.600% Expectancy 0.044 Start Equity 100000 End Equity 100261.37 Net Profit 0.261% Sharpe Ratio -4.426 Sortino Ratio -5.346 Probabilistic Sharpe Ratio 32.072% Loss Rate 48% Win Rate 52% Profit-Loss Ratio 1.02 Alpha -0.051 Beta -0.003 Annual Standard Deviation 0.011 Annual Variance 0 Information Ratio -0.159 Tracking Error 0.23 Treynor Ratio 18.323 Total Fees $0.00 Estimated Strategy Capacity $4600000.00 Lowest Capacity Asset USDJPY 8G Portfolio Turnover 98.89% Drawdown Recovery 91 |
from AlgorithmImports import *
from collections import deque
from datetime import timedelta
class UsdjpyAsiaLondonBreakoutMt5V13Qc(QCAlgorithm):
"""
QuantConnect Python mirror of the uploaded MT5 live engine
`live_usdjpy_asia_london_breakout_MT5_v1_3.py`.
Important scope note
--------------------
This mirrors the STRATEGY LOGIC on QC-native Forex data.
It is not yet a venue-faithful MT5/IBKR replay.
Why this is still useful
------------------------
- The pair is explicitly USDJPY.
- The session logic is preserved.
- ATR gate / TP1 / runner trailing logic is preserved.
- The backtest generates real QC trades and statistics.
- It is suitable to accompany the article as the canonical strategy backtest.
"""
def initialize(self):
self.set_start_date(2025, 1, 1)
self.set_end_date(2025, 5, 31)
self.set_cash(100000)
self.set_time_zone(TimeZones.UTC)
# Explicit pair selection
self.symbol = self.add_forex("USDJPY", Resolution.MINUTE, Market.OANDA).symbol
# ----- Strategy parameters taken from the uploaded live engine -----
self.asia_start_hour = 0
self.asia_end_hour = 5
self.entry_start_hour = 8
self.entry_end_hour = 11
self.time_stop_hour = 11
self.atr_len = 14
self.atr_rising_gate = True
self.entry_buffer_pips = 0.0
self.max_spread_pips_at_entry = 3.0
self.sl_atr_mult = 1.0
self.tp1_atr_mult = 1.0
self.tp1_frac = 0.5
self.trail_atr_mult = 1.5
self.move_to_be_after_tp1 = True
self.one_trade_per_day = True
# FX sizing
self.base_units = 100000
self.tp1_units = int(round(self.base_units * self.tp1_frac))
self.runner_units = self.base_units - self.tp1_units
self.pip = 0.01
# ----- Day/session state -----
self.current_day = None
self.asia_high = None
self.asia_low = None
self.traded_today = False
# ----- Position/model state -----
self._reset_trade_state()
# ----- 15-minute ATR state (built on BID quote bars) -----
self.last_completed_m15_bid_close = None
self.tr_values = deque(maxlen=self.atr_len)
self.atr_current = None
self.atr_prev = None
self.atr_gate_ok = False
# Consolidate minute QuoteBars into 15-minute QuoteBars
self.consolidate(self.symbol, timedelta(minutes=15), self._on_fifteen_minute_quote_bar)
# ----- Article/debug metrics -----
self.model_realized_pips = 0.0
self.trade_count = 0
self.win_count = 0
self.loss_count = 0
self.debug("Initialized USDJPY Asia-London breakout QC mirror")
# ---------------------------------------------------------
# Consolidated 15-minute quote bars -> ATR on BID
# ---------------------------------------------------------
def _on_fifteen_minute_quote_bar(self, bar: QuoteBar):
if bar is None or bar.bid is None:
return
high_ = float(bar.bid.high)
low_ = float(bar.bid.low)
close_ = float(bar.bid.close)
if self.last_completed_m15_bid_close is None:
tr = high_ - low_
else:
tr = max(
high_ - low_,
abs(high_ - self.last_completed_m15_bid_close),
abs(low_ - self.last_completed_m15_bid_close)
)
self.tr_values.append(float(tr))
self.last_completed_m15_bid_close = close_
prev_atr = self.atr_current
if len(self.tr_values) == self.atr_len:
self.atr_current = float(sum(self.tr_values) / len(self.tr_values))
else:
self.atr_current = None
self.atr_prev = prev_atr
if self.atr_rising_gate:
self.atr_gate_ok = (
self.atr_current is not None
and self.atr_prev is not None
and self.atr_current > self.atr_prev
)
else:
self.atr_gate_ok = self.atr_current is not None
if self.atr_current is not None:
self.plot("ATR", "ATR_M15_pips", self.atr_current / self.pip)
if self.atr_prev is not None:
self.plot("ATR", "ATR_M15_prev_pips", self.atr_prev / self.pip)
# ---------------------------------------------------------
# Main minute loop
# ---------------------------------------------------------
def on_data(self, data: Slice):
if self.symbol not in data.quote_bars:
return
qb = data.quote_bars[self.symbol]
if qb is None or qb.bid is None or qb.ask is None:
return
now = self.time
bid_high = float(qb.bid.high)
bid_low = float(qb.bid.low)
bid_close = float(qb.bid.close)
ask_high = float(qb.ask.high)
ask_low = float(qb.ask.low)
ask_close = float(qb.ask.close)
spread_pips = (ask_close - bid_close) / self.pip
self.last_spread_pips = spread_pips
self._ensure_day_reset(now)
# Build Asia range from BID minute bars
if self._in_asia_window(now):
if self.asia_high is None:
self.asia_high = bid_high
self.asia_low = bid_low
else:
self.asia_high = max(self.asia_high, bid_high)
self.asia_low = min(self.asia_low, bid_low)
# Plot levels when available
if self.asia_high is not None:
self.plot("Levels", "AsiaHigh", self.asia_high)
self.plot("Levels", "AsiaLow", self.asia_low)
self.plot("Levels", "BidClose", bid_close)
self.plot("Levels", "AskClose", ask_close)
# 1) Manage open position first
if self.is_open:
self._manage_open_trade(now, bid_high, bid_low, bid_close, ask_high, ask_low, ask_close, spread_pips)
# 2) Only scan for entry if flat
if (not self.is_open) and self._can_scan_for_entry(now):
self._try_enter(now, bid_high, bid_low, bid_close, ask_close, spread_pips)
# ---------------------------------------------------------
# Day/window helpers
# ---------------------------------------------------------
def _ensure_day_reset(self, now):
day_key = now.date()
if self.current_day != day_key:
self.current_day = day_key
self.asia_high = None
self.asia_low = None
self.traded_today = False
self._reset_trade_state()
self.debug(f"NEW DAY {self.current_day}")
def _in_asia_window(self, now):
return self.asia_start_hour <= now.hour < self.asia_end_hour
def _in_entry_window(self, now):
return self.entry_start_hour <= now.hour < self.entry_end_hour
def _time_stop_reached(self, now):
return now.hour >= self.time_stop_hour
def _can_scan_for_entry(self, now):
if self.asia_high is None or self.asia_low is None:
return False
if self.one_trade_per_day and self.traded_today:
return False
if not self._in_entry_window(now):
return False
if self.atr_current is None:
return False
if self.atr_rising_gate and not self.atr_gate_ok:
return False
return True
# ---------------------------------------------------------
# Entry logic
# ---------------------------------------------------------
def _try_enter(self, now, bid_high, bid_low, bid_close, ask_close, spread_pips):
if spread_pips > self.max_spread_pips_at_entry:
return
buffer_px = self.entry_buffer_pips * self.pip
long_trigger = self.asia_high + buffer_px
short_trigger = self.asia_low - buffer_px
hit_long = bid_high >= long_trigger
hit_short = bid_low <= short_trigger
# Ambiguous minute: both breakouts within same minute bar.
# Skip to keep traceability clean for the article.
if hit_long and hit_short:
self.log(
f"AMBIGUOUS_ENTRY {now} bid_high={bid_high:.3f} bid_low={bid_low:.3f} "
f"long_trigger={long_trigger:.3f} short_trigger={short_trigger:.3f}"
)
return
if not hit_long and not hit_short:
return
direction = "LONG" if hit_long else "SHORT"
entry_bid_level = long_trigger if hit_long else short_trigger
atr_at_entry = self.atr_current
if direction == "LONG":
sl_bid = entry_bid_level - self.sl_atr_mult * atr_at_entry
tp1_bid = entry_bid_level + self.tp1_atr_mult * atr_at_entry
qc_qty = +self.base_units
fill_price_model = ask_close # Long market buy at ask
best_extreme = bid_high
else:
sl_bid = entry_bid_level + self.sl_atr_mult * atr_at_entry
tp1_bid = entry_bid_level - self.tp1_atr_mult * atr_at_entry
qc_qty = -self.base_units
fill_price_model = bid_close # Short market sell at bid
best_extreme = bid_low
# Real QC order so the backtest generates actual trades
self.market_order(self.symbol, qc_qty, tag=f"ENTRY_{direction}")
self.is_open = True
self.direction = direction
self.entry_time = now
self.entry_bid_level = entry_bid_level
self.entry_fill_price = fill_price_model
self.atr_at_entry = atr_at_entry
self.sl_bid = sl_bid
self.tp1_bid = tp1_bid
self.be_bid = entry_bid_level
self.tp1_done = (self.tp1_units <= 0)
self.best_extreme_bid = best_extreme
self.trail_bid = sl_bid
self.open_units = qc_qty
self.remaining_fraction = 1.0
self.last_status = "OPEN"
self.traded_today = True
self.trade_count += 1
self.log(
f"ENTRY {now} dir={direction} bid_close={bid_close:.3f} ask_close={ask_close:.3f} "
f"spread_pips={spread_pips:.2f} asia_high={self.asia_high:.3f} asia_low={self.asia_low:.3f} "
f"entry_bid={entry_bid_level:.3f} atr={atr_at_entry/self.pip:.2f}p "
f"sl_bid={sl_bid:.3f} tp1_bid={tp1_bid:.3f}"
)
# ---------------------------------------------------------
# Position management
# ---------------------------------------------------------
def _manage_open_trade(self, now, bid_high, bid_low, bid_close, ask_high, ask_low, ask_close, spread_pips):
atr_eff = self.atr_current if self.atr_current is not None else self.atr_at_entry
if self.direction == "LONG":
self.best_extreme_bid = max(self.best_extreme_bid, bid_high)
trail_candidate = self.best_extreme_bid - self.trail_atr_mult * atr_eff
self.trail_bid = max(self.trail_bid, trail_candidate)
if self.tp1_done and self.move_to_be_after_tp1:
self.trail_bid = max(self.trail_bid, self.be_bid)
stop_bid = self.trail_bid
sl_hit = bid_low <= stop_bid
tp1_hit = (not self.tp1_done) and (bid_high >= self.tp1_bid)
# Conservative same-bar priority: stop first
if sl_hit and tp1_hit:
tp1_hit = False
if tp1_hit:
self._execute_tp1_long(now)
if sl_hit:
self._close_all(now, stop_bid, exit_reason="STOP_OR_TRAIL", spread_pips=spread_pips)
return
if self._time_stop_reached(now):
self._close_all(now, bid_close, exit_reason="TIME_STOP", spread_pips=spread_pips)
return
else: # SHORT
self.best_extreme_bid = min(self.best_extreme_bid, bid_low)
trail_candidate = self.best_extreme_bid + self.trail_atr_mult * atr_eff
self.trail_bid = min(self.trail_bid, trail_candidate)
if self.tp1_done and self.move_to_be_after_tp1:
self.trail_bid = min(self.trail_bid, self.be_bid)
stop_bid = self.trail_bid
sl_hit = bid_high >= stop_bid
tp1_hit = (not self.tp1_done) and (bid_low <= self.tp1_bid)
# Conservative same-bar priority: stop first
if sl_hit and tp1_hit:
tp1_hit = False
if tp1_hit:
self._execute_tp1_short(now, spread_pips)
if sl_hit:
# Short exit buys at ASK, approximated as stop_bid + current spread
exit_fill = stop_bid + spread_pips * self.pip
self._close_all(now, exit_fill, exit_reason="STOP_OR_TRAIL", spread_pips=spread_pips)
return
if self._time_stop_reached(now):
self._close_all(now, ask_close, exit_reason="TIME_STOP", spread_pips=spread_pips)
return
def _execute_tp1_long(self, now):
if self.open_units <= 0:
return
close_units = min(abs(self.open_units), self.tp1_units)
if close_units <= 0:
self.tp1_done = True
return
self.market_order(self.symbol, -close_units, tag="TP1_LONG")
pnl_pips_full = (self.tp1_bid - self.entry_fill_price) / self.pip
weighted_pips = pnl_pips_full * self.tp1_frac
self.model_realized_pips += weighted_pips
self.remaining_fraction -= self.tp1_frac
self.open_units -= close_units
self.tp1_done = True
self.log(
f"TP1 {now} dir=LONG tp1_bid={self.tp1_bid:.3f} "
f"entry_fill={self.entry_fill_price:.3f} pnl_pips_full={pnl_pips_full:.1f} "
f"weighted_pips={weighted_pips:.1f} remaining_fraction={self.remaining_fraction:.2f}"
)
def _execute_tp1_short(self, now, spread_pips):
if self.open_units >= 0:
return
close_units = min(abs(self.open_units), self.tp1_units)
if close_units <= 0:
self.tp1_done = True
return
self.market_order(self.symbol, +close_units, tag="TP1_SHORT")
exit_fill = self.tp1_bid + spread_pips * self.pip # Short exits buy at ask
pnl_pips_full = (self.entry_fill_price - exit_fill) / self.pip
weighted_pips = pnl_pips_full * self.tp1_frac
self.model_realized_pips += weighted_pips
self.remaining_fraction -= self.tp1_frac
self.open_units += close_units
self.tp1_done = True
self.log(
f"TP1 {now} dir=SHORT tp1_bid={self.tp1_bid:.3f} exit_fill={exit_fill:.3f} "
f"entry_fill={self.entry_fill_price:.3f} pnl_pips_full={pnl_pips_full:.1f} "
f"weighted_pips={weighted_pips:.1f} remaining_fraction={self.remaining_fraction:.2f}"
)
def _close_all(self, now, exit_fill_price, exit_reason, spread_pips):
if not self.is_open or self.open_units == 0:
return
qty = -self.open_units
self.market_order(self.symbol, qty, tag=exit_reason)
if self.direction == "LONG":
pnl_pips_full = (exit_fill_price - self.entry_fill_price) / self.pip
else:
pnl_pips_full = (self.entry_fill_price - exit_fill_price) / self.pip
weighted_pips = pnl_pips_full * self.remaining_fraction
self.model_realized_pips += weighted_pips
if weighted_pips >= 0:
self.win_count += 1
else:
self.loss_count += 1
self.log(
f"EXIT {now} dir={self.direction} reason={exit_reason} exit_fill={exit_fill_price:.3f} "
f"entry_fill={self.entry_fill_price:.3f} pnl_pips_full={pnl_pips_full:.1f} "
f"weighted_pips={weighted_pips:.1f} spread_pips={spread_pips:.2f}"
)
self.plot("ModelPips", "CumModelPips", self.model_realized_pips)
self._reset_trade_state()
self.last_status = "CLOSED"
# ---------------------------------------------------------
# State reset / summary
# ---------------------------------------------------------
def _reset_trade_state(self):
self.is_open = False
self.direction = None
self.entry_time = None
self.entry_bid_level = None
self.entry_fill_price = None
self.atr_at_entry = None
self.sl_bid = None
self.tp1_bid = None
self.be_bid = None
self.tp1_done = False
self.best_extreme_bid = None
self.trail_bid = None
self.open_units = 0
self.remaining_fraction = 0.0
self.last_spread_pips = None
self.last_status = "FLAT"
def on_end_of_algorithm(self):
win_rate = 0.0 if self.trade_count == 0 else 100.0 * self.win_count / max(1, self.trade_count)
self.log("==== SUMMARY ====")
self.log(f"Symbol: {self.symbol.value}")
self.log(f"Trades: {self.trade_count}")
self.log(f"Wins: {self.win_count}")
self.log(f"Losses: {self.loss_count}")
self.log(f"Win rate: {win_rate:.1f}%")
self.log(f"Model realized pips: {self.model_realized_pips:.1f}")