Overall Statistics
Total Orders
1925
Average Win
0.16%
Average Loss
-0.10%
Compounding Annual Return
20.844%
Drawdown
36.600%
Expectancy
0.732
Start Equity
10000000
End Equity
26197386.24
Net Profit
161.974%
Sharpe Ratio
0.73
Sortino Ratio
0.759
Probabilistic Sharpe Ratio
30.473%
Loss Rate
32%
Win Rate
68%
Profit-Loss Ratio
1.56
Alpha
0.041
Beta
0.932
Annual Standard Deviation
0.183
Annual Variance
0.034
Information Ratio
0.387
Tracking Error
0.089
Treynor Ratio
0.144
Total Fees
$30826.07
Estimated Strategy Capacity
$62000000.00
Lowest Capacity Asset
HONI R735QTJ8XC9X
Portfolio Turnover
0.69%
# region imports
from AlgorithmImports import *
# endregion

industry_codes = [
    'AGRICULTURAL_INPUTS',
    'BUILDING_MATERIALS',
    'CHEMICALS',
    'SPECIALTY_CHEMICALS',
    'LUMBER_AND_WOOD_PRODUCTION',
    'PAPER_AND_PAPER_PRODUCTS',
    'ALUMINUM',
    'COPPER',
    'OTHER_INDUSTRIAL_METALS_AND_MINING',
    'GOLD',
    'SILVER',
    'OTHER_PRECIOUS_METALS_AND_MINING',
    'COKING_COAL',
    'STEEL',
    'AUTO_AND_TRUCK_DEALERSHIPS',
    'AUTO_MANUFACTURERS',
    'AUTO_PARTS',
    'RECREATIONAL_VEHICLES',
    'FURNISHINGS',
    'FIXTURES_AND_APPLIANCES',
    'RESIDENTIAL_CONSTRUCTION',
    'TEXTILE_MANUFACTURING',
    'APPAREL_MANUFACTURING',
    'FOOTWEAR_AND_ACCESSORIES',
    'PACKAGING_AND_CONTAINERS',
    'PERSONAL_SERVICES',
    'RESTAURANTS',
    'APPAREL_RETAIL',
    'DEPARTMENT_STORES',
    'HOME_IMPROVEMENT_RETAIL',
    'LUXURY_GOODS',
    'INTERNET_RETAIL',
    'SPECIALTY_RETAIL',
    'GAMBLING',
    'LEISURE',
    'LODGING',
    'RESORTS_AND_CASINOS',
    'TRAVEL_SERVICES',
    'ASSET_MANAGEMENT',
    'BANKS_DIVERSIFIED',
    'BANKS_REGIONAL',
    'MORTGAGE_FINANCE',
    'CAPITAL_MARKETS',
    'FINANCIAL_DATA_AND_STOCK_EXCHANGES',
    'INSURANCE_LIFE',
    'INSURANCE_PROPERTY_AND_CASUALTY',
    'INSURANCE_REINSURANCE',
    'INSURANCE_SPECIALTY',
    'INSURANCE_BROKERS',
    'INSURANCE_DIVERSIFIED',
    'SHELL_COMPANIES',
    'FINANCIAL_CONGLOMERATES',
    'CREDIT_SERVICES',
    'REAL_ESTATE_DEVELOPMENT',
    'REAL_ESTATE_SERVICES',
    'REAL_ESTATE_DIVERSIFIED',
    'REIT_HEALTHCARE_FACILITIES',
    'REIT_HOTEL_AND_MOTEL',
    'REIT_INDUSTRIAL',
    'REIT_OFFICE',
    'REIT_RESIDENTIAL',
    'REIT_RETAIL',
    'REIT_MORTGAGE',
    'REIT_SPECIALTY',
    'REIT_DIVERSIFIED',
    'BEVERAGES_BREWERS',
    'BEVERAGES_WINERIES_AND_DISTILLERIES',
    'BEVERAGES_NON_ALCOHOLIC',
    'CONFECTIONERS',
    'FARM_PRODUCTS',
    'HOUSEHOLD_AND_PERSONAL_PRODUCTS',
    'PACKAGED_FOODS',
    'EDUCATION_AND_TRAINING_SERVICES',
    'DISCOUNT_STORES',
    'FOOD_DISTRIBUTION',
    'GROCERY_STORES',
    'TOBACCO',
    'BIOTECHNOLOGY',
    'DRUG_MANUFACTURERS_GENERAL',
    'DRUG_MANUFACTURERS_SPECIALTY_AND_GENERIC',
    'HEALTHCARE_PLANS',
    'MEDICAL_CARE_FACILITIES',
    'PHARMACEUTICAL_RETAILERS',
    'HEALTH_INFORMATION_SERVICES',
    'MEDICAL_DEVICES',
    'MEDICAL_INSTRUMENTS_AND_SUPPLIES',
    'DIAGNOSTICS_AND_RESEARCH',
    'MEDICAL_DISTRIBUTION',
    'UTILITIES_INDEPENDENT_POWER_PRODUCERS',
    'UTILITIES_RENEWABLE',
    'UTILITIES_REGULATED_WATER',
    'UTILITIES_REGULATED_ELECTRIC',
    'UTILITIES_REGULATED_GAS',
    'UTILITIES_DIVERSIFIED',
    'TELECOM_SERVICES',
    'ADVERTISING_AGENCIES',
    'PUBLISHING',
    'BROADCASTING',
    'ENTERTAINMENT',
    'INTERNET_CONTENT_AND_INFORMATION',
    'ELECTRONIC_GAMING_AND_MULTIMEDIA',
    'OIL_AND_GAS_DRILLING',
    'OIL_AND_GAS_E_AND_P',
    'OIL_AND_GAS_INTEGRATED',
    'OIL_AND_GAS_MIDSTREAM',
    'OIL_AND_GAS_REFINING_AND_MARKETING',
    'OIL_AND_GAS_EQUIPMENT_AND_SERVICES',
    'THERMAL_COAL',
    'URANIUM',
    'AEROSPACE_AND_DEFENSE',
    'SPECIALTY_BUSINESS_SERVICES',
    'CONSULTING_SERVICES',
    'RENTAL_AND_LEASING_SERVICES',
    'SECURITY_AND_PROTECTION_SERVICES',
    'STAFFING_AND_EMPLOYMENT_SERVICES',
    'CONGLOMERATES',
    'ENGINEERING_AND_CONSTRUCTION',
    'INFRASTRUCTURE_OPERATIONS',
    'BUILDING_PRODUCTS_AND_EQUIPMENT',
    'FARM_AND_HEAVY_CONSTRUCTION_MACHINERY',
    'INDUSTRIAL_DISTRIBUTION',
    'BUSINESS_EQUIPMENT_AND_SUPPLIES',
    'SPECIALTY_INDUSTRIAL_MACHINERY',
    'METAL_FABRICATION',
    'POLLUTION_AND_TREATMENT_CONTROLS',
    'TOOLS_AND_ACCESSORIES',
    'ELECTRICAL_EQUIPMENT_AND_PARTS',
    'AIRPORTS_AND_AIR_SERVICES',
    'AIRLINES',
    'RAILROADS',
    'MARINE_SHIPPING',
    'TRUCKING',
    'INTEGRATED_FREIGHT_AND_LOGISTICS',
    'WASTE_MANAGEMENT',
    'INFORMATION_TECHNOLOGY_SERVICES',
    'SOFTWARE_APPLICATION',
    'SOFTWARE_INFRASTRUCTURE',
    'COMMUNICATION_EQUIPMENT',
    'COMPUTER_HARDWARE',
    'CONSUMER_ELECTRONICS',
    'ELECTRONIC_COMPONENTS',
    'ELECTRONICS_AND_COMPUTER_DISTRIBUTION',
    'SCIENTIFIC_AND_TECHNICAL_INSTRUMENTS',
    'SEMICONDUCTOR_EQUIPMENT_AND_MATERIALS',
    'SEMICONDUCTORS',
    'SOLAR'
]
# region imports
from AlgorithmImports import *

