Overall Statistics
Total Orders
1144
Average Win
0.91%
Average Loss
-0.61%
Compounding Annual Return
0.879%
Drawdown
33.600%
Expectancy
0.048
Start Equity
100000
End Equity
124491.15
Net Profit
24.491%
Sharpe Ratio
-0.167
Sortino Ratio
-0.197
Probabilistic Sharpe Ratio
0.000%
Loss Rate
58%
Win Rate
42%
Profit-Loss Ratio
1.51
Alpha
-0.014
Beta
0.008
Annual Standard Deviation
0.079
Annual Variance
0.006
Information Ratio
-0.321
Tracking Error
0.176
Treynor Ratio
-1.584
Total Fees
$3189.42
Estimated Strategy Capacity
$2000000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
7.05%
# region imports
from AlgorithmImports import *
from scipy.optimize import minimize
import numpy as np
# endregion

class TI_Research(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2000, 1, 1)
        self.set_cash(100000)

        ticker = "SPY"
        self.eq = self.add_equity(ticker, Resolution.DAILY).Symbol
        
        self.lookback = 252
        self.data_window = RollingWindow[TradeBar](self.lookback)
        self.initial_params = [0.01, 0.01, 0.1]

        self.ti_window = RollingWindow[float](3)

    def on_data(self, data: Slice):

        # Data preperation
        if not (data.contains_key(self.eq) and data[self.eq] is not None):
            return

        if not self.data_window.is_ready:
            trade_bars = self.history[TradeBar](self.eq, self.lookback+1, Resolution.DAILY)
            for tb in trade_bars:
                self.data_window.add(tb)
        
        tb = data.bars.get(self.eq)
        self.data_window.add(tb)

        dw = list(self.data_window)
        volume = [tb.volume for tb in dw]
        lgr = [np.log((dw[i-1].close / dw[i].close)) for i in range(1, len(dw))] # Log returns for later reusability

        result = minimize(self.transient_impact, self.initial_params, args=(volume, lgr), method='CG')
        # alpha, beta, lambda_ = result.x

        # Logging output is so output is easier to read
        ti = np.log10(self.transient_impact(result.x, volume, lgr) + 0.00000000001) # small value to avoid log(0)
        self.ti_window.add(ti)
        self.plot("Transient impact", "Value", ti)

        if not self.ti_window.is_ready: return

        ti_min = self.ti_window[2] > self.ti_window[1] and self.ti_window[0] > self.ti_window[1] # Simple local minimum in transient impact
        # To make this more sophisticated, could use a longer ti window and check if the 0 index is a saddle point or not

        long = ti_min and self.data_window[1].close > self.data_window[0].close # Long off recent dips
        short = ti_min and self.data_window[1].close < self.data_window[0].close # Short off recent tops

        if long: # Enter long
            self.set_holdings(self.eq, 0.50, True)
        elif short: # Enter short
            self.set_holdings(self.eq, -0.50, True)

    def transient_impact(self, params, volume, lgr):
        # Get input how we need it
        alpha, beta, lambda_ = params
        volume = np.array(volume)

        # Get the current level of impact
        instantaneous = alpha * volume[:-1]

        # Get the impact we expect to see with exponential decay
        transient = np.array([beta * np.sum(volume[:t] * np.exp(-lambda_ * (t - np.arange(t)))) for t in range(1, len(volume))])

        # Evaluate the transient impact model value
        predicted_change = instantaneous + transient
        return np.sum((lgr - predicted_change) ** 2)