Model Logic

Assuming that the long cycle can reflect the trend, and after observing the short-term data, we can better judge the long-term trend.

  • Goal: Predict whether there will be a trend in the future
  • Parameter:$\theta$ :The probability of rising is a float between 0 and 1.
  • Model:Based on long-term data,Determine the prior distribution of $\theta$, and then update our belief in $\theta$ based on short-term data to obtain the posterior distribution.
  • prior:Because $\theta$ is a continuous distribution from 0 to 1, we choose Beta distribution.
  • likelihood:Because we want to predict the rise and fall, we can simplify the data to tossing a coin, which is a binomial distribution.
  • beta - binomial conjugate, if beta distribution is used as the prior distribution and binomial distribution is used as the Siran function, then the posterior distribution is also beta distribution, which can simplify the calculation.

Applying Bayesian theory

$f(\theta)$ is the prior distribution prior $f(\theta|X)$ is the posterior distribution posterior $f(X|\theta)$ is the likelihood function

$$f(\theta|X) = \frac{f(X|\theta) \cdot f(\theta)}{\int f(X|\theta) \cdot f(\theta) \, d\theta}$$

According to the assumptions of our model, $\theta$ (the probability of rising, the probability that the open is less than the close) conforms to the beta distribution. The shape parameter $\alpha$ of the beta distribution is the number of K-lines with an open price less than the close price in the long-period sequence, and $\beta$ is the number of K-lines with an open price greater than the close price. Probability density function of Beta distribution:

$$\text{Beta}(\theta | \alpha, \beta) = \frac{\theta^{\alpha - 1} (1 - \theta)^{\beta - 1}}{B(\alpha, \beta)}$$

The binomial function conforms to the binomial distribution, $n$ is the length of the short-period sequence, and $k$ is the number of K-lines whose open price is less than the close price:

$$\text{Binomial}(k | n, \theta) = \binom{n}{k} \theta^k (1 - \theta)^{n-k}$$

The parameters of the posterior distribution are obtained after calculation according to the Bayesian formula:

$$ \alpha_{\text{post}} = \alpha + k, \beta_{\text{post}} = \beta + n - k$$

So the posterior distribution is:

$$\text{Beta}(\theta | \alpha_{\text{post}}, \beta_{\text{post}}) = \frac{\theta^{\alpha_{\text{post}} - 1} (1 - \theta)^{\beta_{\text{post}} - 1}}{B(\alpha_{\text{post}}, \beta_{\text{post}})}$$

In this way, we get the posterior distribution of $\theta$ updated with short-term data, and then we use the mean of the posterior distribution as an estimate of $\theta$, and we get the posterior probability of increase.

  • Example Long cycle sequence length: 30, rise times 20. Short cycle sequence length: 10, rise times 2. Alt text The above figure is a priori and posterior probability distribution diagram. It can be seen that in the short-period sequence, only two of the 10 K-lines are rising, and the posterior distribution of $\theta$ (up) is closer to the left than the prior distribution.

Strategy parameters

  • dual_side: whether to open a position in both directions
  • pyramiding: maximum number of openings in the same direction
  • long_trend_window_size: length of long-period sequence
  • short_trend_window_size: length of short-period sequence
  • long_trend_period: long-period granularity,
  • short_trend_period: short-period granularity
  • buy_threshold: long threshold
  • sell_threshold: short threshold
  • fix_size: the amount of each opening

Default parameters:

  • dual_side : 1
  • pyramiding: 10
  • long_trend_window_size: 300 # hourly k sequence with a length of 300
  • short_trend_window_size: 30 # 5-minute k sequence with a length of 30
  • long_trend_period: 60 # represents 60-minute k, i.e. hourly k
  • short_trend_period: 5 # represents 5-minute k
  • buy_threshold: long threshold 0.54
  • sell_threshold: short threshold 0.45
  • fix_size: the amount of each position opened 1

Asset: BTCUSD Start date: 2023-1-1 End date: 2023-12-8 Fee rate: 0.2% Capital: 100000

Strategy practice

Total trading days: 340

Profitable trading days: 171

Loss trading days: 158

Starting capital: 100,000

Ending capital: 352,001

Rate of return: 252%

