在上一篇文章中,我們已經對Dresdon所提供的功能進行了簡單的介紹。在這篇文章中,我們將介紹如何基於Dresdon進行二次開發。
Dresdon的擴充套件點
就像上一篇文章所介紹的那樣,Dresdon主要是一個量化引擎。使用者可以通過指令碼或者Java程式設計的方式來描述模型的買賣條件,並進一步通過掃描該模型在所有股票中的所有匹配來評估該模型的具體表現。通過這種方式,使用者可以很大程度地優化自己的交易系統,從而實現穩定盈利。
通過指令碼來描述股票的買入賣出條件十分簡單:
// 當日和前日股價上漲 $isRaisingUp = growth(close()) > 0.01 && growth(close(), 1) > 0.01 // 5日前存在著一個長度至少為30,震盪幅度小於5%的平臺 $isPlatform = platform_exists(0.05, 30, 5) // 在平臺前存在長度至少為20日,最大上漲幅度為12%的緩慢增長 $isSlowRaiseBeforePlatform = is_slow_raise(0.12, 20, platform_start(0.05, 30, 5)) …… $buyCondition = $isRaisingUp && $isPlatform && $isSlowRaiseBeforePlatform && …… $sellCondition = close(0) < ma5(0) && ……
接下里使用者就可以通過掃描2006年1月到2020年4月之間所有匹配來統計該模型的表現:
{ "averageBenefit" : 0.049035519980520418, // 平均單筆收益為4.9%左右 "maxBenefit" : 74.86122362293308, // 最高收益為74.9% "minBenefit" : -4.000000000000014, // 最大止損為4% "totalCount" : 313, // 2006.01 – 2020.04之間匹配313次 "averageDayCount" : 11.875656742556918, // 平均持股時間為11.9天 "successRatio" : 0.46059544658493873 // 成功率為46%左右 }
當然,如果使用者會Java,那麼他還可以將模型寫成一個Java類,進而得到編譯器的強型別支援:
// 當日和前日股價上漲 BooleanHolder isRaisingUp = and(greaterThan(growth(close()), 0), greaterThan(growth(close(), 1), 0)); // 5日前存在著一個長度至少為30,震盪幅度小於5%的平臺 BooleanHolder platformExists = platformExists(0.05, 30, 5) // 在平臺前存在長度至少為20日,最大上漲幅度為12%的緩慢增長 IntHolder platformStart = platformStart(0.05, 30, 5); BooleanHolder isSlowRaiseBeforePlatform = isSlowRaise(0.12, 20, platformStart); …… BooleanHolder condition = and(isRaisingUp, platformExists, isSlowRaiseBeforePlatform, ……);
除了新增自定義模型之外,使用者還可以新增自定義函式。這些函式可以用來判斷某日K線的特徵,或者擬合特定K線形態。例如下面就是一個用來計算指定K線震動幅度的函式:
public static Value.Shrink shrink(int index) { return new Value.Shrink(index); } @Operation(key = KEY_SHRINK, resultType = DOUBLE, arguments = { @Arguments(paramTypes = { INT }) }) public static class Shrink extends HolderBase<Double> implements DoubleHolder { protected IntHolder index; protected Integer indexValue; public Shrink(int index) { this(new IntValue(index)); } public Shrink(IntHolder index) { super(KEY_SHRINK); this.index = index; } @Override public void preprocess(QuantContext context) { super.preprocess(context); preprocess(context, index); } @Override public boolean needRefresh(QuantContext context) { return !equals(indexValue, index, context); } @Override protected Double recalculate(QuantContext context) { indexValue = index.getValue(context); if (indexValue == null) { return null; } DailyTrading dailyTrading = context.getTradings().get(indexValue); double blockSize = Math.abs(dailyTrading.getClose() - dailyTrading.getOpen()); double totalVariation = dailyTrading.getHigh() - dailyTrading.getLow(); this.value = blockSize > SMALL_DOUBLE_VALUE ? totalVariation / blockSize : Double.MAX_VALUE; return value; } @Override public void persist(StringBuilder builder) { builder.append(getKey()); builder.append(INDEX_START); getIndex().persist(builder); builder.append(INDEX_END); } }
在後面的章節中,我們將詳細講解上面模型中各個買入賣出條件的意義。
新增自定義模型
下面就讓我們從新增自定義模型開始。抽取一個模型常常需要經過以下一系列步驟:
1. 確定模型形態。使用者首先需要確定需要匹配的模型的大致形態有哪些,如起漲階段的線形是什麼樣子的,整理期是以什麼形態呈現的,甚至之前籌碼是如何收集的等等。
2. 初篩並收集目標匹配。使用者需要為該模型定義一個大致的匹配條件,然後執行引擎。此時得到的結果可能存在著大量的噪音,因此統計資料常常並不好看。但其中也會包含大量的具有較高準確度的匹配。而這些匹配常常是模型的目標匹配。
3. 細化模型。新增其它條件逐漸祛除噪音,以提高模型正確匹配的比率。
4. 細化賣出條件。新增其它賣出提交,以提高模型的收益率及成功率。
當然,凡事都有一個從陌生到熟悉的過程。在新增了幾個模型之後,使用者可能就能摸到其中的訣竅,進而大大提高模型抽取的效率。在這裡給大家列出來我在抽取模型過程中最常使用的一系列經驗型策略,避免大家重走我之前的彎路。
首先,模型的買入特徵線型要明顯,近端的輔助判斷邏輯要嚴格,而遠端的輔助判斷邏輯要具有較高的容錯性。可以說,所謂的股票拉昇實際上就是股票價格的異動,而該異動的阻力則很大程度上決定了股票行情到底能走多遠。因此起漲階段線形的略微不同都可能導致量化結果產生非常大的差異。比如都是上漲5%,一個有長上影的K線就遠不如沒有長上影的K線。反之離當前交易日越遠的交易,其對當前股價的影響越小,因此遠端的輔助判斷邏輯不宜非常嚴格。
其次,要對常見線形所代表的意義有正確的理解。同樣的K線在不同的位置其意義常常並不相同。例如一般來說,低位揉搓線常常是一個好的K線組合,而高位揉搓線,尤其是放量揉搓線則很可能代表一段行情將要終結。
最後,篩選條件常常是可以通用的。就像第一條所說的那樣,我們要將買入的特徵線形嚴格地區分。比如拉昇是通過一根陽線完成的,和拉昇是通過三根K線形成的組合K線完成的效果類似。但是它們的篩選邏輯則常常有一個為2的索引差:一根陽線完成的拉昇,我們要從前一天的K線檢查,而三根K線組成的拉昇,則需要從三天前的交易開始檢查。只不過這些檢查的引數有些不太相同而已。
新增自定義函式
在編寫一段時間的模型之後,使用者可能就會感覺到引擎內建的各個表示式很難表現一些特定的限制條件。例如他可能常常需要通過如下表示式來限制K線的波動情況:
$noBigShrink = abs(close(0) – open(0)) * 5 < high(0) – low(0)
甚至用Java編寫出來的表示式的可讀性更差:
BooleanHolder noBigShrink = lessThan(multiply(abs(minus(close(0), open(0))), 5), minus(high(0), low(0)));
而這部分的邏輯僅僅是在判斷當日K線的實體是否過小,進而呈現十字星或錘頭線等形態。此時使用者就可以在Plugin裡面新增自定義的表示式:
public static Value.Shrink shrink(int index) { return new Value.Shrink(index); } @Operation(key = KEY_SHRINK, resultType = DOUBLE, arguments = { @Arguments(paramTypes = { INT }) }) public static class Shrink extends HolderBase<Double> implements DoubleHolder { protected IntHolder index; protected Integer indexValue; public Shrink(int index) { this(new IntValue(index)); } public Shrink(IntHolder index) { super(KEY_SHRINK); this.index = index; } @Override public void preprocess(QuantContext context) { super.preprocess(context); preprocess(context, index); } @Override public boolean needRefresh(QuantContext context) { return !equals(indexValue, index, context); } @Override protected Double recalculate(QuantContext context) { indexValue = index.getValue(context); if (indexValue == null) { return null; } DailyTrading dailyTrading = context.getTradings().get(indexValue); double blockSize = Math.abs(dailyTrading.getClose() - dailyTrading.getOpen()); double totalVariation = dailyTrading.getHigh() - dailyTrading.getLow(); this.value = blockSize > SMALL_DOUBLE_VALUE ? totalVariation / blockSize : Double.MAX_VALUE; return value; } @Override public void persist(StringBuilder builder) { builder.append(getKey()); builder.append(INDEX_START); getIndex().persist(builder); builder.append(INDEX_END); } }
下面就讓我們一行行地講解這些程式碼的含義。首先是一個靜態函式:
public static Value.Shrink shrink(int index) { return new Value.Shrink(index); }
通過該靜態函式,使用者可以更直觀地描述模型邏輯,屬於一種語法糖:
new lessThan(new Shrink(0), 5) vs. lessThan(shrink(0), 5)
接下來我們則通過@Operation來標明當前類中包含的邏輯是一個引擎操作的定義。該操作的key為KEY_SHRINK,帶有一個Integer型別的引數,返回值的型別為Double:
@Operation(key = KEY_SHRINK, resultType = DOUBLE, arguments = { @Arguments(paramTypes = { INT }) }) public static class Shrink extends HolderBase<Double> implements DoubleHolder {
這裡有一個概念,那就是Holder。馬上您就會看到,Shrink類例項上並沒有記錄和交易相關的資料,它僅僅用來承載計算邏輯。也就是說,它相當於一個佔位符。實際上,Dresdon支援的所有運算子都是一個Holder,內部只記錄演算法,不記錄任何資料。
那麼交易相關的資料都記錄在哪裡呢?答案是Context。使用者可以通過各個holder的getValue()函式來得到各個holder的當前值。現在就讓我們看看Shrink類的recalculate()函式的是如何使用它的:
@Override protected Double recalculate(QuantContext context) { indexValue = index.getValue(context); if (indexValue == null) { return null; } DailyTrading dailyTrading = context.getTradings().get(indexValue); double blockSize = Math.abs(dailyTrading.getClose() - dailyTrading.getOpen()); double totalVariation = dailyTrading.getHigh() - dailyTrading.getLow(); this.value = blockSize > SMALL_DOUBLE_VALUE ? totalVariation / blockSize : Double.MAX_VALUE; return value; }
可以看到,recalculate()函式傳入了一個QuantContext型別的例項。接下來,該函式的實現通過呼叫index的getValue()函式得到了index的實際值。接下來,我們就從context中取得了目標交易資料(dailyTrading),並依次通過計算目標K線的實體大小(blockSize),當日最高價和最低價之差(totalVariation)來計算當日的波動情況。這裡需要注意的是,計算結果將被首先記錄在value這個域中,然後才被該函式返回。
為了提高計算的效能,我們引入了兩個機制:refresh和preprocess。前者通過判斷引數的值是否變化來確定是否需要執行recalculate()函式。畢竟該函式所包含的計算邏輯可能相當複雜。在其它屬性沒有發生變化的時候,我們可以通過直接返回value這個快取域中記錄的值來提高執行效能:
@Override public boolean needRefresh(QuantContext context) { return !equals(indexValue, index, context); }
另一種情況則是對預處理的支援。其主要用來提高擬合功能的效能。讓我們以一隻股票在多年的交易中存在著一系列盤整平臺的情況為例。如果我們針對不同的日期都計算一次擬合邏輯,那麼引擎的效能將變得很差。畢竟在平臺內部的各個交易日對應的是同一個平臺。為了解決這個問題,我們新增了預處理步驟。該步驟允許引擎對所有交易日進行一次掃描,並將其掃描結果儲存在Context中。在需要的時候從Context中取出相應的預處理結果即可:
@Override public void preprocess(QuantContext context) { …… PlatformExtractor extractor = new PlatformExtractor(symbol, ma5s, rangeValue, minLengthValue); List<PlatformInfo> platforms = extractor.extractPlatforms(); context.getVariableMap().setVariable(key, new ObjectWrapper(symbol, platforms)); } protected PlatformInfo getCurrentPlatform(QuantContext context) { …… ValueHolder<?> variable = context.getVariableMap().getVariable(key); List<PlatformInfo> platforms = (List<PlatformInfo>)(((ObjectWrapper)variable).getValue()); return platforms.stream().filter(platform -> platform.getStartDate().compareTo(seedDate) < 0 && platform.getEndDate().compareTo(seedDate) > 0).findFirst().orElse(null); }
通過這種方法,使用者就可以自行建立更高階的函式,進而使得自己的模型變得更為簡潔。
轉載請註明原文地址並標明轉載:https://www.cnblogs.com/loveis715/p/13324937.html
商業轉載請事先與我聯絡:silverfox715@sina.com
公眾號一定幫忙別標成原創,因為協調起來太麻煩了。。。