Overall Statistics
Total Orders
1978
Average Win
0.07%
Average Loss
-0.07%
Compounding Annual Return
-1.868%
Drawdown
15.300%
Expectancy
-0.014
Start Equity
1000000
End Equity
910000.67
Net Profit
-9.000%
Sharpe Ratio
-1.017
Sortino Ratio
-0.955
Probabilistic Sharpe Ratio
0.064%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
1.04
Alpha
-0.047
Beta
-0.013
Annual Standard Deviation
0.047
Annual Variance
0.002
Information Ratio
-0.786
Tracking Error
0.151
Treynor Ratio
3.779
Total Fees
$3625.46
Estimated Strategy Capacity
$4000.00
Lowest Capacity Asset
MHUA XW2TMLLH2VDX
Portfolio Turnover
0.20%
Drawdown Recovery
577
#region imports
from AlgorithmImports import *
#endregion


class NetCurrentAssetValue(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(1_000_000)
        self.settings.seed_initial_prices = True
        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.universe_settings.resolution = Resolution.DAILY
        self._rebalance_month = 6 # June
        self._previous_accrual_factors_by_symbol = {}
        self._sorted_assets = []
        date_rule = self.date_rules.month_start('SPY')
        self.add_universe(date_rule, self._select_assets)
        # monthly scheduled event but will only rebalance once a year
        self.schedule.on(date_rule, self.time_rules.midnight, self._rebalance)
        self.set_warmup(timedelta(365))

    def _select_assets(self, fundamentals):
        if self.time.month != self._rebalance_month:
            return []
        # Filters out companies that don't contain the necessary data.
        accrual_factors_by_symbol = {
            f.symbol: AccrualFactors(f) 
            for f in fundamentals if AccrualFactors.are_available(f)
        }
        # Calculate and sort the balance sheet accruals.
        score_by_symbol = {}
        for symbol, previous_factors in self._previous_accrual_factors_by_symbol.items():
            current_factors = accrual_factors_by_symbol.get(symbol, None)
            if not current_factors:
                continue
            score_by_symbol[symbol] = AccrualFactors.calculate_score(previous_factors, current_factors)
        self._sorted_assets = sorted(score_by_symbol, key=lambda symbol: score_by_symbol[symbol])
        self._previous_accrual_factors_by_symbol = accrual_factors_by_symbol
        return self._sorted_assets
    
    def _rebalance(self):
        if self.time.month != self._rebalance_month:
            return 
        # Drop assets that don't have a price yet, to avoid trading errors.
        self._sorted_assets = [symbol for symbol in self._sorted_assets if self.securities[symbol].price]
        if not self._sorted_assets:
            return
        # Pick the upper decile to short and the lower decile to long
        portfolio_size = int(len(self._sorted_assets)/10)
        targets = self._create_targets(self._sorted_assets[:portfolio_size], 0.25)
        targets += self._create_targets(self._sorted_assets[-portfolio_size:], -0.25)        
        self.set_holdings(targets, True)
    
    def _create_targets(self, symbols, total_weight):
        weight = total_weight / len(symbols)
        return [PortfolioTarget(symbol, weight) for symbol in symbols]
        

class AccrualFactors:

    def __init__(self, fundamental):
        self.cash = fundamental.financial_statements.balance_sheet.cash_and_cash_equivalents.value
        self.current_assets = fundamental.financial_statements.balance_sheet.current_assets.value
        self.total_assets = fundamental.financial_statements.balance_sheet.total_assets.value
        self.liabilities = fundamental.financial_statements.balance_sheet.current_liabilities.value
        self.debt = fundamental.financial_statements.balance_sheet.current_debt.value
        self.tax = fundamental.financial_statements.balance_sheet.income_tax_payable.value
        self.depreciation = fundamental.financial_statements.income_statement.depreciation_and_amortization.value
    
    @staticmethod
    def are_available(fundamental):
        return not any([
            np.isnan(factor) for factor in [
                fundamental.financial_statements.balance_sheet.cash_and_cash_equivalents.value,
                fundamental.financial_statements.balance_sheet.current_assets.value,
                fundamental.financial_statements.balance_sheet.total_assets.value,
                fundamental.financial_statements.balance_sheet.current_liabilities.value,
                fundamental.financial_statements.balance_sheet.current_debt.value,
                fundamental.financial_statements.balance_sheet.income_tax_payable.value,
                fundamental.financial_statements.income_statement.depreciation_and_amortization.value
            ]
        ])
    
    @staticmethod
    def calculate_score(previous_factors, current_factors):
        delta_assets = current_factors.current_assets - previous_factors.current_assets
        delta_cash = current_factors.cash - previous_factors.cash
        delta_liabilities = current_factors.liabilities - previous_factors.liabilities
        delta_debt = current_factors.debt - previous_factors.debt
        delta_tax = current_factors.tax - previous_factors.tax
        avg_total_assets = (current_factors.total_assets + previous_factors.total_assets) / 2
        depreciation = current_factors.depreciation
        return (
            ((delta_assets - delta_cash) - (delta_liabilities - delta_debt - delta_tax) - depreciation) 
            / avg_total_assets
        )