| Overall Statistics |
|
Total Orders 2928 Average Win 0.29% Average Loss -0.18% Compounding Annual Return -4.846% Drawdown 20.000% Expectancy -0.110 Start Equity 100000.00 End Equity 82538.67 Net Profit -17.461% Sharpe Ratio -1.648 Sortino Ratio -4.262 Probabilistic Sharpe Ratio 0.001% Loss Rate 66% Win Rate 34% Profit-Loss Ratio 1.62 Alpha 0 Beta 0 Annual Standard Deviation 0.039 Annual Variance 0.002 Information Ratio -0.848 Tracking Error 0.039 Treynor Ratio 0 Total Fees $4280.33 Estimated Strategy Capacity $7900000.00 Lowest Capacity Asset EURUSD 8G Portfolio Turnover 167.63% |
from AlgorithmImports import *
class BreakoutAlphaModel(AlphaModel):
def __init__(self, resolution=Resolution.Hour):
super().__init__()
self.resolution = resolution
self.symbol_data: Mapping[QuantConnect.Symbol, SymbolData] = dict()
def Update(self, algorithm, data):
insights = []
# Only generate insights at the 8am candle
if not algorithm.Time.hour == 8:
return insights
for symbol, symbol_data in self.symbol_data.items():
# Update rolling window with latest price
if data.Bars.ContainsKey(symbol):
bar = data.Bars[symbol]
insights.append(Insight.Price(symbol, timedelta(minutes=60), InsightDirection.Up))
insights.append(Insight.Price(symbol, timedelta(minutes=60), InsightDirection.Down))
return Insight.group(insights)
def OnSecuritiesChanged(self, algorithm, changes):
for security in changes.AddedSecurities:
if security.Symbol not in self.symbol_data:
self.symbol_data[security.Symbol] = SymbolData(security.Symbol)
for security in changes.RemovedSecurities:
symbol_data = self.symbol_data.pop(security.Symbol, None)
return None
class SymbolData:
def __init__(self, symbol):
self.symbol: QuantConnect.Symbol = symbol
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from AlgorithmImports import *
class ImmediateExecutionModel(ExecutionModel):
'''Provides an implementation of IExecutionModel that immediately submits market orders to achieve the desired portfolio targets'''
def __init__(self):
'''Initializes a new instance of the ImmediateExecutionModel class'''
self.targets_collection = PortfolioTargetCollection()
def execute(self, algorithm, targets):
'''Immediately submits orders for the specified portfolio targets.
Args:
algorithm: The algorithm instance
targets: The portfolio targets to be ordered'''
# for performance we check count value, OrderByMarginImpact and ClearFulfilled are expensive to call
self.targets_collection.add_range(targets)
if not self.targets_collection.is_empty:
for target in self.targets_collection.order_by_margin_impact(algorithm):
security = algorithm.securities[target.symbol]
# calculate remaining quantity to be ordered
quantity = OrderSizing.get_unordered_quantity(algorithm, target, security, True)
if quantity != 0:
above_minimum_portfolio = BuyingPowerModelExtensions.above_minimum_order_margin_portfolio_percentage(
security.buying_power_model,
security,
quantity,
algorithm.portfolio,
algorithm.settings.minimum_order_margin_portfolio_percentage)
if above_minimum_portfolio:
algorithm.market_order(security, quantity)
elif not PortfolioTarget.minimum_order_margin_percentage_warning_sent:
# will trigger the warning if it has not already been sent
PortfolioTarget.minimum_order_margin_percentage_warning_sent = False
self.targets_collection.clear_fulfilled(algorithm)# region imports
from AlgorithmImports import *
from symbol_data import SymbolData
from trailing_stop_risk import TrailingStopRiskManagementModel
from immediate_execution_model import ImmediateExecutionModel
# endregion
class OcODevisenStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2021, 1, 1)
self.set_end_date(2024, 11, 10)
self.set_cash(100000)
self.default_order_properties.time_in_force = TimeInForce.DAY
berlin_time_zone_utc_plus_2 = "Europe/Berlin"
self.set_time_zone(berlin_time_zone_utc_plus_2)
self.set_brokerage_model(
brokerage=BrokerageName.INTERACTIVE_BROKERS_BROKERAGE,
account_type=AccountType.MARGIN,
)
symbols: List[Symbol] = [
self.add_forex(ticker=currency_pair, resolution=Resolution.MINUTE).symbol
# for currency_pair in ["EURUSD", "GBPUSD", "EURGBP"]
for currency_pair in ["EURUSD"]
]
self.add_universe_selection(ManualUniverseSelectionModel(symbols))
self.universe_settings.resolution = Resolution.MINUTE
self.symbol_data: Mapping[Symbol, SymbolData] = {}
self.pip = 0.0001 # TODO make this dependend on currency pair, this is not correct for Yen
self.lot_size = 100000
self.add_risk_management(TrailingStopRiskManagementModel(0.002))
self.set_execution(ImmediateExecutionModel())
self.orders = {}
def on_hourly_quote_bar(self, sender, quote_bar: QuoteBar):
self.symbol_data[quote_bar.symbol].last_hour_quote_bar = quote_bar
def get_quantity(self, quote_bar: QuoteBar) -> Mapping[str, float]:
def get_maximum_loss(price1: float, price2: float) -> float:
maximum_loss = round(price1 - price2, 6)
return maximum_loss if maximum_loss != 0 else 6 * self.pip
def calculate_max_margin() -> float:
available_margin = self.portfolio.margin_remaining
invested_count = sum(
1 if self.portfolio[symbol].invested else 0
for symbol in self.symbol_data
)
# Calculate margin ratio based on current investments
margin_ratio = max(3 - invested_count, 1)
return available_margin / margin_ratio
def calculate_position_size(
max_margin: float, risk_exposure: float, max_loss: float
) -> float:
position_size = risk_exposure / max_loss
desired_size = min(position_size, max_margin)
return max(round(desired_size / self.lot_size), 1) * self.lot_size
risk_exposure = self.portfolio.total_portfolio_value * 0.01
# Calculate maximum potential loss for buy and sell
maximum_loss_buy = get_maximum_loss(quote_bar.close, quote_bar.low)
maximum_loss_sell = get_maximum_loss(quote_bar.high, quote_bar.close)
# Calculate maximum allowable margin for this trade
max_margin = calculate_max_margin()
# Calculate buy and sell sizes, constrained by max margin
buy_size = calculate_position_size(max_margin, risk_exposure, maximum_loss_buy)
sell_size = calculate_position_size(
max_margin, risk_exposure, maximum_loss_sell
)
return {"buy": buy_size, "sell": sell_size}
def on_data(self, data: Slice):
if self.time.time() == time(8,0):
for symbol, symbol_data in self.symbol_data.items():
hour_bar = symbol_data.last_hour_quote_bar
buy_stop_price = hour_bar.high + self.pip * 2
# entry tickets
buy_stop_ticket = self.stop_market_order(
symbol=symbol, quantity=self.get_quantity(hour_bar)["buy"], stop_price=buy_stop_price
)
sell_stop_price = hour_bar.low - self.pip * 2
sell_stop_ticket = self.stop_market_order(
symbol, -self.get_quantity(hour_bar)["sell"], sell_stop_price
)
self.register_oco_orders(buy_stop_ticket, sell_stop_ticket) # TODO test if not cancelling is more profitable
# stop loss tickets
# buy_stop_loss_ticket = self.stop_market_order(
# symbol, -self.portfolio[symbol].quantity, min(hour_bar.low, buy_stop_price - 6 * self.pip)
# )
# sell_stop_loss_ticket = self.stop_market_order(
# symbol, -self.portfolio[symbol].quantity, max(hour_bar.high, sell_stop_price + 6 * self.pip)
# )
# self.register_oco_orders(buy_stop_loss_ticket, sell_stop_loss_ticket)
def register_oco_orders(self, one_ticket, other_ticket):
self.orders[one_ticket.order_id] = {
"oco_order_id": other_ticket.order_id,
"type": "one",
}
self.orders[other_ticket.order_id] = {
"oco_order_id": one_ticket.order_id,
"type": "other",
}
return None
def on_order_event(self, order_event: OrderEvent):
self.log("order event: " + order_event.to_string())
if order_event.status == OrderStatus.FILLED:
if (order := self.orders.get(order_event.order_id)) is not None: # exit
self.transactions.cancel_order(order["oco_order_id"])
def on_securities_changed(self, changes):
for security in changes.AddedSecurities:
if security.Symbol not in self.symbol_data:
self.symbol_data[security.Symbol] = SymbolData(security.Symbol)
consolidator = QuoteBarConsolidator(timedelta(hours=1))
self.subscription_manager.add_consolidator(security.Symbol, consolidator)
consolidator.data_consolidated += self.on_hourly_quote_bar
for security in changes.RemovedSecurities:
symbol_data = self.symbol_data.pop(security.Symbol, None)
# TODO remove consolidator
return None
# region imports
from AlgorithmImports import *
# endregion
# Your New Python File
class SymbolData:
def __init__(self, symbol):
self.symbol = symbol
self.last_hour_quote_bar = None# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from AlgorithmImports import *
class TrailingStopRiskManagementModel(RiskManagementModel):
'''Provides an implementation of IRiskManagementModel that limits the maximum possible loss
measured from the highest unrealized profit'''
def __init__(self, maximum_drawdown_percent = 0.05):
'''Initializes a new instance of the TrailingStopRiskManagementModel class
Args:
maximum_drawdown_percent: The maximum percentage drawdown allowed for algorithm portfolio compared with the highest unrealized profit, defaults to 5% drawdown'''
self.maximum_drawdown_percent = abs(maximum_drawdown_percent)
self.trailing_absolute_holdings_state = dict()
def manage_risk(self, algorithm, targets):
'''Manages the algorithm's risk at each time step
Args:
algorithm: The algorithm instance
targets: The current portfolio targets to be assessed for risk'''
risk_adjusted_targets = list()
for kvp in algorithm.securities:
symbol = kvp.key
security = kvp.value
# Remove if not invested
if not security.invested:
self.trailing_absolute_holdings_state.pop(symbol, None)
continue
position = PositionSide.LONG if security.holdings.is_long else PositionSide.SHORT
absolute_holdings_value = security.holdings.absolute_holdings_value
trailing_absolute_holdings_state = self.trailing_absolute_holdings_state.get(symbol)
# Add newly invested security (if doesn't exist) or reset holdings state (if position changed)
if trailing_absolute_holdings_state == None or position != trailing_absolute_holdings_state.position:
self.trailing_absolute_holdings_state[symbol] = trailing_absolute_holdings_state = self.HoldingsState(position, security.holdings.absolute_holdings_cost)
trailing_absolute_holdings_value = trailing_absolute_holdings_state.absolute_holdings_value
# Check for new max (for long position) or min (for short position) absolute holdings value
if ((position == PositionSide.LONG and trailing_absolute_holdings_value < absolute_holdings_value) or
(position == PositionSide.SHORT and trailing_absolute_holdings_value > absolute_holdings_value)):
self.trailing_absolute_holdings_state[symbol].absolute_holdings_value = absolute_holdings_value
continue
drawdown = abs((trailing_absolute_holdings_value - absolute_holdings_value) / trailing_absolute_holdings_value)
if self.maximum_drawdown_percent < drawdown:
# Cancel insights
algorithm.insights.cancel([ symbol ])
self.trailing_absolute_holdings_state.pop(symbol, None)
# liquidate
risk_adjusted_targets.append(PortfolioTarget(symbol, 0))
return risk_adjusted_targets
class HoldingsState:
def __init__(self, position, absolute_holdings_value):
self.position = position
self.absolute_holdings_value = absolute_holdings_value