Dresdon二次開發

  在上一篇文章中,我們已經對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);
}

  通過這種方法,用戶就可以自行創建更高級的函數,進而使得自己的模型變得更為簡潔。

 

轉載請註明原文地址並標明轉載://www.cnblogs.com/loveis715/p/13324937.html

商業轉載請事先與我聯繫:[email protected]

公眾號一定幫忙別標成原創,因為協調起來太麻煩了。。。

 

Tags: