Overall Statistics
Total Orders
1999
Average Win
0.03%
Average Loss
-0.02%
Compounding Annual Return
7.874%
Drawdown
6.000%
Expectancy
0.476
Start Equity
100000000
End Equity
107821873.96
Net Profit
7.822%
Sharpe Ratio
0.063
Sortino Ratio
0.069
Probabilistic Sharpe Ratio
12.388%
Loss Rate
44%
Win Rate
56%
Profit-Loss Ratio
1.62
Alpha
0.003
Beta
0.001
Annual Standard Deviation
0.049
Annual Variance
0.002
Information Ratio
-1.068
Tracking Error
0.119
Treynor Ratio
2.754
Total Fees
$28864.94
Estimated Strategy Capacity
$4200000.00
Lowest Capacity Asset
BSQR RP1305HZ49K5
Portfolio Turnover
0.74%
Drawdown Recovery
71
# region imports
from AlgorithmImports import *
from typing import List
from math import isfinite
# endregion


class FundamentalSnapshot:
    def __init__(
        self,
        formation_date: datetime,
        file_date: datetime,
        period_ending_date: datetime,
        income_before_extraordinary_items: float,
        total_assets: float,
        industry_code: int,
        market_cap: float,
        business_country_id: str,
        primary_exchange_id: str,
        security_type: str,
        is_primary_share: bool,
    ) -> None:
        self.formation_date = formation_date
        self.file_date = file_date
        self.period_ending_date = period_ending_date
        self.income_before_extraordinary_items = income_before_extraordinary_items
        self.total_assets = total_assets
        self.industry_code = industry_code
        self.market_cap = market_cap
        self.business_country_id = business_country_id
        self.primary_exchange_id = primary_exchange_id
        self.security_type = security_type
        self.is_primary_share = is_primary_share
        self.average_assets = None
        self.return_on_assets = None
        self.market_excluded_return_on_assets = None
        self.industry_excluded_return_on_assets = None
        self.industry_market_intercept = None
        self.industry_market_slope = None
        self.industry_market_residual = None
        self.industry_earnings_beta = None
        self.market_earnings_beta = None
        self.is_beta_eligible = False
        self.market_return_on_assets_component = None
        self.industry_return_on_assets_component = None
        self.idiosyncratic_return_on_assets_component = None


class IdiosyncraticProfitabilityAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 12, 31)
        self.set_cash(100_000_000)
        self.set_warm_up(timedelta(days=5 * 365))
        self.settings.min_absolute_portfolio_target_percentage = 0
        self.settings.minimum_order_margin_portfolio_percentage = 0

        self._snapshots = {}
        self._panels = {}
        self._eligible_snapshots = []
        self._bottom_quintile = []
        self._top_quintile = []
        self._portfolio_weights = {}
        self._selected_symbols = []
        self._pending_weights = None
        self._pending_formation_date = None
        self._spy = self.add_equity("SPY", Resolution.DAILY).symbol
        self._formation_rule = FuncDateRule(
            "QuarterlyFormation",
            self._get_formation_dates,
        )
        self.universe_settings.resolution = Resolution.DAILY
        self.universe_settings.schedule.on(self._formation_rule)
        self._universe = self.add_universe(self._select_assets)
        self.schedule.on(
            self.date_rules.every_day("SPY"),
            self.time_rules.after_market_close("SPY", 1),
            self._execute_rebalance,
        )

    def _get_formation_dates(
        self,
        start: datetime,
        end: datetime,
    ) -> List[datetime]:
        hours = self.securities[self._spy].exchange.hours
        dates = []
        current = start.date()

        while current <= end.date():
            if current.month in (2, 5, 8, 11) and hours.is_date_open(current):
                next_trading_day = hours.get_next_trading_day(current)
                if next_trading_day.month != current.month:
                    dates.append(datetime.combine(current, datetime.min.time()))
            current += timedelta(days=1)

        return dates

    def _previous_calendar_quarter_end(self, period_end: datetime) -> datetime:
        previous_quarter = {
            3: (period_end.year - 1, 12),
            6: (period_end.year, 3),
            9: (period_end.year, 6),
            12: (period_end.year, 9),
        }
        year, month = previous_quarter[period_end.month]
        last_day = 31 if month in (3, 12) else 30
        return datetime(year, month, last_day)

    def _is_usable_roa_record(self, snapshot: FundamentalSnapshot) -> bool:
        return (
            snapshot.return_on_assets is not None
            and isfinite(snapshot.income_before_extraordinary_items)
            and isfinite(snapshot.average_assets)
            and isfinite(snapshot.return_on_assets)
        )

    def _update_excluded_roas(self) -> None:
        records_by_period = {}
        for symbol, symbol_snapshots in self._snapshots.items():
            for period_end, snapshot in symbol_snapshots.items():
                snapshot.market_excluded_return_on_assets = None
                snapshot.industry_excluded_return_on_assets = None
                if self._is_usable_roa_record(snapshot):
                    records_by_period.setdefault(period_end, []).append(
                        (symbol, snapshot)
                    )

        for records in records_by_period.values():
            market_total_ib = sum(
                snapshot.income_before_extraordinary_items
                for _, snapshot in records
            )
            market_total_average_assets = sum(
                snapshot.average_assets for _, snapshot in records
            )
            industry_records = {}
            for symbol, snapshot in records:
                industry_records.setdefault(snapshot.industry_code, []).append(
                    (symbol, snapshot)
                )

            for _, snapshot in records:
                market_excluded_ib = (
                    market_total_ib - snapshot.income_before_extraordinary_items
                )
                market_excluded_average_assets = (
                    market_total_average_assets - snapshot.average_assets
                )
                if market_excluded_average_assets != 0:
                    snapshot.market_excluded_return_on_assets = (
                        market_excluded_ib / market_excluded_average_assets
                    )

                peers = industry_records[snapshot.industry_code]
                if len(peers) < 5:
                    continue

                industry_total_ib = sum(
                    peer.income_before_extraordinary_items for _, peer in peers
                )
                industry_total_average_assets = sum(
                    peer.average_assets for _, peer in peers
                )
                industry_excluded_ib = (
                    industry_total_ib - snapshot.income_before_extraordinary_items
                )
                industry_excluded_average_assets = (
                    industry_total_average_assets - snapshot.average_assets
                )
                if industry_excluded_average_assets != 0:
                    snapshot.industry_excluded_return_on_assets = (
                        industry_excluded_ib / industry_excluded_average_assets
                    )

    def _update_panels(self) -> None:
        self._panels = {}
        for symbol, symbol_snapshots in self._snapshots.items():
            panel = []
            for period_end in sorted(symbol_snapshots):
                snapshot = symbol_snapshots[period_end]
                if (
                    snapshot.return_on_assets is None
                    or snapshot.market_excluded_return_on_assets is None
                    or snapshot.industry_excluded_return_on_assets is None
                ):
                    continue
                panel.append(snapshot)
            self._panels[symbol] = panel[-20:]

    def _update_industry_market_regressions(self) -> None:
        for panel in self._panels.values():
            for snapshot in panel:
                snapshot.industry_market_intercept = None
                snapshot.industry_market_slope = None
                snapshot.industry_market_residual = None

            if len(panel) < 10:
                continue

            market_mean = sum(
                snapshot.market_excluded_return_on_assets for snapshot in panel
            ) / len(panel)
            industry_mean = sum(
                snapshot.industry_excluded_return_on_assets for snapshot in panel
            ) / len(panel)
            market_sum_squared_deviation = sum(
                (snapshot.market_excluded_return_on_assets - market_mean) ** 2
                for snapshot in panel
            )
            if market_sum_squared_deviation == 0:
                continue

            market_industry_covariance = sum(
                (snapshot.market_excluded_return_on_assets - market_mean)
                * (snapshot.industry_excluded_return_on_assets - industry_mean)
                for snapshot in panel
            )
            slope = market_industry_covariance / market_sum_squared_deviation
            intercept = industry_mean - slope * market_mean

            for snapshot in panel:
                fitted_industry_roa = (
                    intercept
                    + slope * snapshot.market_excluded_return_on_assets
                )
                snapshot.industry_market_intercept = intercept
                snapshot.industry_market_slope = slope
                snapshot.industry_market_residual = (
                    snapshot.industry_excluded_return_on_assets
                    - fitted_industry_roa
                )

    def _solve_three_by_three(
        self,
        matrix: List[List[float]],
        vector: List[float],
    ) -> List[float]:
        augmented = [
            matrix[row][:] + [vector[row]] for row in range(3)
        ]
        for column in range(3):
            pivot_row = max(
                range(column, 3),
                key=lambda row: abs(augmented[row][column]),
            )
            if augmented[pivot_row][column] == 0:
                return []
            augmented[column], augmented[pivot_row] = (
                augmented[pivot_row],
                augmented[column],
            )
            pivot = augmented[column][column]
            augmented[column] = [value / pivot for value in augmented[column]]
            for row in range(3):
                if row == column:
                    continue
                factor = augmented[row][column]
                augmented[row] = [
                    augmented[row][index] - factor * augmented[column][index]
                    for index in range(4)
                ]
        return [augmented[row][3] for row in range(3)]

    def _update_own_roa_regressions(self) -> None:
        for panel in self._panels.values():
            for snapshot in panel:
                snapshot.industry_earnings_beta = None
                snapshot.market_earnings_beta = None
                snapshot.is_beta_eligible = False

            if (
                len(panel) < 10
                or any(
                    snapshot.industry_market_residual is None
                    for snapshot in panel
                )
            ):
                continue

            industry_residuals = [
                snapshot.industry_market_residual for snapshot in panel
            ]
            market_roas = [
                snapshot.market_excluded_return_on_assets for snapshot in panel
            ]
            own_roas = [snapshot.return_on_assets for snapshot in panel]
            observation_count = len(panel)
            matrix = [
                [
                    observation_count,
                    sum(industry_residuals),
                    sum(market_roas),
                ],
                [
                    sum(industry_residuals),
                    sum(value ** 2 for value in industry_residuals),
                    sum(
                        industry_residuals[index] * market_roas[index]
                        for index in range(observation_count)
                    ),
                ],
                [
                    sum(market_roas),
                    sum(
                        industry_residuals[index] * market_roas[index]
                        for index in range(observation_count)
                    ),
                    sum(value ** 2 for value in market_roas),
                ],
            ]
            vector = [
                sum(own_roas),
                sum(
                    industry_residuals[index] * own_roas[index]
                    for index in range(observation_count)
                ),
                sum(
                    market_roas[index] * own_roas[index]
                    for index in range(observation_count)
                ),
            ]
            coefficients = self._solve_three_by_three(matrix, vector)
            if not coefficients:
                continue

            industry_beta = coefficients[1]
            market_beta = coefficients[2]
            is_eligible = abs(industry_beta) <= 3 and abs(market_beta) <= 3
            for snapshot in panel:
                snapshot.industry_earnings_beta = industry_beta
                snapshot.market_earnings_beta = market_beta
                snapshot.is_beta_eligible = is_eligible

    def _update_current_quarter_components(self) -> None:
        for panel in self._panels.values():
            for snapshot in panel:
                snapshot.market_return_on_assets_component = None
                snapshot.industry_return_on_assets_component = None
                snapshot.idiosyncratic_return_on_assets_component = None

                if (
                    not snapshot.is_beta_eligible
                    or snapshot.industry_earnings_beta is None
                    or snapshot.market_earnings_beta is None
                    or snapshot.industry_market_residual is None
                ):
                    continue

                market_component = (
                    snapshot.market_earnings_beta
                    * snapshot.market_excluded_return_on_assets
                )
                industry_component = (
                    snapshot.industry_earnings_beta
                    * snapshot.industry_market_residual
                )
                snapshot.market_return_on_assets_component = market_component
                snapshot.industry_return_on_assets_component = industry_component
                snapshot.idiosyncratic_return_on_assets_component = (
                    snapshot.return_on_assets
                    - market_component
                    - industry_component
                )

    def _form_idiosyncratic_quintiles(self) -> None:
        self._eligible_snapshots = []
        self._bottom_quintile = []
        self._top_quintile = []

        for symbol, panel in self._panels.items():
            if not panel:
                continue
            snapshot = panel[-1]
            if (
                not snapshot.is_beta_eligible
                or snapshot.idiosyncratic_return_on_assets_component is None
                or not isfinite(snapshot.idiosyncratic_return_on_assets_component)
                or not isfinite(snapshot.market_cap)
                or snapshot.market_cap <= 0
                or snapshot.business_country_id != "USA"
                or snapshot.primary_exchange_id not in ("NYS", "ASE", "NAS")
                or snapshot.security_type != "ST00000001"
                or not snapshot.is_primary_share
            ):
                continue
            self._eligible_snapshots.append((symbol, snapshot))

        self._eligible_snapshots.sort(
            key=lambda item: item[1].idiosyncratic_return_on_assets_component
        )
        quintile_size = len(self._eligible_snapshots) // 5
        if quintile_size == 0:
            return

        self._bottom_quintile = self._eligible_snapshots[:quintile_size]
        self._top_quintile = self._eligible_snapshots[-quintile_size:]

    def _calculate_portfolio_weights(self) -> None:
        self._portfolio_weights = {}
        if not self._bottom_quintile or not self._top_quintile:
            return

        bottom_market_cap = sum(
            snapshot.market_cap for _, snapshot in self._bottom_quintile
        )
        top_market_cap = sum(
            snapshot.market_cap for _, snapshot in self._top_quintile
        )
        if bottom_market_cap <= 0 or top_market_cap <= 0:
            return

        for symbol, snapshot in self._bottom_quintile:
            self._portfolio_weights[symbol] = (
                -0.5 * snapshot.market_cap / bottom_market_cap
            )
        for symbol, snapshot in self._top_quintile:
            self._portfolio_weights[symbol] = (
                0.5 * snapshot.market_cap / top_market_cap
            )

    def _select_final_constituents(self) -> None:
        self._selected_symbols = list(self._portfolio_weights)

    def _execute_rebalance(self) -> None:
        if (
            self._pending_weights is None
            or self._pending_formation_date is None
        ):
            return

        selected_count = len(self._pending_weights)
        priced_weights = {
            symbol: weight
            for symbol, weight in self._pending_weights.items()
            if self.securities[symbol].price != 0
        }
        zero_price_excluded_count = selected_count - len(priced_weights)
        executable_weights = {}
        for symbol, weight in priced_weights.items():
            quantity = self.calculate_order_quantity(symbol, weight)
            if quantity != 0:
                executable_weights[symbol] = (weight, quantity)

        zero_quantity_excluded_count = len(priced_weights) - len(executable_weights)
        target_symbols = set(executable_weights)
        liquidated_holding_count = sum(
            1
            for holding in self.portfolio.values()
            if holding.invested and holding.symbol not in target_symbols
        )
        target_long_weights = [
            weight
            for weight, _ in executable_weights.values()
            if weight > 0
        ]
        target_short_weights = [
            weight
            for weight, _ in executable_weights.values()
            if weight < 0
        ]
        long_weight_sum = sum(target_long_weights)
        short_weight_sum = sum(target_short_weights)
        self.log(
            "FORMATION_SUMMARY "
            f"date={self._pending_formation_date:%Y-%m-%d} "
            f"selected={selected_count} "
            f"zero_price_excluded={zero_price_excluded_count} "
            f"zero_quantity_excluded={zero_quantity_excluded_count} "
            f"executable={len(executable_weights)} "
            f"target_longs={len(target_long_weights)} "
            f"target_shorts={len(target_short_weights)} "
            f"long_weight={long_weight_sum:.10f} "
            f"short_weight={short_weight_sum:.10f} "
            f"net_weight={long_weight_sum + short_weight_sum:.10f} "
            f"gross_weight={long_weight_sum - short_weight_sum:.10f} "
            f"departed_holding_liquidations={liquidated_holding_count}"
        )

        for symbol, (_, quantity) in executable_weights.items():
            self.market_on_open_order(symbol, quantity)

        for holding in self.portfolio.values():
            if holding.invested and holding.symbol not in target_symbols:
                self.market_on_open_order(holding.symbol, -holding.quantity)

        self._pending_weights = None
        self._pending_formation_date = None

    def on_end_of_algorithm(self) -> None:
        invested_holdings = [
            holding for holding in self.portfolio.values() if holding.invested
        ]
        final_long_count = sum(
            1 for holding in invested_holdings if holding.quantity > 0
        )
        final_short_count = sum(
            1 for holding in invested_holdings if holding.quantity < 0
        )
        self.log(
            "FINAL_HOLDINGS_SUMMARY "
            f"longs={final_long_count} shorts={final_short_count} "
            f"invested={len(invested_holdings)}"
        )

    def _select_assets(self, fundamentals: List[Fundamental]) -> List[Symbol]:
        formation_date = self.time

        for fundamental in fundamentals:
            statements = fundamental.financial_statements
            file_date = statements.file_date.three_months
            period_ending_date = statements.period_ending_date.three_months

            if period_ending_date.month not in (3, 6, 9, 12):
                continue

            if file_date < period_ending_date or file_date > formation_date:
                continue

            snapshot = FundamentalSnapshot(
                formation_date,
                file_date,
                period_ending_date,
                statements.income_statement.net_income_continuous_operations.three_months,
                statements.balance_sheet.total_assets.three_months,
                fundamental.asset_classification.morningstar_industry_code,
                fundamental.market_cap,
                fundamental.company_reference.business_country_id,
                fundamental.company_reference.primary_exchange_id,
                fundamental.security_reference.security_type,
                fundamental.security_reference.is_primary_share,
            )
            symbol_snapshots = self._snapshots.setdefault(fundamental.symbol, {})
            symbol_snapshots[period_ending_date] = snapshot

            if len(symbol_snapshots) > 20:
                del symbol_snapshots[min(symbol_snapshots)]

            prior_period = self._previous_calendar_quarter_end(period_ending_date)
            if prior_period in symbol_snapshots:
                prior_snapshot = symbol_snapshots[prior_period]
                average_assets = (
                    snapshot.total_assets + prior_snapshot.total_assets
                ) / 2
                if isfinite(average_assets) and average_assets != 0:
                    snapshot.average_assets = average_assets
                    snapshot.return_on_assets = (
                        snapshot.income_before_extraordinary_items / average_assets
                    )

        self._update_excluded_roas()
        self._update_panels()
        self._update_industry_market_regressions()
        self._update_own_roa_regressions()
        self._update_current_quarter_components()
        self._form_idiosyncratic_quintiles()
        self._calculate_portfolio_weights()
        self._select_final_constituents()

        selector_output = [] if self.is_warming_up else self._selected_symbols
        if not self.is_warming_up and self._selected_symbols:
            self._pending_weights = self._portfolio_weights.copy()
            self._pending_formation_date = formation_date

        return selector_output
SPEC = "# Idiosyncratic Profitability — Quintile Hedge Portfolio on the Firm-Specific Component of ROA\n\n**Deutsche Bank \"Academic Insights\" (AI October 2025), item #81 — \"Cash equities — Accountancy #8: Idiosyncratic Profitability\" (p.277).**\n**Underlying paper:** Han, Jackson & Monroe, *\"Excess Returns on Idiosyncratic Profitability: Evidence from a Hedge Portfolio Strategy\"*, Australian Journal of Management, forthcoming (SSRN 4890319). Disaggregation method from Jackson, Plumlee & Rountree (2018), Review of Financial Studies. Sample CRSP/Compustat 1977–2023. No author code published.\n\nImplement the **full method** below. Do not build a reduced version, and do not substitute total profitability (plain ROA) for the disaggregated idiosyncratic component — the paper shows plain ROA carries no significant return premium while the idiosyncratic component does.\n\n---\n\n## Structured brief (for the QC Assistants)\n\n**Concept.** Disaggregate each firm's quarterly return on assets (ROA) into three additive components — a market component, an industry component, and a firm-idiosyncratic component — using firm-specific earnings betas estimated from the past twenty quarters. Rank firms on the idiosyncratic component (IdiosROA), hold a hedge portfolio long the top quintile and short the bottom quintile, value-weighted within legs, refreshed each quarter when the new quarter's accounting data has become available for the full cross-section.\n\n**Why the pattern exists.** The market and industry components of a firm's profitability are easy for investors to verify and anticipate: macroeconomic and industry-level information is abundant, and common information reaches prices as peer firms announce earnings. The idiosyncratic component — the ex-post earnings outcome of the firm's own strategic response to competitive pressure — is the opposite: it is harder to verify, less persistent (competition and imitation erode firm-specific advantage), and costlier to analyze, and capital markets systematically discount uniqueness in firm strategy because evaluating a unique strategy is expensive. Investors therefore make systematic expectation errors about exactly this component: they underreact to strategy-driven performance. Consistent with mispricing rather than risk, the premium loads on no standard factor (alphas survive CAPM, Fama-French 3/5/6-factor, and Hou-Xue-Zhang q-factor models), plain ROA and the market/industry components carry no premium, and sophisticated investors (institutions, analysts) concentrate their holdings monotonically in the high-IdiosROA quintiles.\n\n**Investment universe.** US common stocks listed on NYSE, AMEX, or NASDAQ with an industry classification and quarterly financial statements. Only firms whose fiscal quarters end in March, June, September, and December (calendar-quarter reporters). A firm-quarter enters only if its industry has at least five member firms that quarter, the firm has at least ten of the past twenty quarters of data available to estimate its earnings betas, and its estimated market and industry betas are both no larger than 3 in absolute value.\n\n**Signal.** IdiosROA — the firm-idiosyncratic component of quarterly ROA, i.e. what remains of ROA after removing the fitted market and industry components implied by the firm's own historical sensitivities (exact procedure below).\n\n**Portfolio construction & rebalance.** Quarterly. At the end of February, May, August, and November (two months after each calendar quarter-end, so that essentially all firms have reported), re-estimate the disaggregation on the just-completed quarter, sort all eligible firms into quintiles on the signed magnitude of IdiosROA (equal-count quintiles of the eligible cross-section), and hold: LONG the top quintile, SHORT the bottom quintile, value-weighted by market capitalization within each leg, legs of equal gross size (long 0.5, short 0.5 of portfolio value — total gross exposure 1x, dollar-neutral). Hold until the next quarterly refresh. Trading decisions are made after the close of the formation day; orders fill at the next market open.\n\n**Reference.** Han, Miaodi, Andrew B. Jackson & Gary S. Monroe (2025), SSRN 4890319 — §3.1 (disaggregation), §3.2 (sample), §4.2 (trading strategy), Appendix 1 (variable definitions). Jackson, Plumlee & Rountree (2018), RFS — the earnings-beta disaggregation method.\n\n---\n\n## Implementation specifics from the source paper\n\nNotation: firm i, industry j, calendar quarter t. All accounting inputs are quarterly (single-quarter, not trailing-twelve-month) values.\n\n1. **Quarterly profitability.**\n\n$$\nROA_{i,t} = \\frac{IB_{i,t}}{\\tfrac{1}{2}\\left(AT_{i,t} + AT_{i,t-1}\\right)}\n$$\n\n   where `IB` is quarterly income before extraordinary items (quarterly net income from continuing operations) and `AT` is total assets; the denominator is the average of beginning and ending total assets.\n\n2. **Firm-excluded market and industry ROA.** For each firm-quarter, compute the market-level and industry-level ROA *excluding the firm itself* (this prevents large firms from mechanically loading on themselves):\n\n$$\nROA^{M}_{i,t} = \\frac{\\sum_k IB_{k,t} - IB_{i,t}}{\\sum_k AT_{k,t} - AT_{i,t}}, \\qquad\nROA^{I}_{i,j,t} = \\frac{\\sum_{k \\in j} IB_{k,t} - IB_{i,t}}{\\sum_{k \\in j} AT_{k,t} - AT_{i,t}}\n$$\n\n   The market sum runs over all sample firms in quarter t; the industry sum runs over the members of firm i's industry j. Industry = the six-digit GICS level, an intermediate granularity of roughly 60–70 industries; where GICS itself is unavailable, use the data provider's industry-group classification of comparable granularity. Each `AT` in these sums is the same beginning/ending average used in item 1. An industry-quarter must contain at least five firms; otherwise its member firm-quarters are excluded.\n\n3. **Two-step earnings-beta estimation.** For each firm at each quarterly refresh, using the trailing twenty quarters of the three series above (minimum ten non-missing quarters; the firm is excluded this quarter if fewer):\n\n   Step 1 — orthogonalize industry to market. Regress the firm's industry-ROA series on its market-ROA series and keep the residual series, which is the pure-industry component stripped of market effects (common market/industry information is attributed to the market):\n\n$$\nROA^{I}_{i,j,t} = \\beta_0 + \\beta_1 \\, ROA^{M}_{i,t} + \\epsilon_{i,j,t}\n$$\n\n   Step 2 — earnings betas. Regress the firm's own ROA series on the orthogonalized industry residual and the market ROA:\n\n$$\nROA_{i,t} = \\beta_0' + \\beta_1' \\, \\hat{\\epsilon}_{i,j,t} + \\beta_2' \\, ROA^{M}_{i,t} + \\epsilon'_{i,t}\n$$\n\n   The slopes are the firm's industry earnings beta (beta_1') and market earnings beta (beta_2'). Exclude the firm this quarter if either |beta_1'| > 3 or |beta_2'| > 3.\n\n4. **Component construction.** Apply the estimated betas to the current quarter's values, and define the idiosyncratic component as the remainder so the three components sum exactly to total ROA:\n\n$$\nMktROA_{i,t} = \\hat{\\beta}_2' \\, ROA^{M}_{i,t}, \\qquad\nIndROA_{i,t} = \\hat{\\beta}_1' \\, \\hat{\\epsilon}_{i,j,t}, \\qquad\nIdiosROA_{i,t} = ROA_{i,t} - MktROA_{i,t} - IndROA_{i,t}\n$$\n\n   where the current quarter's orthogonalized industry value is the Step-1 residual evaluated at quarter t.\n\n5. **Formation timing (reporting lag).** The disaggregation for the calendar quarter ending in month M is performed at the end of month M+2 (end of February, May, August, November), by which point essentially all calendar-quarter reporters have filed. Use each firm's most recently *reported* quarterly statements as of the formation date — never statements whose public release date is after the formation date. Portfolio decisions are made after the close of the formation day; the rebalance fills at the next market open. Positions are held unchanged until the next quarterly formation.\n\n6. **Sort and weights.** At each formation, rank all eligible firms by the signed value of IdiosROA and cut into five equal-count quintiles. LONG every firm in the top quintile, SHORT every firm in the bottom quintile. Within each leg, weight positions by market capitalization (value-weighted). Scale the legs to +0.5 and −0.5 of portfolio value: total gross exposure 1x, net exposure 0. A firm that delists mid-quarter leaves the book at its delisting (no special handling beyond the platform's delisting processing).\n\n---\n"

# === CLIENT-DIRECTED OVERRIDE (2026-07-27) - supersedes: execution treatment for selected constituents that have no tradable price at rebalance - directed change: In the rebalance function, check whether each selected security price is zero; do not submit an order for a selected security whose price is zero. ===

# === CLIENT-DIRECTED OVERRIDE (2026-07-27) - supersedes: any temporary starting-cash scaling used to force small selected targets into nonzero share quantities - directed change: Keep starting cash at $100,000,000. ===

# === CLIENT-DIRECTED OVERRIDE (2026-07-27) - supersedes: the requirement to submit an order for every selected nonzero-price constituent when its exact value-weight target rounds to zero shares - directed change: Explicitly exclude and report selected nonzero-price constituents whose built-in calculated order quantity is zero; do not force a minimum one-share order. ===