?“量化學習之算法篇”
即使你并無代碼的經驗,但只要您學會如何在Quantopian平臺上克隆這些極為有利可圖的算法代碼,多多練習回測和交易,就能為您帶來不小的收獲。
以下算法來自世界各地的開放作者社區提交,資金分配給了八個國家的作者,其中包括澳大利亞,加拿大,中國,哥倫比亞,印度,西班牙和美國。
這八個算法均在Medium上公布,它們分別是:
Zack’s Long-Short PEAD with News Sentiment and the Street’s Consensus (LIVE TRADING)
Zack’s Long PEAD with News Sentiment
Are Earnings Predictable with Buyback Announcements? (LIVE TRADING)
?Reversals During Earnings Announcements (LIVE TRADING)
Clenow Momentum Strategy (as Amended)
VIX Robinhood Momentum Strategy (LIVE TRADING)
JAVoIS: Just Another Volatility Strategy (LIVE TRADING)
101 Alphas Project: Alpha# 41
它們都有幾個共同點:
1)展示出持續盈利的回測;
2)使用廣泛的股票并廣泛分配資本,而與任何特定的股票或行業無關;
3)與市場無關;
4)符合Quantopian團隊規定的標準;
要知道,選用不同的量化交易算法所帶來的組合收益截然不同。上一篇中我們講解了ALGO-1 Zack’s Long-Short PEAD:金融小課堂 | 零基礎30天API量化速成_第13講,接下來介紹第二種算法:
?ALGO - 2?
Zack’s Long PEAD with News Sentiment
收益公告期是每種股票生命中的特殊時期。?股票受到了越來越多的審查,投資者和交易者對與它們有關的所有新聞都做出了更加積極的反應。
ALGO-2?Zack’s Long PEAD with News Sentiment算法的目的是對沖公告發布后的價格浮動,與前一篇文章中講解的Zack’s Long-Short PEAD?一樣,Long PEAD也使用了Zack和Accern的數據。但是,當預期浮動為正時,該算法僅持有多頭頭寸。
完整代碼如下(來源github):
importnumpyasnp?fromquantopian.algorithmimportattach_pipeline, pipeline_outputfromquantopian.pipelineimportPipelinefromquantopian.pipeline.data.builtinimportUSEquityPricingfromquantopian.pipeline.factorsimportCustomFactor, AverageDollarVolumefromquantopian.pipeline.filters.morningstarimportQ500US, Q1500USfromquantopian.pipeline.dataimportmorningstarasmstarfromquantopian.pipeline.classifiers.morningstarimportSectorfromquantopian.pipeline.filters.morningstarimportIsPrimaryShare?fromquantopian.pipeline.data.zacksimportEarningsSurprisesfromquantopian.pipeline.factors.zacksimportBusinessDaysSinceEarningsSurprisesAnnouncement??# from quantopian.pipeline.data.accern import alphaone_free as alphaone# Premium version availabe at# https://www.quantopian.com/data/accern/alphaonefromquantopian.pipeline.data.accernimportalphaoneasalphaone?defmake_pipeline(context):# Create our pipeline? ? ? pipe = Pipeline()? ?# Instantiating our factors? ? ? factor = EarningsSurprises.eps_pct_diff_surp.latest?# Filter down to stocks in the top/bottom according to# the earnings surprise? ? longs = (factor >= context.min_surprise) & (factor <= context.max_surprise)#shorts = (factor <= -context.min_surprise) & (factor >= -context.max_surprise)'''? ? change value of q_filters to Q1500US to use Q1500US universe? ? '''? ? q_filters = Q500US#q_filter1 = Q1500US?# Set our pipeline screens? # Filter down stocks using sentiment? ? ? article_sentiment = alphaone.article_sentiment.latest? ? top_universe = q_filters() & universe_filters() & longs & article_sentiment.notnan() \& (article_sentiment >.30)# bottom_universe = q_filters() & universe_filters() & shorts & article_sentiment.notnan() \& (article_sentiment < -.30)?# Add long/shorts to the pipeline? pipe.add(top_universe,"longs")# pipe.add(bottom_universe, "shorts")pipe.add(BusinessDaysSinceEarningsSurprisesAnnouncement(),'pe')? ? pipe.set_screen(factor.notnan())returnpipe? ? ? ? definitialize(context):#: Set commissions and slippage to 0 to determine pure alpha'''? ? ? ? set_commission(commission.PerShare(cost=0, min_trade_cost=0))? ? ? ? set_slippage(slippage.FixedSlippage(spread=0))? ? ? ? set_slippage(slippage.FixedSlippage(spread=0.02))? ? ? set_commission(commission.PerTrade(cost=5.00))? ? ? ? set_slippage(TradeAtTheOpenSlippageModel(0.2,.05))? ? set_commission(commission.PerShare(cost=0.01))? ? ? '''#: Declaring the days to hold, change this to what you wantcontext.days_to_hold =3#: Declares which stocks we currently held and how many days we've held them dict[stock:days_held]? ? context.stocks_held = {}?#: Declares the minimum magnitude of percent surprisecontext.min_surprise =.00context.max_surprise =.05?#: OPTIONAL - Initialize our Hedge# See order_positions for hedging logic# context.spy = sid(8554)? ? # Make our pipelineattach_pipeline(make_pipeline(context),'earnings')?? ? # Log our positions at 10:00AM? ? schedule_function(func=log_positions,? ? ? ? ? ? ? ? ? ? ? date_rule=date_rules.every_day(),time_rule=time_rules.market_close(minutes=30))# Order our positions? ? schedule_function(func=order_positions,? ? ? ? ? ? ? ? ? ? ? date_rule=date_rules.every_day(),? ? ? ? ? ? ? ? ? ? ? time_rule=time_rules.market_open())?defbefore_trading_start(context, data):# Screen for securities that only have an earnings release# 1 business day previous and separate out the earnings surprises into# positive and negative results = pipeline_output('earnings')results = results[results['pe'] ==1]? ? assets_in_universe = results.index? ? context.positive_surprise = assets_in_universe[results.longs]#context.negative_surprise = assets_in_universe[results.shorts]?deflog_positions(context, data):#: Get all positions? iflen(context.portfolio.positions) >0:all_positions ="Current positions for %s : "% (str(get_datetime()))forposincontext.portfolio.positions:ifcontext.portfolio.positions[pos].amount !=0:all_positions +="%s at %s shares, "% (pos.symbol, context.portfolio.positions[pos].amount)? ? ? ? log.info(all_positions)? ? ? ? ? deforder_positions(context, data):"""? ? Main ordering conditions to always order an equal percentage in each position? ? so it does a rolling rebalance by looking at the stocks to order today and the stocks? ? we currently hold in our portfolio.? ? """? ? port = context.portfolio.positions? ? record(leverage=context.account.leverage)?# Check our positions for loss or profit and exit if necessary? ? check_positions_for_loss_or_profit(context, data)? ? # Check if we've exited our positions and if we haven't, exit the remaining securities# that we have leftforsecurityinport:ifdata.can_trade(security):ifcontext.stocks_held.get(security)isnotNone:context.stocks_held[security] +=1ifcontext.stocks_held[security] >= context.days_to_hold:order_target_percent(security,0)delcontext.stocks_held[security]# If we've deleted it but it still hasn't been exited. Try exiting again? else:log.info("Haven't yet exited %s, ordering again"% security.symbol)order_target_percent(security,0)?# Check our current positionscurrent_positive_pos = [posforposinportif(port[pos].amount >0andposincontext.stocks_held)]#current_negative_pos = [pos for pos in port if (port[pos].amount < 0 and pos in context.stocks_held)]#negative_stocks = context.negative_surprise.tolist() + current_negative_pos? ? positive_stocks = context.positive_surprise.tolist() + current_positive_pos? ? '''? ? # Rebalance our negative surprise securities (existing + new)? ? for security in negative_stocks:? ? ? ? can_trade = context.stocks_held.get(security) <= context.days_to_hold or \? ? ? ? ? ? ? ? ? ? context.stocks_held.get(security) is None? ? ? ? if data.can_trade(security) and can_trade:? ? ? ? ? ? order_target_percent(security, -1.0 / len(negative_stocks))? ? ? ? ? ? if context.stocks_held.get(security) is None:? ? ? ? ? ? ? ? context.stocks_held[security] = 0? ? '''# Rebalance our positive surprise securities (existing + new)? ? ? ? ? ? ? ? forsecurityinpositive_stocks:can_trade = context.stocks_held.get(security) <= context.days_to_holdor\context.stocks_held.get(security)isNoneifdata.can_trade(security)andcan_trade:order_target_percent(security,1.0/ len(positive_stocks))ifcontext.stocks_held.get(security)isNone:context.stocks_held[security] =0?#: Get the total amount ordered for the day# amount_ordered = 0 # for order in get_open_orders():#? ? for oo in get_open_orders()[order]:#? ? ? ? amount_ordered += oo.amount * data.current(oo.sid, 'price')?#: Order our hedge# order_target_value(context.spy, -amount_ordered)# context.stocks_held[context.spy] = 0# log.info("We currently have a net order of $%0.2f and will hedge with SPY by ordering $%0.2f" % (amount_ordered, -amount_ordered))? ? defcheck_positions_for_loss_or_profit(context, data):# Sell our positions on longs/shorts for profit or lossforsecurityincontext.portfolio.positions:is_stock_held = context.stocks_held.get(security) >=0ifdata.can_trade(security)andis_stock_heldandnotget_open_orders(security):? ? ? ? ? ? current_position = context.portfolio.positions[security].amount? ? ? ? ? ? ? cost_basis = context.portfolio.positions[security].cost_basis? price = data.current(security,'price')# On Long & Profitifprice >= cost_basis *1.10andcurrent_position >0:order_target_percent(security,0)log.info( str(security) +' Sold Long for Profit')delcontext.stocks_held[security]'''? ? ? ? ? ? # On Short & Profit? ? ? ? ? ? if price <= cost_basis* 0.90 and current_position < 0:? ? ? ? ? ? ? ? order_target_percent(security, 0)? ? ? ? ? ? ? ? ? log.info( str(security) + ' Sold Short for Profit')? ? ? ? ? ? ? ? ? del context.stocks_held[security]? ? ? ? ? ? '''# On Long & Lossifprice <= cost_basis *0.90andcurrent_position >0:order_target_percent(security,0)log.info( str(security) +' Sold Long for Loss')delcontext.stocks_held[security]'''? ? ? ? ? ? # On Short & Loss? ? ? ? ? ? if price >= cost_basis * 1.10 and current_position < 0:? ? ? ? ? ? ? ? ? order_target_percent(security, 0)? ? ? ? ? ? ? ? ? log.info( str(security) + ' Sold Short for Loss')? ? ? ? ? ? ? ? ? del context.stocks_held[security]? ? ? ? ? ? '''# Constants that need to be globalCOMMON_STOCK='ST00000001'?SECTOR_NAMES = {101:'Basic Materials',102:'Consumer Cyclical',103:'Financial Services',104:'Real Estate',205:'Consumer Defensive',206:'Healthcare',207:'Utilities',308:'Communication Services',309:'Energy',310:'Industrials',311:'Technology',}?# Average Dollar Volume without nanmean, so that recent IPOs are truly removedclassADV_adj(CustomFactor):? ? inputs = [USEquityPricing.close, USEquityPricing.volume]window_length =252? ? defcompute(self, today, assets, out, close, volume):close[np.isnan(close)] =0out[:] = np.mean(close * volume,0)defuniverse_filters():# Equities with an average daily volume greater than 750000.high_volume = (AverageDollarVolume(window_length=252) >750000)? ? # Not Misc. sector:? ? sector_check = Sector().notnull()? ? # Equities that morningstar lists as primary shares.#NOTE:This will return False for stocks not in the morningstar database.? ? primary_share = IsPrimaryShare()? ? # Equities for which morningstar's most recent Market Cap value is above $300m.have_market_cap = mstar.valuation.market_cap.latest >300000000? ? # Equities not listed as depositary receipts by morningstar.# Note the inversion operator, `~`, at the start of the expression.? ? not_depositary = ~mstar.share_class_reference.is_depositary_receipt.latest? ? # Equities that listed as common stock (as opposed to, say, preferred stock).# This is our first string column. The .eq method used here produces a Filter returning# True for all asset/date pairs where security_type produced a value of 'ST00000001'.? ? common_stock = mstar.share_class_reference.security_type.latest.eq(COMMON_STOCK)? ? # Equities whose exchange id does not start with OTC (Over The Counter).# startswith() is a new method available only on string-dtype Classifiers.# It returns a Filter.not_otc = ~mstar.share_class_reference.exchange_id.latest.startswith('OTC')? ? # Equities whose symbol (according to morningstar) ends with .WI# This generally indicates a "When Issued" offering.# endswith() works similarly to startswith().not_wi = ~mstar.share_class_reference.symbol.latest.endswith('.WI')? ? # Equities whose company name ends with 'LP' or a similar string.# The .matches() method uses the standard library `re` module to match# against a regular expression.not_lp_name = ~mstar.company_reference.standard_name.latest.matches('.* L[\\. ]?P\.?$')? ? # Equities with a null entry for the balance_sheet.limited_partnership field.# This is an alternative way of checking for LPs.? ? not_lp_balance_sheet = mstar.balance_sheet.limited_partnership.latest.isnull()? ? # Highly liquid assets only. Also eliminates IPOs in the past 12 months# Use new average dollar volume so that unrecorded days are given value 0# and not skipped over# S&P Criterionliquid = ADV_adj() >250000? ? # Add logic when global markets supported# S&P Criteriondomicile =True? ? # Keep it to liquid securitiesranked_liquid = ADV_adj().rank(ascending=False) <1500? ? ? ? universe_filter = (high_volume & primary_share & have_market_cap & not_depositary &? ? ? ? ? ? ? ? ? ? ? common_stock & not_otc & not_wi & not_lp_name & not_lp_balance_sheet &? ? ? ? ? ? ? ? ? ? liquid & domicile & sector_check & liquid & ranked_liquid)? ? returnuniverse_filter# Slippage model to trade at the open or at a fraction of the open - close range.? classTradeAtTheOpenSlippageModel(slippage.SlippageModel):'''Class for slippage model to allow trading at the open? ? ? ? or at a fraction of the open to close range.? ? ? '''# Constructor, self and fraction of the open to close range to add (subtract)? #? from the open to model executions more optimistically? def__init__(self, fractionOfOpenCloseRange, spread):?# Store the percent of open - close range to take as the execution price? ? ? ? ? self.fractionOfOpenCloseRange = fractionOfOpenCloseRange?# Store bid/ask spread? ? ? ? ? self.spread = spread?defprocess_order(self, data, order):# Apply fractional slippage? openPrice = data.current(order.sid,'open')closePrice = data.current(order.sid,'close')? ? ? ? ocRange = closePrice - openPrice? ? ? ? ? ocRange = ocRange * self.fractionOfOpenCloseRange? ? ? ? ? targetExecutionPrice = openPrice + ocRange? log.info('\nOrder:{0} open:{1} close:{2} exec:{3} side:{4}'.format(? ? ? ? ? ? order.sid, openPrice, closePrice, targetExecutionPrice, order.direction))?# Apply spread slippage? ? ? ? ? targetExecutionPrice += self.spread * order.direction?# Create the transaction using the new price we've calculated.? return(targetExecutionPrice, order.amount)
交易員Robb在2.5年內使用AUM $ 100K的條件下進行回測的Algo結果如下:
總回報率:81.49%
基準回報率:17%
Alpha:0.17
Beta:0.47
Sharpe:1.45
Sortino:2.38
波動率:0.17
最大跌幅:-13%
以上
作者:修恩
▎系列閱讀
『聲明:作者對提及的任何產品都沒有既得利益,修恩筆記所有文章僅供參考,不構成任何投資建議策略。』
據說長得好看的人都點了??