Annualized rate of return: 270%

Maximum percentage retracement: 28.7%

Total profit and loss: 252,001.83

Total fee: 372.33

Total transaction volume: 186,165.84

Total number of transactions: 10

Sharp ratio: 1.70

Return-retracement ratio: 8.78

  • Strategy performance Alt text Alt text

Backtest result 2:

It can be seen that the number of transactions in the backtest result is too small, which is overfitting. Here is another backtest result: Parameters: Alt text Alt text Alt text Alt text Alt text

Conclusion

It can be seen that the number of transactions in backtest 2 has increased. In order to prevent too few transactions, you can choose to reduce the cycle.## 策略代码

from vnpy_ctastrategy import (
    CtaTemplate,
    StopOrder,
    TickData,
    BarData,
    TradeData,
    OrderData,
    BarGenerator,
    ArrayManager,
)
import numpy as np

class BayesBetaBinomial(CtaTemplate):
    author = "vnpy"

    prior = 0
    pyramiding = 1
    long_trend_window_size = 5
    short_trend_window_size = 5
    long_trend_period = 60
    short_trend_period = 5
    fix_size = 1
    buy_threshold = 0.55
    sell_threshold = 0.45
    dual_side = 1
    parameters = ["dual_side", "pyramiding", "long_trend_window_size","short_trend_window_size","long_trend_period", "short_trend_period" ,"fix_size", "buy_threshold", "sell_threshold"]

    def __init__(self, cta_engine, strategy_name, vt_symbol, setting):
        super().__init__(cta_engine, strategy_name, vt_symbol, setting)

        self.bg_long = BarGenerator(self.on_bar, self.long_trend_period, self.on_long_bar)
        self.bg_short = BarGenerator(self.on_bar, self.short_trend_period, self.on_short_bar)
        self.am_long = ArrayManager(self.long_trend_window_size)
        self.am_short = ArrayManager(self.short_trend_window_size)
        self.priors_inited = False

    def on_init(self):
        self.write_log("策略初始化")
        self.load_bar(10)

    def on_start(self):
        self.write_log("策略启动")
        self.put_event()

    def on_stop(self):
        self.write_log("策略停止")
        self.put_event()

    def on_bar(self, bar: BarData):
        self.bg_long.update_bar(bar)
        self.bg_short.update_bar(bar)

    def on_long_bar(self, long_bar: BarData):
        self.am_long.update_bar(long_bar)
        if not self.am_long.inited:
            return

        ups = sum(1 for index in range(self.am_long.size) if self.am_long.close_array[index] > self.am_long.open_array[index])
        self.alpha = ups + 1
        self.beta_param = self.am_long.size - ups + 1
        self.priors_inited = True


    def on_short_bar(self, short_bar: BarData):
        self.am_short.update_bar(short_bar)
        if not self.am_short.inited:
            return

        if not self.priors_inited:
            return

        ups = sum(1 for index in range(self.am_short.size) if self.am_short.close_array[index] > self.am_short.open_array[index])
        pred_post = (self.alpha + ups) / (self.alpha + self.beta_param + self.am_short.size)

        random_number = np.random.rand()
        if self.dual_side:
            if random_number < 1:         # < 1时即是忽略此条件
                # both long and short
                if pred_post > self.buy_threshold:
                    if self.pos >= 0:
                        if abs(self.pos) < self.pyramiding:
                            self.buy(short_bar.close_price, self.fix_size)
                    else:
                        self.cover(short_bar.close_price, abs(self.pos))
                        self.buy(short_bar.close_price, self.fix_size)


                if pred_post < self.sell_threshold:
                    if self.pos <= 0:
                        if abs(self.pos) < self.pyramiding:
                            self.short(short_bar.close_price, self.fix_size)
                    else:
                        self.sell(short_bar.close_price, abs(self.pos))
                        self.short(short_bar.close_price, self.fix_size)

        else:
            if pred_post > self.buy_threshold:
                if self.pos >= 0 and self.pos < self.pyramiding:
                    self.buy(short_bar.close_price, self.fix_size)

            if pred_post < self.sell_threshold:
                if self.pos != 0:
                    self.sell(short_bar.close_price, self.pos)