| Overall Statistics |
|
Total Trades 94 Average Win 0.02% Average Loss -0.02% Compounding Annual Return 0.166% Drawdown 0.200% Expectancy 0.015 Net Profit 0.014% Sharpe Ratio 0.394 Probabilistic Sharpe Ratio 43.229% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.17 Alpha -0.01 Beta 0.046 Annual Standard Deviation 0.008 Annual Variance 0 Information Ratio -2.629 Tracking Error 0.109 Treynor Ratio 0.067 Total Fees $94.00 Estimated Strategy Capacity $10000000.00 |
from SymbolData import SymbolData
class PSARStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2020, 11, 16) # Set Start Date
self.SetEndDate(2020, 12, 16) # Set End Date
self.SetCash(100000) # Set Strategy Cash
# Handles setting benchmark symbol for statistics
# The benchmark is NOT traded
self.benchmark = "SPY"
self.AddEquity(self.benchmark, Resolution.Minute)
self.SetBenchmark(self.benchmark)
# Trading from 9:30 AM to 11:30 AM
self.trading_start_time = (9, 30)
self.trading_end_time = (11, 30)
# Creates a dummy 5 minute consolidator, we can use to calculate signals
# on a regular interval
self.five_minute_consolidator = QuoteBarConsolidator(timedelta(minutes=5))
self.SubscriptionManager.AddConsolidator(self.benchmark, self.five_minute_consolidator)
self.five_minute_consolidator.DataConsolidated += self.EveryFiveMinutes
# our chosen tickers
tickers = ["JNJ", "AAPL", "WFC"]
# data subscriptions
for ticker in tickers:
symbol = self.AddEquity(ticker, Resolution.Minute).Symbol
# dictionary to hold symbol_data objects for each symbol
# in our universe
self.symbols = {}
# EOD scheduled event to liquidate holdings
self.Schedule.On(self.DateRules.EveryDay(self.benchmark), self.TimeRules.BeforeMarketClose(self.benchmark, 1), self.EndOfDayLiquidate)
def EveryFiveMinutes(self, sender, bar):
'''Fires every 5 minutes
Handles portfolio management logic
1. Loops through universe and finds securities with valid entry signals and no existing position
2. Liquidates all invested securities which meet exit signal
3. Distributes portfolio equally across all existing positions'''
# If not trading hours, do nothing
if not self.DesignatedTradingHours:
return
# list to hold new entry symbols
selected_entry_symbols = []
# loop through universe and find uninvested symbols which meet entry signal
# does not submit orders, just collects them into a list
for symbol, symbol_data in self.symbols.items():
if not symbol_data.IsReady:
continue
# self.Debug(f"{symbol} Ready")
valid_entry_signal = self.CalculateEntrySignal(symbol_data)
if not self.Portfolio[symbol].Invested and valid_entry_signal:
selected_entry_symbols.append(symbol)
### EXIT LOGIC
# Loops through universe
# Looks for invested symbols which meet exit criteria
for symbol, symbol_data in self.symbols.items():
if not self.Portfolio[symbol].Invested or not symbol_data.IsReady:
continue
exit_signal = self.CalculateExitSignal(symbol_data)
if exit_signal:
self.Liquidate(symbol)
self.Debug(f"Liquidating {symbol}....")
# enter new positions
for symbol in selected_entry_symbols:
quantity = 3000 // self.Securities[symbol].Price
self.MarketOrder(symbol, quantity)
def OnSecuritiesChanged(self, changes):
'''Fires each time a security is added to our universe
handles creation of symbol data containers'''
for security in changes.AddedSecurities:
symbol = security.Symbol
if symbol not in self.symbols and symbol != self.benchmark:
self.symbols[symbol] = SymbolData(self, symbol)
for security in changes.RemovedSecurities:
symbol = security.Symbol
if symbol in self.symbols:
symbol_data = self.symbols.pop(symbol, None)
symbol_data.KillConsolidators()
def CalculateEntrySignal(self, symbol_data):
'''Is fired from within SymbolData when each respective
symbol is ready
Computes total signal'''
## ENTRY LOGIC
signal_one = self.SignalOne(symbol_data)
signal_two = self.SignalTwo(symbol_data)
signal_three = self.SignalThree(symbol_data)
signal_four = self.SignalFour(symbol_data)
# Signal One MUST be met and one of Signals 2,3,4 must be met
valid_entry_signal = signal_one and (signal_two or signal_three or signal_four)
# For logging purposes
if valid_entry_signal and not self.Portfolio[symbol_data.symbol].Invested:
self.Debug(f"{symbol_data.symbol} valid entry...signal_one:{signal_one}, signal_two:{signal_two}, signal_three:{signal_three} " \
+ f", signal_four: {signal_four}")
return valid_entry_signal
def SignalOne(self, symbol_data):
'''All 3 currently trending up (slow < moderate < fast)'''
psar_slow = symbol_data.psar_slow.Current.Value
psar_moderate = symbol_data.psar_moderate.Current.Value
psar_fast = symbol_data.psar_fast.Current.Value
return psar_slow < psar_moderate and psar_moderate < psar_fast
def SignalTwo(self, symbol_data):
'''One of the following:
a. All 3 indicators equal on the previous bar (slow == moderate == fast)
b. All 3 indicators within .02 of each other on the previous bar (slow + 0.02/0.01 == moderate/fast)
c. Slow from 2 bars ago is > open'''
# Condition A
# All 3 indicators equal on the previous bar (slow == moderate == fast)
previous_psar_slow = symbol_data.psar_slow_window[1].Value
previous_psar_moderate = symbol_data.psar_moderate_window[1].Value
previous_psar_fast = symbol_data.psar_fast_window[1].Value
condition_a = previous_psar_slow == previous_psar_moderate and \
previous_psar_slow == previous_psar_fast
# Condition B
# All 3 indicators within .02 of each other on the previous bar (slow + 0.02/0.01 == moderate/fast)
max_distance = 0.02
condition_b = abs(previous_psar_slow - previous_psar_moderate) < max_distance and \
abs(previous_psar_slow - previous_psar_fast) < max_distance and \
abs(previous_psar_fast - previous_psar_moderate) < max_distance
# Condition C
# Slow from 2 bars ago is > open
two_bars_ago_psar_slow = symbol_data.psar_slow_window[2].Value
current_bar_open = symbol_data.bar_window[0].Open
condition_c = two_bars_ago_psar_slow > current_bar_open
return condition_a or condition_b or condition_c
def SignalThree(self, symbol_data):
'''Slow/moderate/fast < open'''
psar_slow = symbol_data.psar_slow.Current.Value
psar_moderate = symbol_data.psar_moderate.Current.Value
psar_fast = symbol_data.psar_fast.Current.Value
current_bar_open = symbol_data.bar_window[0].Open
return psar_slow < current_bar_open and \
psar_moderate < current_bar_open and \
psar_fast < current_bar_open
def SignalFour(self, symbol_data):
'''Up to 2 of the previous bars can be equal or within 0.02 of each other.
Otherwise not valid (example: equal | equal |trending == valid. Equal | equal |
equal | trending == false)'''
max_distance = 0.02
current_psar_slow = symbol_data.psar_slow_window[0].Value
current_psar_moderate = symbol_data.psar_moderate_window[0].Value
current_psar_fast = symbol_data.psar_fast_window[0].Value
current_bar = abs(current_psar_slow - current_psar_moderate) < max_distance and \
abs(current_psar_slow - current_psar_fast) < max_distance and \
abs(current_psar_fast - current_psar_moderate) < max_distance
previous_psar_slow = symbol_data.psar_slow_window[1].Value
previous_psar_moderate = symbol_data.psar_moderate_window[1].Value
previous_psar_fast = symbol_data.psar_fast_window[1].Value
previous_bar = abs(previous_psar_slow - previous_psar_moderate) < max_distance and \
abs(previous_psar_slow - previous_psar_fast) < max_distance and \
abs(previous_psar_fast - previous_psar_moderate) < max_distance
two_bars_ago_psar_slow = symbol_data.psar_slow_window[2].Value
two_bars_ago_psar_moderate = symbol_data.psar_moderate_window[2].Value
two_bars_ago_psar_fast = symbol_data.psar_fast_window[2].Value
two_bars_ago = abs(two_bars_ago_psar_slow - two_bars_ago_psar_moderate) < max_distance and \
abs(two_bars_ago_psar_slow - two_bars_ago_psar_fast) < max_distance and \
abs(two_bars_ago_psar_fast - two_bars_ago_psar_moderate) < max_distance
not_all_met = not (two_bars_ago and previous_bar and current_bar)
two_met = (two_bars_ago and previous_bar) or (current_bar and previous_bar) or (current_bar and two_bars_ago)
return not_all_met and two_met
def CalculateExitSignal(self, symbol_data):
'''Calculates the exit signal
Slow is > current bar open
'''
current_psar_slow = symbol_data.psar_slow.Current.Value
current_bar_open = symbol_data.bar_window[0].Open
return current_psar_slow > current_bar_open
@property
def DesignatedTradingHours(self):
'''Determines whether we are within the
allowed trading time interval'''
start_hour = self.trading_start_time[0]
start_minute = self.trading_start_time[1]
end_hour = self.trading_end_time[0]
end_minute = self.trading_end_time[1]
current_hour = self.Time.hour
current_minute = self.Time.minute
if current_hour < start_hour or current_hour > end_hour:
return False
if current_hour == start_hour:
return current_minute >= start_minute
if current_hour == end_hour:
return current_minute <= end_minute
return True
def EndOfDayLiquidate(self):
'''Called at end of trading day'''
if self.Portfolio.Invested:
invested_tickers = [symbol.Value for symbol in self.Portfolio.Keys if self.Portfolio[symbol].Invested]
self.Debug(f"Invested in... {invested_tickers}...EOD Liquidate")
self.Liquidate()class SymbolData:
def __init__(self, algorithm, symbol):
self.algorithm = algorithm
self.symbol = symbol
self.consolidator = QuoteBarConsolidator(timedelta(minutes=5))
self.algorithm.SubscriptionManager.AddConsolidator(self.symbol, self.consolidator)
self.consolidator.DataConsolidated += self.OnFiveMinuteBar
self.psar_slow = ParabolicStopAndReverse(0.01, 0.01, 0.20)
self.psar_moderate = ParabolicStopAndReverse(0.01, 0.02, 0.20)
self.psar_fast = ParabolicStopAndReverse(0.01, 0.03, 0.20)
self.algorithm.RegisterIndicator(self.symbol, self.psar_slow, self.consolidator)
self.algorithm.RegisterIndicator(self.symbol, self.psar_moderate, self.consolidator)
self.algorithm.RegisterIndicator(self.symbol, self.psar_fast, self.consolidator)
self.psar_slow.Updated += self.OnPSARSlow
self.psar_moderate.Updated += self.OnPSARModerate
self.psar_fast.Updated += self.OnPSARFast
self.psar_slow_window = RollingWindow[IndicatorDataPoint](3)
self.psar_moderate_window = RollingWindow[IndicatorDataPoint](3)
self.psar_fast_window = RollingWindow[IndicatorDataPoint](3)
self.bar_window = RollingWindow[QuoteBar](5)
def OnFiveMinuteBar(self, sender, bar):
'''Fires each time there is a new five minute bar
Stores five minute bars as they arrive'''
self.bar_window.Add(bar)
def OnPSARSlow(self, sender, updated):
'''Fires each time psar_slow is updated'''
if self.psar_slow.IsReady:
self.psar_slow_window.Add(self.psar_slow.Current)
def OnPSARModerate(self, sender, updated):
'''Fires each time psar_moderate is updated'''
if self.psar_moderate.IsReady:
self.psar_moderate_window.Add(self.psar_moderate.Current)
def OnPSARFast(self, sender, updated):
'''Fires each time psar_fast is updated'''
if self.psar_fast.IsReady:
self.psar_fast_window.Add(self.psar_fast.Current)
@property
def IsReady(self):
'''Checks whether all data is ready to be used to calculate signals'''
return self.psar_slow_window.IsReady and self.psar_fast_window.IsReady and \
self.psar_moderate_window.IsReady and self.bar_window.IsReady
def KillConsolidators(self):
'''Removes data subscriptions'''
self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.consolidator)