Overall Statistics
Total Orders
118
Average Win
0.77%
Average Loss
-0.62%
Compounding Annual Return
10.083%
Drawdown
8.500%
Expectancy
0.408
Start Equity
100000
End Equity
110112.42
Net Profit
10.112%
Sharpe Ratio
0.19
Sortino Ratio
0.213
Probabilistic Sharpe Ratio
36.174%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.25
Alpha
0
Beta
0
Annual Standard Deviation
0.102
Annual Variance
0.01
Information Ratio
0.729
Tracking Error
0.102
Treynor Ratio
0
Total Fees
$229.99
Estimated Strategy Capacity
$770000.00
Lowest Capacity Asset
INDY UHSHWL3SZOTH
Portfolio Turnover
4.13%
Drawdown Recovery
54
# https://quantpedia.com/strategies/seasonal-front-running-in-country-etfs
#
# The investment universe for this strategy consists of 23 country ETFs, as outlined in the research paper. 
# These ETFs include SPY, EWU, EWG, EWQ, EWI, EWD, EWN, EWP, EWK, EWL, EWC, EWJ, EWW, EWM, EWA, EWS, EWY, EWT, EWZ, EWH, EZA, FXI, and INDY. The selection of these ETFs is based on their availability and historical data, 
# with most having data available from the year 2000. The strategy focuses on these ETFs to exploit seasonal patterns in their returns. The strategy employs a cross-sectional approach to seasonality. At the end of each month,
# the returns of all included ETFs are ranked based on their performance in the same month of the previous year. This ranking is used to identify the top-performing ETFs. The methodology involves selecting a subset of these 
# top-ranked ETFs for investment in the following month. The buy rule is to invest in the top 3 to 8 ETFs based on their seasonal ranking, while the sell rule involves exiting positions at the end of the month to reassess 
# and rebalance based on new rankings. The strategy involves rebalancing the portfolio at the end of each month. The number of positions selected ranges from 3 to 8 ETFs, based on their seasonal ranking. Capital is allocated
# equally among the selected ETFs to ensure diversification and manage risk.

# region imports
from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
from pandas.core.frame import DataFrame
from typing import List
import pandas as pd
# endregion

class SeasonalFrontRunninginCountryETFs(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 1, 1)
        self.set_end_date(2025, 1, 1)
        self.set_cash(100_000)

        self._period: int = 12
        self._offset_months: int = 10
        self._slice = slice(3, 9)
        self._selected_assets: List[Symbol] = []

        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)

        tickers: List[str] = [
            'SPY',  # SPDR S&P 500 ETF Trust
            'EWU',  # iShares MSCI United Kingdom ETF
            'EWG',  # iShares MSCI Germany ETF
            'EWQ',  # iShares MSCI France ETF
            'EWI',  # iShares MSCI Italy ETF
            'EWD',  # iShares MSCI Sweden ETF
            'EWN',  # iShares MSCI Netherlands ETF
            'EWP',  # iShares MSCI Spain ETF
            'EWK',  # iShares MSCI Belgium ETF
            'EWL',  # iShares MSCI Switzerland ETF
            'EWC',  # iShares MSCI Canada ETF
            'EWJ',  # iShares MSCI Japan ETF
            'EWW',  # iShares MSCI Mexico ETF
            'EWM',  # iShares MSCI Malaysia ETF
            'EWA',  # iShares MSCI Australia ETF
            'EWS',  # iShares MSCI Singapore ETF
            'EWY',  # iShares MSCI South Korea ETF
            'EWT',  # iShares MSCI Taiwan ETF
            'EWZ',  # iShares MSCI Brazil ETF
            'EWH',  # iShares MSCI Hong Kong ETF
            'EZA',  # iShares MSCI South Africa ETF
            'FXI',  # iShares China Large-Cap ETF
            'INDY' # iShares India 50 ETF
        ]

        self._traded_assets: List[Symbol] = [
            self.add_equity(ticker, Resolution.DAILY).symbol for ticker in tickers
        ]

        self._selection_flag: bool = False
        self.settings.minimum_order_margin_portfolio_percentage = 0.
        self.settings.daily_precise_end_time = False

        self.schedule.on(
            self.date_rules.month_end(self._traded_assets[0]), 
            self.time_rules.before_market_close(self._traded_assets[0]), 
            self._selection
        )

    def on_data(self, slice: Slice) -> None: 
        # Monthly rebalance.
        if not self._selection_flag:
            return
        self._selection_flag = False
 
        # Order execution.
        targets: List[PortfolioTarget] = []
        for symbol in self._selected_assets:
            if slice.contains_key(str(symbol)) and slice[str(symbol)]:
                targets.append(PortfolioTarget(str(symbol), 1 / len(self._selected_assets)))
            
        self.set_holdings(targets, True)
        self._selected_assets.clear()

    def _selection(self) -> None:
        history: DataFrame = self.history(
            self._traded_assets, start=self.time - relativedelta(months=self._period), end=self.time
        )

        prices: DataFrame = history.close.unstack(level=0)
        monthly_returns: DataFrame = prices.groupby(pd.Grouper(freq='M')).last().pct_change()[1:].dropna(axis=1)

        if len(monthly_returns) >= self._period - 1: # The paper suggests that a complete set of assets is not required prior to evaluation.
            self._selection_flag = True
            # Historical performance.
            observed_performance: DataFrame = monthly_returns[monthly_returns.index.month == (self.time - pd.DateOffset(months=self._offset_months)).month].iloc[0]
            self._selected_assets = list(observed_performance.sort_values(ascending=False).iloc[self._slice].index)