| Overall Statistics |
|
Total Trades 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Net Profit 0% Sharpe Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio 0 Tracking Error 0 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 Lowest Capacity Asset |
#region imports
from AlgorithmImports import *
#endregion
import numpy as np
import pandas as pd
import scipy as sc
from datetime import timedelta
from QuantConnect.Data.UniverseSelection import *
from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
class Roboto(QCAlgorithm):
# bubble signal
FAST = 20 # for EMA, lower values >> higher risk, higher returns
SLOW = 50 # for EMA
MAGNITUDE = 1.50 # magnitude of the bubble
# position configuration for opening in CheckForEntries
FREE_CASH = 0.04 # adjust based on risk tolerance for FreePortfolioValuePercentage in Initialize
DYN_POSITION_SIZE = 0.50 # variable affecting dynamic position sizing for next short positions
MAX_POSITION_SIZE = 0.10 # maximum individual position size. has a big effect on total returns (more negative values >> larger returns)
MIN_BP = 0.08 # liquidate most profitable position if buying power (= MarginRemaining / PortfolioValue) is too low
OF_TOTAL_DV = 0.02 # max share of daily dollar volume for order size
MAX_POS = 3 # max number of open positions
USE_BULL = False
# position configuration for liquidation in CheckForExits
CUT_LOSS = -0.10 # -10% = -0.10 !!!
TCL_GET_EVEN = 0.50 # how fast is TCL trailing until break even (0.0 for none) !!!
TCL_TRAIL = 0.30 # how fast is TCL trailing after break even (0.0 for none, if larger than TCL_GET_EVEN, overrides it) !!!
TAKE_PROFIT = 0.3 # 55% = 0.55 !!!
MAX_POSITION_AGE = 40 # 45 days optimal
TP_TRAIL = 0.25 # decreases TP with age up to 0.55 * (1 - 0.5) at MAX_POSITION_AGE (0.0 for none) !!!
TP_KICK_IN = 0.5 # decreases TP with age kicking in at 80% of MAX_POSITION_AGE (never 1.0)
# liquidity configuration
MIN_Price = 50. # min price !!!
MAX_Price = 200. # max price !!!
MIN_VOLUME = 2e6 # min volume !!!
MIN_DOLLAR_VOLUME = 2e5 # min dollar volume
#MIN_TIME_OF_HISTORY = 0 # only include if there is a min of x days of history data (currently unused)
MIN_TIME_IN_UNIVERSE = SLOW # min amount of time a security must remain in the universe before being removed (drives the speed of the backtest)
# funnel
N_COARSE = MAX_POS # max number of coarse securities
# portfolio configuration
STARTING_CASH = 25000 # for backtest in Initialize
# debugging level
#MSGS = ['main', 'filter', 'logic', 'order', 'debug']
MSGS = []
class SecurityData:
# access yesterday's close via self.universe[Symbol].close
def __init__(self, symbol, history):
self.symbol = symbol
self.close = 0
self.ratio = 0
self.isAntiBubble = False
self.fast = ExponentialMovingAverage(Roboto.FAST)
self.slow = ExponentialMovingAverage(Roboto.SLOW)
self.vol = ExponentialMovingAverage(Roboto.SLOW)
# update all but the last day, as this will be updated after adding a new obj
for bar in history[:history.size-1].itertuples():
self.fast.Update(bar.Index[1], bar.close)
self.slow.Update(bar.Index[1], bar.close)
self.vol.Update(bar.Index[1], ((bar.open + bar.close)/2.0) * bar.volume) # we need to init with DollarVolume
def update(self, time, price, volume, magnitude):
self.close = price
self.ratio = 0
self.isAntiBubble = False
if self.fast.Update(time, price) and self.slow.Update(time, price) and self.vol.Update(time, volume):
self.ratio = self.fast.Current.Value / self.slow.Current.Value
self.isAntiBubble = (self.ratio < magnitude) and (price / self.slow.Current.Value < magnitude)
def Initialize(self):
self.Debug("*** Roboto is initializing ***")
self.SetTimeZone("America/New_York")
# backtest
self.SetBrokerageModel(BrokerageName.AlphaStreams)
self.SetStartDate(2018, 1, 1)
#self.SetEndDate(2021,1,5)
#self.AddRiskManagement(TrailingStopRiskManagementModel(0.05))
# live settings
if self.LiveMode:
self.minutes = 15
res = Resolution.Minute
else:
self.minutes = 60
res = Resolution.Hour
# portfolio
self.SetCash(Roboto.STARTING_CASH)
self.Settings.FreePortfolioValuePercentage = Roboto.FREE_CASH
self.min_dollar_vol = Roboto.MIN_DOLLAR_VOLUME
# universe selection
self.UniverseSettings.Resolution = res
self.UniverseSettings.MinimumTimeInUniverse = Roboto.MIN_TIME_IN_UNIVERSE # min amount of time a security must remain in the universe before being removed
# self.AddUniverse(self.CoarseFilter)
self.AddUniverse(self.CoarseFilter, self.Fine)
self.__numberOfSymbols = 1000
self.__numberOfSymbolsFine = 3
self.universe = {} # contains all tracked securities in the universe
self.open = [] # positions to open based on signal
# set security symbols
self.market = self.AddEquity("SPY", res).Symbol
self.bull = self.AddEquity("QQQ", res).Symbol
self.excl_smbls = [self.market, self.bull]
self.magnitude = Roboto.MAGNITUDE
# further vars
self.expiry = {} # contains age of position
self.trail_cut_loss = {} # contains trailing max of unrealized profit pct for cut loss
self.bp = 1.0 # buying power
# schedule our CheckForExits check for liquidation of positions using range(start, stop, step), NYSE 9:30 .. 16:00
for i in range(0, 389, self.minutes):
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.bull, i),
self.CheckForExits)
# schedule our CheckForEntries check for shorting and entering bull security
for i in range(60, 389, 60):
self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.bull, i),
self.CheckForEntries)
self.spy = self.AddEquity('SPY')
self.AddAlpha(MomentumAlphaModel(lookback=410, resolution=Resolution.Daily))
self.AddRiskManagement(RiskModelWithSpy(self.spy))
def CustomFilter(self,coarse):
cf_selected = [x for x in coarse
if x.Symbol.Value == "RRC"
]
for cf in cf_selected:
if (cf.Symbol not in self.universe):
history = self.History(cf.Symbol, Roboto.SLOW, Resolution.Daily)
self.universe[cf.Symbol] = Roboto.SecurityData(cf.Symbol, history)
self.universe[cf.Symbol].update(cf.EndTime, cf.AdjustedPrice, cf.DollarVolume, self.magnitude)
values = [x for x in self.universe.values()]
symbols = [x.symbol for x in values[:Roboto.N_COARSE]]
return symbols
def CoarseFilter(self, coarse):
if ('main' in Roboto.MSGS): self.Debug("{} CoarseFilter".format(self.Time))
# ensure a minimum dollar volume corresponding to orders of 2 x our maximum fill rate
self.min_dollar_vol = max(Roboto.MIN_DOLLAR_VOLUME,
(self.Portfolio.TotalPortfolioValue * (1-Roboto.FREE_CASH) * abs(Roboto.MAX_POSITION_SIZE)) / (2. * Roboto.OF_TOTAL_DV))
# 1st filter for hard and soft criteria
cf_selected = [x for x in coarse
if x.Market == "usa"
and x.HasFundamentalData
and x.Volume > Roboto.MIN_VOLUME
and x.DollarVolume > self.min_dollar_vol
and float(x.Price) >= Roboto.MIN_Price
and float(x.Price) <= Roboto.MAX_Price
]
if 'filter' in Roboto.MSGS: self.Debug("{} CoarseFilter-1 len:{}".format(self.Time, len(cf_selected))) # approx 500 securities
# collect symbols which are new to our universe
new_universe = {}
for cf in cf_selected:
# for every new symbol, create an entry in our universe with a SecurityData object (including initial population of our indicators with daily history)
if (cf.Symbol not in self.universe):
history = self.History(cf.Symbol, Roboto.SLOW, Resolution.Daily)
self.universe[cf.Symbol] = Roboto.SecurityData(cf.Symbol, history)
# for our complete universe, update our indicators with the data of the last trading day
self.universe[cf.Symbol].update(cf.EndTime, cf.AdjustedPrice, cf.DollarVolume, self.magnitude)
# for all cf_selected securities (and not the dropped ones), based on our newly created or updated universe entries,
new_universe[cf.Symbol] = self.universe[cf.Symbol]
self.universe = new_universe
# 2nd filter the values of our SecurityData dict to those who are over their bubble
values = [x for x in self.universe.values() if x.isAntiBubble]
if 'filter' in Roboto.MSGS: self.Debug("{} CoarseFilter-2 len:{}".format(self.Time, len(values)))
# 3rd filter for n_coarse sorted by the highest ratio
values.sort(key = lambda x: x.ratio, reverse = False) # highest ratios first
# we need to return only our array of Symbol objects
symbols = [x.symbol for x in values[:Roboto.N_COARSE]]
if 'filter' in Roboto.MSGS: self.Debug("{} CoarseFilter-3 len:{}".format(self.Time, len(symbols)))
#return symbols
# return self.coarselist
def Fine(self, fine):
tech_securities = [f for f in fine if f.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.Technology and
f.OperationRatios.ROA.ThreeMonths]
for security in tech_securities:
# we use deques instead of RWs since deques are picklable
symbol = security.Symbol
if symbol not in self.tech_ROA:
# 3 years * 4 quarters = 12 quarters of data
self.tech_ROA[symbol] = deque(maxlen=12)
self.tech_ROA[symbol].append(security.OperationRatios.ROA.ThreeMonths)
if self.LiveMode:
# this ensures we don't lose new data from an algorithm outage
self.SaveData()
# we want to rebalance in the fourth month after the (fiscal) year ends
# so that we have the most recent quarter's data
if self.Time.month != 4 or (self.quarters < 12 and not self.LiveMode):
return Universe.Unchanged
# make sure our stocks has these fundamentals
tech_securities = [x for x in tech_securities if x.OperationRatios.ROA.OneYear and
x.FinancialStatements.CashFlowStatement.OperatingCashFlow.TwelveMonths and
x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths and
x.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths and
x.FinancialStatements.CashFlowStatement.CapExReported.TwelveMonths and
x.FinancialStatements.IncomeStatement.SellingGeneralAndAdministration.TwelveMonths and
x.MarketCap]
# compute the variance of the ROA for each tech stock
tech_VARROA = {symbol:stat.variance(ROA) for symbol, ROA in self.tech_ROA.items() if len(ROA) == ROA.maxlen}
if len(tech_VARROA) < 2:
return Universe.Unchanged
tech_VARROA_median = stat.median(tech_VARROA.values())
# we will now map tech Symbols to various fundamental ratios,
# and compute the median for each ratio
# ROA 1-year
tech_ROA1Y = {x.Symbol:x.OperationRatios.ROA.OneYear for x in tech_securities}
tech_ROA1Y_median = stat.median(tech_ROA1Y.values())
# Cash Flow ROA
tech_CFROA = {x.Symbol: (
x.FinancialStatements.CashFlowStatement.OperatingCashFlow.TwelveMonths
/ x.FinancialStatements.BalanceSheet.TotalAssets.TwelveMonths
) for x in tech_securities}
tech_CFROA_median = stat.median(tech_CFROA.values())
# R&D to MktCap
tech_RD2MktCap = {x.Symbol: (
x.FinancialStatements.IncomeStatement.ResearchAndDevelopment.TwelveMonths / x.MarketCap
) for x in tech_securities}
tech_RD2MktCap_median = stat.median(tech_RD2MktCap.values())
# CapEx to MktCap
tech_CaPex2MktCap = {x.Symbol: (
x.FinancialStatements.CashFlowStatement.CapExReported.TwelveMonths / x.MarketCap
) for x in tech_securities}
tech_CaPex2MktCap_median = stat.median(tech_CaPex2MktCap.values())
# Advertising to MktCap
tech_Ad2MktCap = {x.Symbol: (
x.FinancialStatements.IncomeStatement.SellingGeneralAndAdministration.TwelveMonths / x.MarketCap
) for x in tech_securities}
tech_Ad2MktCap_median = stat.median(tech_Ad2MktCap.values())
# sort fine by book-to-market ratio, get lower quintile
has_book = [f for f in fine if f.FinancialStatements.BalanceSheet.NetTangibleAssets.TwelveMonths and f.MarketCap]
sorted_by_BM = sorted(has_book, key=lambda x: x.FinancialStatements.BalanceSheet.NetTangibleAssets.TwelveMonths / x.MarketCap)[:len(has_book)//4]
# choose tech stocks from lower quintile
tech_symbols = [f.Symbol for f in sorted_by_BM if f in tech_securities]
ratioDicts_medians = [(tech_ROA1Y, tech_ROA1Y_median),
(tech_CFROA, tech_CFROA_median), (tech_RD2MktCap, tech_RD2MktCap_median),
(tech_CaPex2MktCap, tech_CaPex2MktCap_median), (tech_Ad2MktCap, tech_Ad2MktCap_median)]
def compute_g_score(symbol):
g_score = 0
if tech_CFROA[symbol] > tech_ROA1Y[symbol]:
g_score += 1
if symbol in tech_VARROA and tech_VARROA[symbol] < tech_VARROA_median:
g_score += 1
for ratio_dict, median in ratioDicts_medians:
if symbol in ratio_dict and ratio_dict[symbol] > median:
g_score += 1
return g_score
# compute g-scores for each symbol
g_scores = {symbol:compute_g_score(symbol) for symbol in tech_symbols}
return [symbol for symbol, g_score in g_scores.items() if g_score >= 5]
#return symbols
def OnSecuritiesChanged(self, changes):
# is called whenever the universe changes
if 'main' in Roboto.MSGS: self.Debug("{} Securities changed".format(self.Time))
# remember all changed securities so they can be opened, and cancel all their orders
self.open = []
for security in changes.AddedSecurities:
self.CancelAllOrders(security.Symbol)
if not security.Invested and (security.Symbol not in self.excl_smbls):
if 'logic' in Roboto.MSGS: self.Debug("{} Identified bubble for security {}".format(self.Time, security.Symbol))
self.open.append(security.Symbol)
def CheckForEntries(self):
# once per day, check for entering new short positions for added securities from UniverseSelection
if 'main' in Roboto.MSGS: self.Debug("{} CheckForEntries".format(self.Time))
num_pos = len([f.Key for f in self.ActiveSecurities if f.Value.Invested]) # positions incl. bullish stock
# open new positions based on self.open which is populated in OnSecuritiesChanged
new_pos=0
for symb in self.open:
if (num_pos+new_pos) < Roboto.MAX_POS:
new_pos += 1
dynamic = Roboto.DYN_POSITION_SIZE/(num_pos + new_pos) # negtive
target = max(Roboto.MAX_POSITION_SIZE, dynamic) # max of negative = min of positive
tag = "New pos. target allocation {}".format(round(target, 4))
self.Buy(symb, target, tag)
self.open.remove(symb)
# set some portion of portfolio to hold bullish index
if Roboto.USE_BULL:
remaining_allocation = max(1.0 - self.MIN_BP - (num_pos * (-1 * Roboto.MAX_POSITION_SIZE)), Roboto.MAX_POSITION_SIZE)
if 'order' in Roboto.MSGS: self.Debug("{} *** Entering: bull security with {}".format(self.Time, remaining_allocation))
self.SetHoldings([PortfolioTarget(self.bull, remaining_allocation)])
self.Plot("Buying Power", "Bull", remaining_allocation)
self.bp = self.Portfolio.MarginRemaining/self.Portfolio.TotalPortfolioValue
self.Plot("Buying Power", "BP", self.bp)
self.Plot("# Positions", "pos", num_pos)
def Buy(self, symbol, target, tag = "No Tag Provided"):
# handle entry position sizing, target = negative
# get close of yesterday, mean close of last 30 minutes, and price of last minute
close_yesterday = self.universe[symbol].close
price = float(self.Securities[symbol].Close) # price of last bar according to res
if 'logic' in Roboto.MSGS: self.Debug("{} Short check {} @ yest:{}, price:{}".format(self.Time, symbol, close_yesterday, price))
# enter buy if price is decreasing
if price > 0:
# calc target order quantity from target percent (quantity is calculated based on current price and is adjusted for the fee model attached to that security)
q_target = self.CalculateOrderQuantity(symbol, target)
# calc maximum order quantity based on max allowed securities dollar volume
q_max = float(Roboto.OF_TOTAL_DV * self.universe[symbol].vol.Current.Value) / price
# enter buy with allowed quantity
q = int(min(q_target, q_max)) # max of negative = min of positive
if q > 0:
if 'order' in Roboto.MSGS: self.Debug("{} *** Entering: buy for {} @ {}".format(self.Time, q, symbol, price))
self.EmitInsights(Insight.Price(symbol, timedelta(days = Roboto.MAX_POSITION_AGE), InsightDirection.Up, None, None, None, target))
self.LimitOrder(symbol, q, price, tag)
else:
if q != 0:
if 'error' in Roboto.MSGS: self.Error("{} Received negative quantity for buy order: {} {} @ {} (Target: {})".format(self.Time, q, symbol, price, target))
else:
if 'logic' in Roboto.MSGS: self.Debug("{} Buying skipped for {} @ {}".format(self.Time, symbol, price))
def CheckForExits(self):
# every OnDate event, check for liquidation of portfolio positions based on loss, profit, age, and buying power
if 'main' in Roboto.MSGS: self.Debug("{} CheckForExits".format(self.Time))
closing = set()
invested = [f.Key for f in self.ActiveSecurities if (f.Value.Invested and (f.Value.Symbol not in self.excl_smbls))]
# liquidate loss positions or old positions or take profit
for symb in invested:
holding = self.Portfolio[symb]
# update cut loss, limited to UnrealizedProfitPercent = 0
self.trail_cut_loss[symb] = min(-Roboto.CUT_LOSS, max(self.trail_cut_loss[symb], holding.UnrealizedProfitPercent * Roboto.TCL_GET_EVEN))
# update cut loss, not limited
self.trail_cut_loss[symb] = max(self.trail_cut_loss[symb], holding.UnrealizedProfitPercent * Roboto.TCL_TRAIL)
take_profit = Roboto.TAKE_PROFIT
# update trailing profit decrease, kicking in at 70% of the days, decreasing up to Roboto.TP_TRAIL
tp_decrease = 1 - max(0, ( (1+(self.Time - self.expiry[symb]).days) - Roboto.MAX_POSITION_AGE*Roboto.TP_KICK_IN) / (Roboto.MAX_POSITION_AGE - Roboto.MAX_POSITION_AGE*Roboto.TP_KICK_IN)) * Roboto.TP_TRAIL
# exit positions with a large loss quickly with a market order
if (holding.UnrealizedProfitPercent < (self.trail_cut_loss[symb] + Roboto.CUT_LOSS)) and ((self.Time - self.expiry[symb]).days > 1):
if 'order' in Roboto.MSGS: self.Debug("{} *** Liquidating: Market Order for Cutting Losses on {} at {} days, {}%".format(self.Time, holding.Symbol, (self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100))
self.CancelAllOrders(holding.Symbol)
tag = "Cutting loss, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
self.MarketOrderExit(holding.Symbol, tag)
closing.add(holding.Symbol)
# exit positions that have a large profit with a limit order
elif (holding.UnrealizedProfitPercent > take_profit * tp_decrease):
if 'order' in Roboto.MSGS: self.Debug("{} *** Liquidating: Limit Order for Taking Profit on {} at {} days, {}%".format(self.Time, holding.Symbol, (self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100))
self.CancelAllOrders(holding.Symbol)
tag = "Taking profit, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
self.LimitOrderExit(holding.Symbol, tag)
closing.add(holding.Symbol)
# exit old positions with a limit order
elif (self.Time - self.expiry[holding.Symbol]).days > Roboto.MAX_POSITION_AGE:
if 'order' in Roboto.MSGS: self.Debug("{} *** Liquidating: Limit Order for Expired {} at {} days, {}%".format(self.Time, holding.Symbol, (self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100))
self.CancelAllOrders(holding.Symbol)
tag = "Expired, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
#self.MarketOrderExit(holding.Symbol, tag)
self.LimitOrderExit(holding.Symbol, tag)
closing.add(holding.Symbol)
# liquidate most profitable position if buying power is too low
self.bp = self.Portfolio.MarginRemaining / self.Portfolio.TotalPortfolioValue
if self.bp < Roboto.MIN_BP:
if 'logic' in Roboto.MSGS: self.Debug("{} Buying Power too low: {}".format(self.Time, self.bp))
class Factor:
def __init__(self, holding):
self.holding = holding
self.unrealized = self.holding.UnrealizedProfitPercent
track = {}
for symb in invested:
holding = self.Portfolio[symb]
track[holding.Symbol] = Factor(holding)
values = list(set(track.values()) - set(closing)) # remove any symbols already beeing closed above (loss positions or old positions or take profit)
if len(values) > 0:
values.sort(key=lambda f: f.unrealized, reverse=True)
if 'order' in Roboto.MSGS: self.Debug("{} *** Liquidating: Limit Order for Buying Power {} @ {}".format(self.Time, values[0].holding.Symbol, values[0].unrealized))
self.CancelAllOrders(values[0].holding.Symbol)
tag = "Liquidating, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
self.LimitOrderExit(values[0].holding.Symbol, tag)
else:
if 'error' in Roboto.MSGS: self.Error("{} Unable to liquidate: {} {}".format(self.Time, len(values), len(closing)))
def MarketOrderExit(self, symbol, tag = "No Tag Provided"):
q = int(self.Portfolio[symbol].Quantity)
if 'debug' in Roboto.MSGS: self.Debug("{} Rapid Exit {} {}".format(self.Time, q, symbol))
self.EmitInsights(Insight.Price(symbol, timedelta(days = Roboto.MAX_POSITION_AGE), InsightDirection.Down, None, None, None, 0.00))
self.MarketOrder(symbol, -q, False, tag)
def LimitOrderExit(self, symbol, tag = "No Tag Provided"):
q = int(self.Portfolio[symbol].Quantity)
price = self.Securities[symbol].Close
if 'debug' in Roboto.MSGS: self.Debug("{} LimitOrderExit {} {} @ {}".format(self.Time, q, symbol, price))
self.EmitInsights(Insight.Price(symbol, timedelta(days = Roboto.MAX_POSITION_AGE), InsightDirection.Flat, None, None, None, 0.00))
self.LimitOrder(symbol, -q, price, tag)
def CancelAllOrders(self, symbol):
if 'debug' in Roboto.MSGS: self.Debug("{} Cancelling all orders for {}".format(self.Time, symbol))
openOrders = self.Transactions.CancelOpenOrders(symbol)
for oo in openOrders:
if not (oo.Status == OrderStatus.CancelPending):
r = oo.Cancel()
if not r.IsSuccess:
if 'error' in Roboto.MSGS: self.Error("{} Failed to cancel open order {} of {} for reason: {}, {}".format(self.Time, oo.Quantity, oo.Symbol, r.ErrorMessage, r.ErrorCode))
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
order = self.Transactions.GetOrderById(orderEvent.OrderId)
if 'debug' in Roboto.MSGS: self.Debug("{} Filled {} of {} at {}".format(self.Time, order.Quantity, order.Symbol, order.Price))
# if completely liquidating position, stop tracking position age
if not self.Portfolio[order.Symbol].Invested:
try:
del self.expiry[order.Symbol]
del self.trail_cut_loss[order.Symbol]
if 'debug' in Roboto.MSGS: self.Debug("{} No longer tracking {}".format(self.Time, order.Symbol))
except Error:
if 'error' in Roboto.MSGS: self.Error("{} Key deletion failed for {}".format(self.Time, order.Symbol))
# if position is completely new, start tracking position age
else:
if (order.Symbol not in self.expiry):
self.expiry[order.Symbol] = self.Time
else:
if 'error' in Roboto.MSGS: self.Error("{} Key already existed for {}".format(self.Time, order.Symbol))
if (order.Symbol not in self.trail_cut_loss):
self.trail_cut_loss[order.Symbol] = 0
else:
if 'error' in Roboto.MSGS: self.Error("{} Key already existed for {}".format(self.Time, order.Symbol))
class RiskModelWithSpy(RiskManagementModel):
def __init__(self, spy, lookback = 200, resolution = Resolution.Daily):
self.spy = spy
self.lookback = lookback
self.resolution = resolution
self.symboldata = {}
#Flag so we only instanciate it once
self.init = False
def ManageRisk(self, algorithm, targets):
targets = []
if self.init == False:
#Takes in spy, our lookback and resolution
self.symboldata[self.spy.Symbol] = EMASymbolData(algorithm, self.spy, self.lookback, self.resolution)
self.init = True
for symbol, symboldata in self.symboldata.items():
#logic. If price is below the current value for EMA, we send a portfoliotarget of 0
spyValue = self.spy.Price
AlmaValue = symboldata.EMA.Current.Value
for kvp in algorithm.ActiveSecurities:
security = kvp.Value
if not security.Invested:
continue
if spyValue <= AlmaValue:
targets.append(PortfolioTarget(security.Symbol, 0))
return targets
class MomentumAlphaModel(AlphaModel):
def __init__(self, lookback = 200, resolution = Resolution.Daily):
self.lookback = lookback
self.resolution = resolution
self.predictionInterval = Expiry.EndOfMonth
self.symbolDataBySymbol = {}
self.num_insights = 10
self.lastMonth = -1
def Update(self, algorithm, data):
if algorithm.Time.month == self.lastMonth:
return []
self.lastMonth = algorithm.Time.month
insights = []
for symbol, symbolData in self.symbolDataBySymbol.items():
if symbolData.CanEmit:
direction = InsightDirection.Flat
magnitude = symbolData.Return
if magnitude > 0:
direction = InsightDirection.Up
if magnitude < 0:
continue
insights.append(Insight.Price(symbol, self.predictionInterval, direction, magnitude, None))
insights1 = sorted([x for x in insights], key = lambda x: x.Magnitude, reverse=True)
return [x for x in insights1[:self.num_insights]]
def OnSecuritiesChanged(self, algorithm, changes):
# clean up data for removed securities
for removed in changes.RemovedSecurities:
symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
if symbolData is not None:
symbolData.RemoveConsolidators(algorithm)
# initialize data for added securities
symbols = [ x.Symbol for x in changes.AddedSecurities ]
history = algorithm.History(symbols, self.lookback, self.resolution)
if history.empty: return
tickers = history.index.levels[0]
for ticker in tickers:
symbol = SymbolCache.GetSymbol(ticker)
if symbol not in self.symbolDataBySymbol:
symbolData = SymbolData(symbol, self.lookback)
self.symbolDataBySymbol[symbol] = symbolData
symbolData.RegisterIndicators(algorithm, self.resolution)
symbolData.WarmUpIndicators(history.loc[ticker])
class EMASymbolData:
def __init__(self, algorithm, security, lookback, resolution):
symbol = security.Symbol
self.Security = symbol
self.Consolidator = algorithm.ResolveConsolidator(symbol, resolution)
smaName = algorithm.CreateIndicatorName(symbol, f"ALMA{lookback}", resolution)
self.EMA = ExponentialMovingAverage(smaName, lookback)
algorithm.RegisterIndicator(symbol, self.EMA, self.Consolidator)
history = algorithm.History(symbol, lookback, resolution)
if 'close' in history:
history = history.close.unstack(0).squeeze()
for time, value in history.iteritems():
self.EMA.Update(time, value)
class FScore(object):
def __init__(self, netincome, operating_cashflow, roa_current,
roa_past, issued_current, issued_past, grossm_current, grossm_past,
longterm_current, longterm_past, curratio_current, curratio_past,
assetturn_current, assetturn_past):
self.netincome = netincome
self.operating_cashflow = operating_cashflow
self.roa_current = roa_current
self.roa_past = roa_past
self.issued_current = issued_current
self.issued_past = issued_past
self.grossm_current = grossm_current
self.grossm_past = grossm_past
self.longterm_current = longterm_current
self.longterm_past = longterm_past
self.curratio_current = curratio_current
self.curratio_past = curratio_past
self.assetturn_current = assetturn_current
self.assetturn_past = assetturn_past
def ObjectiveScore(self):
fscore = 0
fscore += np.where(self.netincome > 0, 1, 0)
fscore += np.where(self.operating_cashflow > 0, 1, 0)
fscore += np.where(self.roa_current > self.roa_past, 1, 0)
fscore += np.where(self.operating_cashflow > self.roa_current, 1, 0)
fscore += np.where(self.longterm_current <= self.longterm_past, 1, 0)
fscore += np.where(self.curratio_current >= self.curratio_past, 1, 0)
fscore += np.where(self.issued_current <= self.issued_past, 1, 0)
fscore += np.where(self.grossm_current >= self.grossm_past, 1, 0)
fscore += np.where(self.assetturn_current >= self.assetturn_past, 1, 0)
return fscore
class SymbolData:
def __init__(self, symbol, lookback):
self.Symbol = symbol
self.ROC = RateOfChange('{}.ROC({})'.format(symbol, lookback), lookback)
self.Consolidator = None
self.previous = 0
def RegisterIndicators(self, algorithm, resolution):
self.Consolidator = algorithm.ResolveConsolidator(self.Symbol, resolution)
algorithm.RegisterIndicator(self.Symbol, self.ROC, self.Consolidator)
def RemoveConsolidators(self, algorithm):
if self.Consolidator is not None:
algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.Consolidator)
def WarmUpIndicators(self, history):
for tuple in history.itertuples():
self.ROC.Update(tuple.Index, tuple.close)
@property
def Return(self):
return float(self.ROC.Current.Value)
@property
def CanEmit(self):
if self.previous == self.ROC.Samples:
return False
self.previous = self.ROC.Samples
return self.ROC.IsReady
def __str__(self, **kwargs):
return '{}: {:.2%}'.format(self.ROC.Name, (1 + self.Return)**252 - 1)