Overall Statistics
Total Orders
15944
Average Win
0.66%
Average Loss
-0.45%
Compounding Annual Return
7.048%
Drawdown
36.400%
Expectancy
0.056
Start Equity
100000
End Equity
563755.04
Net Profit
463.755%
Sharpe Ratio
0.27
Sortino Ratio
0.313
Probabilistic Sharpe Ratio
0.023%
Loss Rate
57%
Win Rate
43%
Profit-Loss Ratio
1.46
Alpha
0
Beta
0
Annual Standard Deviation
0.133
Annual Variance
0.018
Information Ratio
0.432
Tracking Error
0.133
Treynor Ratio
0
Total Fees
$174127.22
Estimated Strategy Capacity
$52000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
356.77%
# region imports
from AlgorithmImports import *
import numpy as np
from collections import deque
# endregion

class FormalRedPelican(QCAlgorithm):

    def initialize(self):
        self.set_start_date(1999, 1, 1)
        self.set_cash(100000)
        self.set_brokerage_model(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)

        spy = self.add_equity("SPY", Resolution.MINUTE)
        self._symbol = spy.symbol
        spy.margin_model = PatternDayTradingMarginModel()

        self._vwap = self.vwap(spy.symbol)
        self._roc = self.rocp(self._symbol, 1, Resolution.DAILY)
        self._vol = IndicatorExtensions.of(StandardDeviation(14), self._roc)
        self._deviation = AbsoluteDeviation('deviation', 14)

        self.consolidator = TradeBarConsolidator(timedelta(minutes=30))
        self.consolidator.data_consolidated += (self.consolidate)
        self.subscription_manager.add_consolidator(self._symbol, self.consolidator)

        self.previous_date = None
        self.open_price = None
        self.previous_close = None
        self.bars = deque(maxlen=2)

        self.schedule.on(
            self.date_rules.every_day(self._symbol),
            self.time_rules.before_market_close(self._symbol, 1),
            self.end_of_day
        )

    def consolidate(self, sender, bar):
        current_date = bar.end_time.date()

        self.bars.append(bar)

        if current_date != self.previous_date:
            self.previous_date = current_date
            self.open_price = bar.open
            self.previous_close = self.bars[-2].close if len(self.bars) == 2 else None

        self._deviation.update(bar)

        if not self._vol.is_ready or not self.previous_close:
            return

        upper_band = (max(self.open_price, self.previous_close) * (1 + self._deviation.value))
        lower_band = (min(self.open_price, self.previous_close) * (1 - self._deviation.value))

        vwap_price = self._vwap.current.value

        long_stop_price = np.max([vwap_price, upper_band])
        short_stop_price = np.min([vwap_price, lower_band])

        is_long = self.portfolio[self._symbol].is_long
        is_short = self.portfolio[self._symbol].is_short

        is_long_stopped_out = is_long and bar.close < long_stop_price
        is_short_stopped_out = is_short and bar.close > short_stop_price

        vol_target = 0.02
        spy_vol = self._vol.current.value / 100
        leverage = np.min([4, vol_target / spy_vol])

        if is_long_stopped_out or is_short_stopped_out:
            self.liquidate()

        if bar.close > upper_band and not is_long:
            self.set_holdings(self._symbol, 1 * leverage)
        elif bar.close < lower_band and not is_short:
            self.set_holdings(self._symbol, -1 * leverage)


    def end_of_day(self):
        self.set_holdings("SPY", 0)


class AbsoluteDeviation(PythonIndicator):

    def __init__(self, name, period):
        super().__init__()
        self.name = name
        self.period = period
        self.data = {}

        self.previous_data = None
        self.open_price = None

    def update(self, data: BaseData):
        current_data = data.end_time.date()

        if current_data != self.previous_data:
            self.previous_data = current_data
            self.open_price = data.open

        current_time = data.end_time.time()
        
        if current_time not in self.data:
            self.data[current_time] = deque(maxlen=self.period)

        self.data[current_time].append(
            np.abs(data.close / self.open_price - 1)
            )
        
        self.value = np.mean(self.data[current_time])

        return len(self.data[current_time]) == self.period