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