from industry_codes import industry_codes
# endregion

class CreativeRedCow(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2019, 9, 1)
        self.set_end_date(2024, 9, 30)
        self.set_cash(10_000_000)
        self.settings.min_absolute_portfolio_target_percentage = 0
        self.set_warm_up(self.start_date - datetime(2007, 1, 1))#datetime(1998, 2, 1))
        self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)))

        spy = Symbol.create('SPY', SecurityType.EQUITY, Market.USA)
        self.universe_settings.schedule.on(self.date_rules.month_start())
        self.universe_settings.resolution = Resolution.DAILY
        self._universe = self.add_universe(self._select_assets)

        self.schedule.on(self.date_rules.month_start(spy, 1), self.time_rules.midnight, self._rebalance)
        
        self._lookback = 12*12 # 144 months = 12 years = 3 presidential terms.
        self._previous_selection_time = None
        self._outperforming_industries = pd.DataFrame(columns=industry_codes + ['party=R'])
        self._winning_party_by_result_date = {date(1996, 11, 6): "D", date(2000, 11, 8): "R", date(2004, 11, 3): "R", date(2008, 11, 5): "D", date(2012, 11, 7): "D", date(2016, 11, 9): "R", date(2020, 11, 8): "D"}

    def _select_assets(self, fundamentals):
        ruling_party = [party for t, party in self._winning_party_by_result_date.items() if t < self.time.date()][-1]

        # Record which industries outperformed over the previous month.
        if self._previous_selection_time:
            # Determine which industries outerperformed.
            history = self.history(list(self._leader_by_industry.values()), self._previous_selection_time, self.time, Resolution.DAILY)['close'].unstack(0)
            monthly_returns = (history.iloc[-1] - history.iloc[0]) / history.iloc[0]
            outperformers = (monthly_returns > monthly_returns.median()).astype(int)
            # Append the result to the `self._outperforming_industries` DataFrame.
            row = [outperformers.get(self._leader_by_industry.get(industry_code, None), 0) for industry_code in industry_codes]
            row.append(int(ruling_party == 'R'))
            self._outperforming_industries.loc[self._previous_selection_time] = row
            # Trim off the samples that fall out of the lookback window.
            self._outperforming_industries = self._outperforming_industries.iloc[-self._lookback:]
        self._previous_selection_time = self.time

        # Get the largest asset of each industry.
        self._leader_by_industry = {}
        for industry_code in industry_codes:
            industry_assets = [f for f in fundamentals if f.asset_classification.morningstar_industry_code == eval(f"MorningstarIndustryCode.{industry_code}") and f.market_cap]
            if industry_assets:
                self._leader_by_industry[industry_code] = sorted(industry_assets, key=lambda f: f.market_cap)[-1].symbol

        # During warm-up, keep the universe empty.
        if self.is_warming_up:
            return []
        
        # Calculate correlation coefficient of each industry.
        correlation_coefficients = self._outperforming_industries.corr().drop('party=R')['party=R'].sort_values()
        if ruling_party == 'D':
            correlation_coefficients *= -1

        self._corr_by_symbol = {
            self._leader_by_industry[industry]: corr
            for industry, corr in correlation_coefficients[correlation_coefficients > 0].items()
            if industry in self._leader_by_industry
        }
        return list(self._corr_by_symbol.keys())
        
    def _rebalance(self):
        if not self._universe.selected:
            return
        corr_sum = sum(self._corr_by_symbol.values())
        targets = [PortfolioTarget(symbol, corr/corr_sum) for symbol, corr in self._corr_by_symbol.items()]
        self.set_holdings(targets, True)