Overall Statistics
Total Orders
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Start Equity
10000000
End Equity
10000000
Net Profit
0%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
-0.767
Tracking Error
0.138
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
# region imports
from AlgorithmImports import *

from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

from industry_codes import industry_codes
# endregion

fundamental_factors = [
    'operation_ratios.revenue_growth.three_months',              # How a company's sales are affected over time, possibly reflecting policy impacts like tax changes or sector-specific subsidies.
    #'valuation_ratios.first_year_estimated_eps_growth',         # Indicates how profitable a company has become, potentially reflecting beneficial or adverse policy effects.
    'valuation_ratios.pe_ratio',                                 # Changes in this ratio might reflect investor expectations under different political climates. For instance, sectors expected to benefit from new policies might see an increase in their P/E ratios due to higher anticipated earnings.
    'operation_ratios.roe.three_months',                         # Indicate how well companies utilize their assets to generate profit, which could be influenced by corporate tax policies or regulatory environments.
    'operation_ratios.roa.three_months',                         # Indicate how well companies utilize their assets to generate profit, which could be influenced by corporate tax policies or regulatory environments.
    'operation_ratios.current_ratio.three_months',               # To see how policies affecting economic stability might impact a company's short-term financial health.
    'operation_ratios.total_debt_equity_ratio.three_months',     # Policies affecting interest rates or corporate taxes could influence a company's capital structure.
    'valuation_ratios.trailing_dividend_yield',                  # Reflects income generation for investors, which might be influenced by tax policies on dividends.
]

class CreativeRedCow(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2012, 3, 1)
        self.set_end_date(2024, 9, 30)
        self.set_cash(10_000_000)
        self.set_warm_up(self.start_date - datetime(1998, 2, 1))

        self.universe_settings.schedule.on(self.date_rules.month_start())
        self.add_universe(self._select_assets)

        self._daily_returns_by_industry = {industry: pd.Series() for industry in industry_codes}
        
        self._scaler = StandardScaler()
        self._clustering_model = KMeans(n_clusters=3, random_state=0)
        self._lookback = timedelta(12*365) # 144 months = 12 years = 3 presidential terms.
        self._previous_selection_time = None
        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"}
        self._periods_by_party = {
            'D': [
                (datetime(1996, 11, 6), datetime(2000, 11, 7)), 
                (datetime(2008, 11, 5), datetime(2016, 11, 8)),
                (datetime(2020, 11, 8), self.end_date)
            ],
            'R': [
                (datetime(2000, 11, 8), datetime(2008, 11, 4)),
                (datetime(2016, 11, 9), datetime(2020, 11, 7))
            ]
        }

    def _has_all_factors(self, fundamental):
        values = []
        for factor in fundamental_factors:
            values.append(np.isnan(eval(f"fundamental.{factor}")))
            #values.append(eval(f"fundamental.{factor}"))
        return not any(values)

    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]

        # Update the daily returns of each industry.
        if self._previous_selection_time:
            # Get the daily returns of each industry leader.
            daily_returns = self.history([leader['symbol'] for leader in self._leader_by_industry.values()], self._previous_selection_time-timedelta(2), self.time, Resolution.DAILY)['close'].unstack(0).pct_change()
            for industry in industry_codes:
                if industry not in self._leader_by_industry or self._leader_by_industry[industry]['symbol'] not in daily_returns.columns:
                    continue # Fill with 0 later
                # Append the daily returns of the current leader to the series of daily returns for the industry.
                industry_daily_returns = pd.concat([self._daily_returns_by_industry[industry], daily_returns[self._leader_by_industry[industry]['symbol']].dropna()])
                self._daily_returns_by_industry[industry] = industry_daily_returns[~industry_daily_returns.index.duplicated(keep='first')]
                # Trim off history that has fallen out of the lookback window.
                self._daily_returns_by_industry[industry] = self._daily_returns_by_industry[industry][self._daily_returns_by_industry[industry].index >= self.time - self._lookback]
        self._previous_selection_time = self.time

        # Get the largest asset of each industry (and its fundamental factors).
        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 and self._has_all_factors(f)
            ]
            if industry_assets:
                leader = sorted(industry_assets, key=lambda f: f.market_cap)[-1]
                leader_factors = {}
                for factor in fundamental_factors + ['symbol']:
                    leader_factors[factor] = eval(f"leader.{factor}") 
                self._leader_by_industry[industry_code] = leader_factors

        # During warm-up, keep the universe empty.
        if self.is_warming_up or not (self.time.year % 4 == 0 and self.time.month == 11):
            return []

        # Cluster the assets based on their fundamental factors.
        factors_df = pd.DataFrame(columns=fundamental_factors)
        for industry, leader in self._leader_by_industry.items():
            factors_df.loc[industry, :] = [leader[f] for f in fundamental_factors]
        cluster_by_industry = pd.Series(
            self._clustering_model.fit_predict(self._scaler.fit_transform(factors_df)),
            index=self._leader_by_industry.keys()
        )
        
        # Calculate the performance of each cluster.
        other_party = 'D' if ruling_party == 'R' else 'R'
        cluster_scores = []
        for cluster in cluster_by_industry.unique():
            result = {}

            # Get the daily returns of the cluster.
            industries = cluster_by_industry[cluster_by_industry == cluster].index
            cluster_daily_returns = pd.concat([self._daily_returns_by_industry[industry] for industry in industries], axis=1)
            
            # Select the periods of time when the current party was in office.
            sliced_daily_returns = pd.DataFrame()
            for start_date, end_date in self._periods_by_party[ruling_party]:
                sliced_daily_returns = pd.concat([sliced_daily_returns, cluster_daily_returns.loc[start_date:end_date]])
            
            # Calculate metric (ex: Return of an equal-weighted portfolio).
            if sliced_daily_returns.empty:
                result[ruling_party] = 0
            else:
                equity_curve = (sliced_daily_returns.fillna(0).mean(axis=1) + 1).cumprod()
                result[ruling_party] = (equity_curve.iloc[-1] - equity_curve.iloc[0]) / equity_curve.iloc[0]

            # Select the periods of time when the other party was in office.
            sliced_daily_returns = pd.DataFrame()
            for start_date, end_date in self._periods_by_party[other_party]:
                sliced_daily_returns = pd.concat([sliced_daily_returns, cluster_daily_returns.loc[start_date:end_date]])
            
            # Calculate metric (ex: Return of an equal-weighted portfolio).
            if sliced_daily_returns.empty:
                result[other_party] = 0
            else:
                equity_curve = (sliced_daily_returns.fillna(0).mean(axis=1) + 1).cumprod()
                result[other_party] = (equity_curve.iloc[-1] - equity_curve.iloc[0]) / equity_curve.iloc[0]

            # Select the ROC over the trailing month.
            result['last_month'] = (cluster_daily_returns.fillna(0).mean(axis=1) + 1).cumprod().pct_change(21).iloc[-1]
            
            cluster_scores.append(result)

        # Whichever cluster performs best over the last month, find which party it leans towards.
        winning_cluster = sorted(cluster_scores, key=lambda scores: scores['last_month'])[-1]
        predicted_party = ruling_party if winning_cluster[ruling_party] > winning_cluster[other_party] else other_party
        self.debug(f'Predicted party: {predicted_party}')
        self.log(f'Predicted party: {predicted_party}')

        # Return an empty universe
        return []
        

# 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'
]