金融小課堂 | 零基礎30天API量化速成_第14講

?“量化學習之算法篇”

即使你并無代碼的經驗,但只要您學會如何在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%

以上

作者:修恩

系列閱讀

金融小課堂 | 零基礎30天API量化速成_第1講

金融小課堂 | 零基礎30天API量化速成_第2講

金融小課堂 | 零基礎30天API量化速成_第3講

金融小課堂 | 零基礎30天API量化速成_第4講

金融小課堂 | 零基礎30天API量化速成_第5講

金融小課堂 | 零基礎30天API量化速成_第6講

金融小課堂 | 零基礎30天API量化速成_第7講

金融小課堂 | 零基礎30天API量化速成_第8講

金融小課堂 | 零基礎30天API量化速成_第9講

金融小課堂 | 零基礎30天API量化速成_第10講

金融小課堂 | 零基礎30天API量化速成_第11講

金融小課堂 | 零基礎30天API量化速成_第12講

金融小課堂 | 零基礎30天API量化速成_第13講

聲明:作者對提及的任何產品都沒有既得利益,修恩筆記所有文章僅供參考,不構成任何投資建議策略。

據說長得好看的人都點了??

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,815評論 2 372

推薦閱讀更多精彩內容