| Overall Statistics |
|
Total Orders 18 Average Win 24.28% Average Loss -9.62% Compounding Annual Return 89.347% Drawdown 33.100% Expectancy 1.349 Start Equity 100000 End Equity 260605.91 Net Profit 160.606% Sharpe Ratio 1.918 Sortino Ratio 2.099 Probabilistic Sharpe Ratio 83.536% Loss Rate 33% Win Rate 67% Profit-Loss Ratio 2.52 Alpha -0.004 Beta 0.484 Annual Standard Deviation 0.298 Annual Variance 0.089 Information Ratio -2.007 Tracking Error 0.307 Treynor Ratio 1.181 Total Fees $0.00 Estimated Strategy Capacity $21000000.00 Lowest Capacity Asset TQQQ UK280CGTCB51 Portfolio Turnover 3.26% |
#region imports
from AlgorithmImports import *
from market_surge_stochastic import MarketSurgeStochastic
from dateutil.relativedelta import relativedelta
#endregion
# consolicator example: https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/BasicTemplateFuturesConsolidationAlgorithm.py
class ValueAlphaModel(AlphaModel):
def __init__(self, stoch_k = 0, stoch_d = 0):
'''Initializes a new instance of the ValueAlphaModel class
Args:
_securities: list of securities received from Universe Selection
_stoch_k: the last _stoch_k from the stochastics indicator
_stoch_d: the last _stoch_d from the stochastics indicator'''
self._stoch_k = 0
self._stoch_d = 0
self._securities = []
# Parameters for rules
self._ticker = "TQQQ"
self._over_sold = 12
self._over_bought = 65
# PD stop (per day?) percent.
self._one_day_drop = 12.9
# Local status
self.move_above_delay = False
self.cross_under_delay = False
self.can_rebuy = False
# when resolution is daily, update() enters every day at 16:00, no matter whether it uses a weekly consolidator.
# when resolution is hour, update() enters every day at [10, 16:00], no matter whether it uses a weekly consolidator.
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
# algorithm.debug(" (david) update() enters: " + str(algorithm.time))
# https://www.quantconnect.com/docs/v2/writing-algorithms/historical-data/warm-up-periods#03-Warm-Up-Vs-Reality
# https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/automatic-indicators#08-Warm-Up-Indicators
if algorithm.is_warming_up:
raise Exception("algorithm should have warmed up, but is_warming_up is actually false.")
if not self._ms_sto.is_ready:
raise Exception(f"{self._ms_sto.name} should have been ready, but it wasn't. The indicator received {self._ms_sto.samples} samples.")
# It shows daily chart, because update() enters daily.
# algorithm.plot("MarketSurgeStochastic", "fast_stoch", self._ms_sto.fast_stoch)
algorithm.plot("MarketSurgeStochastic", "stoch_k", self._ms_sto.stoch_k)
algorithm.plot("MarketSurgeStochastic", "stoch_d", self._ms_sto.stoch_d)
insights = []
# For corporate actions, returns directly.
# https://www.quantconnect.com/docs/v2/writing-algorithms/securities/asset-classes/us-equity/corporate-actions
if data[self._ticker] == None:
return insights
# Reset cross_under_delay and move_above_delay
if self._ms_sto.stoch_k >= self._ms_sto.stoch_d:
# because self._ms_sto.stoch_k >= self._ms_sto.stoch_d, so it's not cross under.
self.cross_under_delay = False
else:
# because self._ms_sto.stoch_k < self._ms_sto.stoch_d, so it's not move above.
self.move_above_delay = False
# Buy Rule #1: Buy if the weekly fast line crosses above the slow line EOD, but only if the fast line is not below the Over Sold Line* (a variable). If it is below, wait to purchase.
# The purchase will be on a Friday. If Friday is a holiday, purchase EOD Thursday. -- this cannot happen on Friday, because _ms_sto only updates on Monday.
if (not algorithm.portfolio[self._ticker].invested) and self._ms_sto.stoch_k >= self._ms_sto.stoch_d and (self._stoch_k < self._stoch_d or self.move_above_delay):
if self._ms_sto.stoch_k >= self._over_sold:
algorithm.debug(f" (david) update() {algorithm.time}: Buy Rule #1 triggers, create UP insights, self._ms_sto.stoch_k = {self._ms_sto.stoch_k}, self._ms_sto.stoch_d = {self._ms_sto.stoch_d}")
# https://www.quantconnect.com/forum/discussion/16788/how-can-construct-portfolio-properly-without-re-balancing-if-there-wasn-039-t-signal-to-change-direction/p1/comment-48027
# insights.append(Insight.price(self._ticker, Expiry.ONE_YEAR, InsightDirection.UP))
insights.append(Insight.price("TQQQ", datetime.now() + relativedelta(years=100), InsightDirection.UP))
self.move_above_delay = False
else:
# Wait until self._ms_sto.stoch_k moves above oversold
self.move_above_delay = True
# Re-buy rule: If you sell because of Sell Rules 2, 3, or 4, and you verify both Buy conditions are still true
# 1. Fast Line is higher than the Slow Line
# 2. Fast Line is above the Oversold Line
# Re-Buy the next day at or near EOD for both the Daily and Weekly Strategies.
# It's exactly on the next day.
if self.can_rebuy and algorithm.time.date() == (self.sell_time + relativedelta(days=1)).date() and self._ms_sto.stoch_k >= self._ms_sto.stoch_d and self._ms_sto.stoch_k >= self._over_sold:
algorithm.debug(f" (david) update() {algorithm.time}: Re-buy Rule triggers, create UP insights, self._ms_sto.stoch_k = {self._ms_sto.stoch_k}, self._ms_sto.stoch_d = {self._ms_sto.stoch_d}")
# https://www.quantconnect.com/forum/discussion/16788/how-can-construct-portfolio-properly-without-re-balancing-if-there-wasn-039-t-signal-to-change-direction/p1/comment-48027
# insights.append(Insight.price(self._ticker, Expiry.ONE_YEAR, InsightDirection.UP))
insights.append(Insight.price("TQQQ", datetime.now() + relativedelta(years=100), InsightDirection.UP))
self.can_rebuy = False
self.move_above_delay = False
# Sell Rule #1: Sell if the Weekly Fast Line drops below the Slow Line, but only if the Fast Line is lower than the Over Bought Line* (a variable), sell at EOD price.
# If the Fast Line is above the Over Bought Line*, wait to sell. This will be on a Friday. If Friday is a holiday, sell EOD Thursday.
if algorithm.insights.contains_key("TQQQ") and self._ms_sto.stoch_k < self._ms_sto.stoch_d and (self._stoch_k >= self._stoch_d or self.cross_under_delay):
if self._ms_sto.stoch_k < self._over_bought:
algorithm.debug(f" (david) update() {algorithm.time}: Sell Rule #1 triggers, cancel UP insights")
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/insight-manager#10-Cancel-Insights
algorithm.insights.cancel(["TQQQ"])
self.sell_time = algorithm.time
self.cross_under_delay = False
self.can_rebuy = False
else:
self.cross_under_delay = True
# Sell Rule #2a: Sell at the Open if the Open Price is Below the 1-Day Drop Percentage* (a variable) Price, based on previous EOD.
elif algorithm.insights.contains_key("TQQQ") and data["TQQQ"].open / self._daily_close_identity.current.price < (1 - self._one_day_drop / 100):
algorithm.debug(f" (david) update() {algorithm.time}: Sell Rule #2a triggers, cancel UP insights")
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/insight-manager#10-Cancel-Insights
algorithm.insights.cancel(["TQQQ"])
self.sell_time = algorithm.time
self.can_rebuy = True
# The current resolution is hourly, so it cannot sell immediately. It sells when the hourly low is lower the threshold.
# Sell Rule #2b: Sell intraday if the intraday price is Below the 1-Day Drop Percentage* (a variable) Price, based on previous EOD.
elif algorithm.insights.contains_key("TQQQ") and (data["TQQQ"].low / self._daily_close_identity.current.price) < (1 - self._one_day_drop / 100):
algorithm.debug(f" (david) update() {algorithm.time}: Sell Rule #2b triggers, cancel UP insights")
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/insight-manager#10-Cancel-Insights
algorithm.insights.cancel(["TQQQ"])
self.sell_time = algorithm.time
self.can_rebuy = True
self._stoch_k = self._ms_sto.stoch_k
self._stoch_d = self._ms_sto.stoch_d
return insights
# on_securities_changed() enters only once, and it's before update().
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
algorithm.debug(" (david) " + str(algorithm.time) + " on_securities_changed: changes.removed_securities=" + ', '.join(map(str, changes.removed_securities)) + "; changes.added_securities=" + ', '.join(map(str, changes.added_securities)))
for security in changes.removed_securities:
if security in self._securities:
self._securities.remove(security)
self._securities.extend(changes.added_securities)
# https://www.quantconnect.com/forum/discussion/12428/how-to-compare-current-price-to-previous-day-039-s-close/p1/comment-36582
# https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/supported-indicators/identity
self._daily_close_identity = algorithm.Identity(self._ticker, Resolution.Daily, Field.Close)
algorithm.warm_up_indicator(self._ticker, self._daily_close_identity, Resolution.DAILY)
# https://www.quantconnect.com/docs/v2/writing-algorithms/consolidating-data/consolidator-types/calendar-consolidators
# self._consolidator = algorithm.consolidate(self._ticker, Calendar.WEEKLY, self._consolidation_handler)
# self._consolidator = algorithm.create_consolidator(Calendar.WEEKLY, TradeBar) # API error
self._consolidator = TradeBarConsolidator(Calendar.WEEKLY)
# https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/custom-indicators
self._ms_sto = MarketSurgeStochastic("MarketSurgeStochastic", 5, 4, 3)
# algorithm.register_indicator(self._ticker, self._ms_sto, Resolution.DAILY, self._consolidator)
algorithm.register_indicator(self._ticker, self._ms_sto, self._consolidator)
# https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/automatic-indicators#08-Warm-Up-Indicators
# This function will invoke MarketSurgeStochastic.update 5 times right away, before running assert() below.
# Make sure that there are enough history before the start date, otherwise it won't work for warmup.
# algorithm.warm_up_indicator(self._ticker, self._ms_sto, Resolution.Daily)
# By using `timedelta(weeks=1)`, it will use weekly data to warm up it {warm_up_period} times.
algorithm.warm_up_indicator(self._ticker, self._ms_sto, timedelta(weeks=1))
# samples is a field defined in the parent class PythonIndicator.
assert(self._ms_sto.samples >= 5), f"_ms_sto indicator was expected to have processed 5 datapoints already, but self._ms_sto.samples=" + str(self._ms_sto.samples)
assert(self._ms_sto.is_ready), "_ms_sto indicator was expected to be ready"
# It seems to be useless, but I have to define _consolidation_handler() for algorithm.consolidate()
# Define the consolidation handler.
def _consolidation_handler(self, consolidated_bar: TradeBar) -> None:
# it does not have algorithm, so it cannot call algorithm.log().
# self.log("_consolidation_handler enters:" + str(self.time))
# self.log(str(consolidated_bar))
pass
# region imports
from AlgorithmImports import *
from market_surge_stochastic import MarketSurgeStochastic
# from universe import LowPBRatioUniverseSelectionModel
from alpha import ValueAlphaModel
# from portfolio import EqualWeightingRebalanceOnInsightsPortfolioConstructionModel
# endregion
# 4/11/2022 Buy: i don't have this buy, becuase I'm selling on that day. why they buy on that day?
# 7/1/2022 Buy: i don't have this buy, why they buy on that day?
# 11/3/2022 Buy: i don't have this buy, why they buy on that day?
#
class BuffetBargainHunterAlgorithm(QCAlgorithm):
def initialize(self):
# self.set_start_date(2022, 12, 26) # consolidator warm up starts from 2022/11/4
# self.set_start_date(2022, 12, 27) # consolidator warm up starts from 2022/11/4,
self.set_start_date(2022, 12, 28) # consolidator warm up starts from 2022/11/7, Monday
# self.set_start_date(2022, 12, 29) # consolidator warm up starts from 2022/11/8, Tuesday
self.set_end_date(2024, 7, 23)
self.set_cash(100000)
self._ticker = "TQQQ"
self._trailing_stop = 35.5
self._profit_target = 33.6
# `add_equity` may be duplicated with universe selection.
# Default is ADJUSTED, it's preferred because it's easier to handle splits.
# https://www.quantconnect.com/docs/v2/research-environment/datasets/us-equity#07-Data-Normalization
# self._tqqq = self.add_equity(self._ticker, Resolution.DAILY, data_normalization_mode=DataNormalizationMode.RAW).symbol
# seems to be useless
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/universe-selection/universe-settings#03-Leverage
# self.universe_settings.leverage = 0
# DAILY is not enough, becuase we need to sell on the open / intraday.
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/universe-selection/universe-settings#02-Resolution
# self.universe_settings.resolution = Resolution.DAILY
self.universe_settings.resolution = Resolution.HOUR
# self.universe_settings.resolution = Resolution.MINUTE
# The default is ADJUSTED, which seems to be much different from Yahoo.
# SPLIT_ADJUSTED and TOTAL_RETURN are close to Yahoo.
# https://www.quantconnect.com/docs/v2/research-environment/datasets/us-equity#07-Data-Normalization
# https://www.quantconnect.com/docs/v2/writing-algorithms/universes/settings#07-Data-Normalization-Mode
self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
# self.universe_settings.data_normalization_mode = DataNormalizationMode.SPLIT_ADJUSTED
# self.universe_settings.data_normalization_mode = DataNormalizationMode.TOTAL_RETURN
tickers = [self._ticker]
symbols = [Symbol.create(ticker, SecurityType.EQUITY, Market.USA) for ticker in tickers]
self.add_universe_selection(ManualUniverseSelectionModel(symbols))
# https://www.quantconnect.com/docs/v2/writing-algorithms/initialization#11-Set-Benchmark
self.set_benchmark(self._ticker)
## Set trading fees to $0
# https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/transaction-fees/key-concepts#02-Set-Models
self.set_security_initializer(lambda security: security.set_fee_model(ConstantFeeModel(0)))
self.add_alpha(ValueAlphaModel())
# It does not create any orders, which is not what I want.
# self.set_portfolio_construction(NullPortfolioConstructionModel())
# EqualWeightingPortfolioConstructionModel sells when the insights is expired.
# Set `rebalance = lambda time:None`, so that there is no rebalance.
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/portfolio-construction/supported-models#03-Equal-Weighting-Model
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel(rebalance = lambda time:None))
# When using `rebalance=lambda time:None`, these settings cannot be set as False, otherwise it will not create any orders.
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/portfolio-construction/key-concepts#08-Rebalance-Settings
# Disable automatic portfolio rebalancing upon insight change, allowing for manual control over when portfolio adjustments are made based on insights.
# self.settings.rebalance_portfolio_on_insight_changes = False
# Disable automatic portfolio rebalancing upon security change, allowing for manual control over when portfolio adjustments are made based on security additions or removals.
# self.settings.rebalance_portfolio_on_security_changes = False
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/risk-management/key-concepts#03-Multi-Model-Algorithms
# https://www.quantconnect.com/docs/v2/writing-algorithms/algorithm-framework/risk-management/supported-models#07-Trailing-Stop-Model
# https://github.com/QuantConnect/Lean/blob/master/Algorithm.Framework/Risk/TrailingStopRiskManagementModel.py
# https://github.com/QuantConnect/Lean/blob/master/Algorithm.Framework/Risk/MaximumUnrealizedProfitPercentPerSecurity.py
# self.add_risk_management(NullRiskManagementModel())
self.add_risk_management(TrailingStopRiskManagementModel(maximum_drawdown_percent = self._trailing_stop / 100))
self.add_risk_management(MaximumUnrealizedProfitPercentPerSecurity(maximum_unrealized_profit_percent = self._profit_target / 100))
self.set_execution(ImmediateExecutionModel())
# on_data() enters every day at 16:00, including warm up period.
# def on_data(self, slice: Slice) -> None:
# if self._sto.is_ready:
# # The current value of self._sto is represented by self._sto.current.value
# self.plot("Stochastic", "sto", self._sto.current.value)
# # Plot all attributes of self._sto
# self.plot("Stochastic", "fast_stoch", self._sto.fast_stoch.current.value)
# self.plot("Stochastic", "stoch_k", self._sto.stoch_k.current.value)
# self.plot("Stochastic", "stoch_d", self._sto.stoch_d.current.value)
# def on_data(self, data):
# if not self.is_warming_up:
# pass
# if not self._sto.is_ready:
# pass
# if not self.insights.has_active_insights(self._tqqq, self.time):
# pass
# # The current value of self._sto is represented by self._sto.current.value
# self.plot("Stochastic", "sto", self._sto.current.value)
# # Plot all attributes of self._sto
# self.plot("Stochastic", "fast_stoch", self._sto.fast_stoch.current.value)
# self.plot("Stochastic", "stoch_k", self._sto.stoch_k.current.value)
# self.plot("Stochastic", "stoch_d", self._sto.stoch_d.current.value)# region imports
from AlgorithmImports import *
from collections import deque
# endregion
# Your New Python File
# https://www.quantconnect.com/docs/v2/writing-algorithms/indicators/custom-indicators
# https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/CustomIndicatorAlgorithm.py
# https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/CustomWarmUpPeriodIndicatorAlgorithm.py
######################################################
# Stochastic that matches MarketSmith
# declare lower;
# input period = 14;
# input k_period = 5;
# input d_period = 5;
# #Get the aggregation data from chart instead
# #input tsagg = AggregationPeriod.DAY;
# input upper = 80;
# input lower = 10;
# #def price = close(period = tsagg);
# def price = close(period = GetAggregationPeriod());
# def range = 100*(price - Lowest(price, period)) / (Highest(price, period) - Lowest(price, period));
# def k1 = 2 / (k_period + 1);
# def slow_k = RoundDown(range * k1, 0) + RoundDown(slow_k[1] * (1 - k1), 0);
# slow_k[1] means the former vaule of slow_k
# def k2 = 2 / (d_period + 1);
# def slow_d = RoundDown(slow_k * k2, 0) + RoundDown(slow_d[1] * (1 - k2), 0);
# slow_d[1] means the former value of slow_d
# plot Up = upper;
# plot Lo = lower;
# plot Fast = K;
# plot Slow = D;
######################################################
class MarketSurgeStochastic(PythonIndicator):
def __init__(self, name, period, k_period, d_period):
# name, time, value are required for each indicator.
self.name = name
self.time = datetime.min
self.value = 0
# When warm_up_period=464, it start from 2010/2/11, which is a Thursday.
self.warm_up_period = 464
self.queue = deque(maxlen=period)
self.period = period
self.k_period = k_period
self.d_period = d_period
self.fast_stoch = 0
self.stoch_k = 0
self.stoch_d = 0
def update(self, input: BaseData) -> bool:
if not isinstance(input, TradeBar):
raise TypeError('MarketSurgeStochastic.update: input must be a TradeBar')
# `input.time` seems to be at 9:30 am on Monday, but the input.high/low/close are the prices of the week when I use RAW.
self.time = input.time
self.queue.appendleft(input.close)
count = len(self.queue)
if max(self.queue) == min(self.queue):
# if there's no range, just return constant zero
self.fast_stoch = 0
else:
self.fast_stoch = 100 * (input.close - min(self.queue)) / (max(self.queue) - min(self.queue))
k1 = 2 / (self.k_period + 1)
self.stoch_k = int(self.fast_stoch * k1) + int(self.stoch_k * (1 - k1))
k2 = 2 / (self.d_period + 1)
self.stoch_d = int(self.stoch_k * k2) + int(self.stoch_d * (1 - k2))
# It does not have algorithm, so it cannot call algorithm.plot().
# algorithm.plot("MarketSurgeStochastic", "stoch_k", self.stoch_k)
# algorithm.plot("MarketSurgeStochastic", "stoch_d", self.stoch_d)
# return if it's ready to use.
return count == self.queue.maxlen
#region imports
from AlgorithmImports import *
#endregion
class EqualWeightingRebalanceOnInsightsPortfolioConstructionModel(EqualWeightingPortfolioConstructionModel):
def __init__(self, algorithm):
super().__init__()
self._algorithm = algorithm
self._new_insights = False
def is_rebalance_due(self, insights: List[Insight], algorithm_utc: datetime) -> bool:
if not self._new_insights:
self._new_insights = len(insights) > 0
is_rebalance_due = self._new_insights and not self._algorithm.is_warming_up and self._algorithm.current_slice.quote_bars.count > 0
if is_rebalance_due:
self._new_insights = False
return is_rebalance_due#region imports from AlgorithmImports import * #endregion # 05/24/2023: -Updated universe selection timing to run at the start of each month. # -Added warm-up. # -Removed the risk management model so the algorithm could warm-up properly. # -Added OnWarmupFinished to liquidate existing holdings that aren't backed by active insights. # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_210cae3651bd5aa87f79cd88b5f8109b.html # # 05/24/2023: -Updated universe selection timing so that the first trading day of each month always has the latest universe already selected. # Before this change, if the first trading day of the month was a Monday, the universe selection would run on Tuesday morning, # cancelling some of the month's insights during warm-up. # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_3806b7739d53f7b3687a948d0613cb25.html # # 05/26/2023: -Updated IsRebalanceDue function in the portfolio construction model to avoid MOO orders when deploying outside of regular trading hours # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_d15053c45241cf1803495ca83f5452c5.html # # 07/13/2023: -Fixed warm-up logic to liquidate undesired portfolio holdings on re-deployment # -Set the MinimumOrderMarginPortfolioPercentage to 0 # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_ac226bd3f017d0d0812d3b41fc25772a.html # # 10/27/2023: - Implement new Fundamental Universe Selection Model, merging coarse and fine selections # # 04/15/2023: -Updated to PEP8 style # -Added nan filter to universe selection # https://www.quantconnect.com/terminal/processCache?request=embedded_backtest_69e07892dfd0829a67f228ab5c7d7bdd.html
#region imports
from AlgorithmImports import *
#endregion
class LowPBRatioUniverseSelectionModel(FundamentalUniverseSelectionModel):
def __init__(self, universe_settings: UniverseSettings = None, coarse_size: int = 1000, fine_size: int = 250) -> None:
def select(fundamentals):
fundamentals = [f for f in fundamentals if not np.isnan(f.valuation_ratios.pb_ratio)]
sorted_by_dollar_volume = sorted(fundamentals, key=lambda c: c.dollar_volume, reverse=True)[:coarse_size]
return [c.symbol for c in sorted(sorted_by_dollar_volume, key=lambda x: x.valuation_ratios.pb_ratio)[:fine_size]]
super().__init__(None, select, universe_settings)