Overall Statistics
Total Orders
332
Average Win
1.59%
Average Loss
-1.45%
Compounding Annual Return
-0.474%
Drawdown
5.400%
Expectancy
0.009
Start Equity
100000
End Equity
97655.6
Net Profit
-2.344%
Sharpe Ratio
-2.548
Sortino Ratio
-1.396
Probabilistic Sharpe Ratio
0.085%
Loss Rate
52%
Win Rate
48%
Profit-Loss Ratio
1.09
Alpha
-0.041
Beta
-0
Annual Standard Deviation
0.016
Annual Variance
0
Information Ratio
-0.753
Tracking Error
0.142
Treynor Ratio
142.407
Total Fees
$8530.82
Estimated Strategy Capacity
$580000.00
Lowest Capacity Asset
UA.C W9JOYIS9CBXH
Portfolio Turnover
9.16%
Drawdown Recovery
269
# region imports
from math import floor

from AlgorithmImports import *
# endregion

class KalmanFilter:

    def __init__(self):
        self._delta = 1e-4
        self._wt = self._delta / (1 - self._delta) * np.eye(2)
        self._vt = 1e-3
        self._theta = np.zeros(2)
        self._P = np.zeros((2, 2))
        self._R = None

    def update(self, price_one, price_two, quantity):
        # Create the observation matrix of the latest prices
        # of TLT and the intercept value (1.0)
        F = np.asarray([price_one, 1.0]).reshape((1, 2))
        y = price_two

        # The prior value of the states \theta_t is
        # distributed as a multivariate Gaussian with
        # mean a_t and variance-covariance R_t
        if self._R is not None:
            self._R = self._C + self._wt
        else:
            self._R = np.zeros((2, 2))

        # Calculate the Kalman Filter update
        # ----------------------------------
        # Calculate prediction of new observation
        # as well as forecast error of that prediction
        yhat = F.dot(self._theta)
        et = y - yhat

        # Q_t is the variance of the prediction of
        # observations and hence \sqrt{Q_t} is the
        # standard deviation of the predictions
        Qt = F.dot(self._R).dot(F.T) + self._vt
        sqrt_Qt = np.sqrt(Qt)

        # The posterior value of the states \theta_t is
        # distributed as a multivariate Gaussian with mean
        # m_t and variance-covariance C_t
        At = self._R.dot(F.T) / Qt
        self._theta = self._theta + At.flatten() * et
        self._C = self._R - At * F.dot(self._R)
        hedge_quantity = int(floor(quantity*self._theta[0]))
        
        return et, sqrt_Qt, hedge_quantity
# region imports
from AlgorithmImports import *
from KalmanFilter import KalmanFilter
# endregion


class VerticalParticleInterceptor(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(100_000)
        # Add the asests to trade.
        self._security_a = self.add_equity('UA')
        self._security_b = self.add_equity('UAA')
        # Add the kalman filter.
        self.kf = KalmanFilter()
        # Add a Scheduled Event to update the kalman filter and place orders.
        self.schedule.on(
            self.date_rules.every_day(self._security_a), 
            self.time_rules.before_market_close(self._security_a, 5), 
            self._update_and_trade
        )
    
    def _update_and_trade(self):
        # Get recent price and holdings information.
        quantity = int(self.portfolio.total_portfolio_value / 2 / self._security_b.price)
        forecast_error, prediction_std_dev, hedge_quantity = self.kf.update(self._security_a.price, self._security_b.price, quantity)
        # Check for entries.
        if not self.portfolio.invested and hedge_quantity:
            # Long the spread.
            if forecast_error < -prediction_std_dev:
                insights = Insight.group([
                    Insight.price(self._security_a, timedelta(1), InsightDirection.DOWN),
                    Insight.price(self._security_b, timedelta(1), InsightDirection.UP)
                ])
                self.emit_insights(insights)
                self.market_order(self._security_b, quantity)
                self.market_order(self._security_a, -hedge_quantity)
            # Short the spread
            elif forecast_error > prediction_std_dev:
                insights = Insight.group([
                    Insight.price(self._security_a, timedelta(1), InsightDirection.UP),
                    Insight.price(self._security_b, timedelta(1), InsightDirection.DOWN)
                ])
                self.emit_insights(insights)
                self.market_order(self._security_b, -quantity)
                self.market_order(self._security_a, hedge_quantity)
        # Check for exits.
        if self.portfolio.invested:
            if (self._security_a.holdings.is_short and (forecast_error >= -prediction_std_dev) or
                self._security_a.holdings.is_long and (forecast_error <= prediction_std_dev)):
                insights = Insight.group([
                    Insight.price(self._security_a, timedelta(1), InsightDirection.FLAT),
                    Insight.price(self._security_b, timedelta(1), InsightDirection.FLAT)
                ])
                self.emit_insights(insights)
                self.liquidate()