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