数字货币量化之止盈和止损

完善双均线策略

浮动止盈

  • 算法描述
    • 建仓后,时刻记录产品价格的最高点
    • 计算最新价相对于前期最高点下跌幅幅度的百分比
    • 如果下跌百分比大于某个阀值,止盈退出
  • 结果:
    • 策略收益 194.24%
    • 策略年化收益:109.006%
    • 策略波动率:54.675%
    • 夏普比率:1.604
    • 最大回撤: 37.042%

代码实战

dma_float_exit_alg.py (浮动止盈)


"""
    双均线改进策略
    - 浮动止盈
"""

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import talib
from logbook import Logger

from catalyst import run_algorithm
from catalyst.api import record, symbol, order_target, order

# 需要先加载数据
# catalyst ingest-exchange -x binance -i btc_usdt -f daily
# catalyst ingest-exchange -x binance -i eth_usdt -f daily
# catalyst ingest-exchange -x binance -i ltc_usdt -f daily
# catalyst ingest-exchange -x binance -i eos_usdt -f daily

NAMESPACE = 'dma_float_exit_alg'
log = Logger(NAMESPACE)

SHORT_WIN = 5               # 短周期窗口
LONG_WIN = 20               # 长周期窗口

ATR_WIN_SIZE = 20           # ATR窗口
RISK_RATIO = 0.05          # 风险比例

INC_POS_RATE = 0.05         # 价格每上涨5%加仓

MAX_DROP_RATE = 0.3        # 从最高点回撤的百分比

def get_available_cash(context, use_compound_interest=False):
    """
        获取当前可用资金
        use_compound_interest: 是否使用复利
    """
    if use_compound_interest:
        # 使用复利
        available_cash = context.portfolio.cash
    else:
        available_cash = min(context.portfolio.starting_cash, context.portfolio.cash)
    return available_cash

def get_risk_indices(perf):
    """
        计算风险指标,包括:
        1. 策略收益
        2. 策略年化收益
        3. 策略波动率
        4. 夏普比率
        5. 最大回撤
    """
    # 策略执行天数
    n = len(perf)

    # 1. 策略收益
    total_returns = perf.iloc[-1]['algorithm_period_return']

    # 2. 策略年化收益
    total_ann_returns = (1 + total_returns) ** (250 / n) - 1

    # 3. 策略波动率
    algo_volatility = perf.iloc[-1]['algo_volatility']

    # 4. 夏普比率
    sharpe = perf.iloc[-1]['sharpe']

    # 5. 最大回撤
    max_drawdown = np.abs(perf.iloc[-1]['max_drawdown'])

    return total_returns, total_ann_returns, algo_volatility, sharpe, max_drawdown

def initialize(context):
    """
        初始化
    """
    context.i = 0                       # 经历过的交易周期
    # 设置加密货币池
    context.asset_pool = [symbol('btc_usdt'),
                          symbol('eth_usdt'),
                          symbol('ltc_usdt'),
                          symbol('eos_usdt')]
    context.set_commission(maker=0.001, taker=0.001)    # 设置手续费
    context.set_slippage(slippage=0.001)                # 设置滑点

    # 初始化字典,用于记录上一次买入加密货币的价格
    context.last_price = {asset: None for asset in context.asset_pool}

    # 初始化字典,用于记录在当前交易周期中加密货币是否被交易过
    # 这里限制每天只能交易一次
    context.traded_today = {asset: False for asset in context.asset_pool}

    # 初始化字典,用于记录上一次买入加密货币的日期
    context.entry_dates = {asset: None for asset in context.asset_pool}

def handle_data(context, data):
    """
        在每个交易周期上运行的策略
    """
    context.i += 1  # 记录交易周期
    for asset in context.traded_today:
        # 每个交易周期重置交易flag
        context.traded_today[asset] = False

    if context.i < LONG_WIN + 1:
        # 如果交易周期过短,无法计算均线,则跳过循环
        log.warning('交易周期过短,无法计算指标')
        return

    # 获取当前周期内有效的加密货币
    context.available_asset_pool = [asset
                                    for asset in context.asset_pool
                                    if asset.start_date <= data.current_dt]

    context.up_cross_signaled = set()   # 初始化金叉的交易对集合
    context.down_cross_signaled = set()  # 初始化死叉的交易对集合

    for asset in context.available_asset_pool:
        # 遍历每一个加密货币对
        # 获得历史价格
        hitory_data = data.history(asset,
                                   'close',
                                   bar_count=LONG_WIN + 1,
                                   frequency='1D',
                                   )
        if len(hitory_data) >= LONG_WIN + 1:
            # 保证新的货币有足够的时间计算均线
            # 计算双均线
            short_avgs = hitory_data.rolling(window=SHORT_WIN).mean()
            long_avgs = hitory_data.rolling(window=LONG_WIN).mean()

            # 双均线策略
            # 短期均线上穿长期均线
            if (short_avgs[-2] < long_avgs[-2]) and (short_avgs[-1] >= long_avgs[-1]):
                # 形成金叉
                context.up_cross_signaled.add(asset)

            # 短期均线下穿长期均线
            if (short_avgs[-2] > long_avgs[-2]) and (short_avgs[-1] <= long_avgs[-1]):
                # 形成死叉
                context.down_cross_signaled.add(asset)

    # 卖出均线死叉信号的持仓交易对
    for asset in context.portfolio.positions:
        if context.traded_today[asset]:
            # 当前交易周期中该产品被交易过,跳过循环
            continue

        if asset in context.down_cross_signaled:
            order_target(asset, 0)
            # 卖出货币对,重置购买价和买入日期
            context.last_price[asset] = None
            context.entry_dates[asset] = None
            context.traded_today[asset] = True

    # 检查有无符合浮动止盈的持仓品种
    for asset in context.portfolio.positions:
        if context.traded_today[asset]:
            # 当前交易周期中该产品被交易过,跳过循环
            continue

        if context.portfolio.positions[asset].amount > 0:
            last_entry_date = context.entry_dates[asset]  # 上一次买入的日期
            bar_count = (data.current_dt - last_entry_date).days + 1
            # 计算截止到当前交易周期的最高价最大值(Highest High Value, HHV)
            history_data = data.history(asset,
                                        ['high', 'close'],
                                        bar_count=bar_count,
                                        frequency='1D',
                                        )
            if len(history_data) > 1:
                hhv = history_data['high'].max()
                drop_rate = (hhv - history_data.iloc[-1]['close']) / hhv  # 以当前交易周期的收盘价计算跌幅比例
                if drop_rate > MAX_DROP_RATE:
                    log.info('止盈操作...')
                    order_target(asset, 0)
                    context.last_price[asset] = None
                    context.entry_dates[asset] = None
                    context.traded_today[asset] = True

    # 买入均线金叉信号的持仓股
    for asset in context.up_cross_signaled:
        if context.traded_today[asset]:
            # 当前交易周期中该产品被交易过,跳过循环
            continue

        if asset not in context.portfolio.positions:
            history_data = data.history(asset,
                                        ['high', 'low', 'close'],
                                        bar_count=ATR_WIN_SIZE + 1,
                                        frequency='1D',
                                        )
            atr = talib.ATR(history_data['high'], history_data['low'], history_data['close'],
                            timeperiod=ATR_WIN_SIZE)

            available_cash = get_available_cash(context)

            if available_cash > 0:
                # 如果有可用现金
                amount_to_buy = available_cash * RISK_RATIO / atr[-1]  # 计算购买的数量
                close_price = data.current(asset, 'close')
                if (amount_to_buy >= asset.min_trade_size) and (available_cash >= amount_to_buy * close_price):
                    # 购买的数量大于最小购买数量并且有足够的现金购买
                    order(asset, amount_to_buy)
                    # 买入货币对,记录购买日期和购买价
                    context.entry_dates[asset] = data.current_dt
                    context.last_price[asset] = close_price
                    context.traded_today[asset] = True

    # 检查有无符合加仓条件的持仓品种
    for asset in context.portfolio.positions:
        if context.traded_today[asset]:
            # 当前交易周期中该产品被交易过,跳过循环
            continue

        if context.portfolio.positions[asset].amount > 0:
            close_price = data.current(asset, 'close')
            last_price = context.last_price[asset]
            if (close_price - last_price) / last_price >= INC_POS_RATE:
                available_cash = get_available_cash(context)
                if available_cash > 0:
                    # 如果有可用现金
                    # 按盈利比例均匀加仓
                    log.info('加仓操作...')
                    cash_for_each_asset = available_cash / len(context.available_asset_pool)
                    amount_to_buy = cash_for_each_asset / close_price
                    if amount_to_buy >= asset.min_trade_size:
                        # 购买的数量大于最小购买数量
                        order(asset, amount_to_buy)
                        # 买入货币对,记录购买日期和购买价
                        context.entry_dates[asset] = data.current_dt
                        context.last_price[asset] = close_price
                        context.traded_today[asset] = True

    # 持仓比例
    pos_level = context.portfolio.positions_value / context.portfolio.portfolio_value

    # 记录每个交易周期的现金
    record(cash=context.portfolio.cash, pos_level=pos_level)

    # 输出信息
    log.info('日期:{},资产:{:.2f},持仓比例:{:.6f}%,持仓产品:{}'.format(
        data.current_dt, context.portfolio.portfolio_value, pos_level * 100,
        ', '.join([asset.asset_name for asset in context.portfolio.positions]))
    )

def analyze(context, perf):
    # 保存交易记录
    perf.to_csv('./perf_results/dma_float_exit_performance.csv')

    # 获取交易所的计价货币
    exchange = list(context.exchanges.values())[0]
    quote_currency = exchange.quote_currency.upper()

    # 图1:可视化资产值
    ax1 = plt.subplot(311)
    perf['portfolio_value'].plot(ax=ax1)
    ax1.set_ylabel('Portfolio Value\n({})'.format(quote_currency))
    start, end = ax1.get_ylim()
    ax1.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))

    # 图2:可视化仓位
    ax2 = plt.subplot(312)
    perf['pos_level'].plot(ax=ax2)
    ax2.set_ylabel('Position Level')
    start, end = 0, 1
    ax2.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))

    # 图3:可视化现金数量
    ax3 = plt.subplot(313, sharex=ax1)
    perf['cash'].plot(ax=ax3)
    ax3.set_ylabel('Cash\n({})'.format(quote_currency))
    start, end = ax3.get_ylim()
    ax3.yaxis.set_ticks(np.arange(0, end, end / 5))

    plt.tight_layout()
    plt.show()

    # 评价策略
    total_returns, total_ann_returns, algo_volatility, sharpe, max_drawdown = get_risk_indices(perf)
    log.info('策略收益: {:.3f}%, 策略年化收益: {:.3f}%, 策略波动率: {:.3f}%, 夏普比率: {:.3f}, 最大回撤: {:.3f}%'.format(
        total_returns * 100, total_ann_returns * 100, algo_volatility * 100, sharpe, max_drawdown * 100
    ))

if __name__ == '__main__':
    run_algorithm(
        capital_base=100000,
        data_frequency='daily',
        initialize=initialize,
        handle_data=handle_data,
        analyze=analyze,
        exchange_name='binance',
        algo_namespace=NAMESPACE,
        quote_currency='usdt',
        start=pd.to_datetime('2019-02-01', utc=True),
        end=pd.to_datetime('2019-12-22', utc=True)
    )

file

输出打印:

[2019-12-24 15:41:35.332583] INFO: dma_float_exit_alg: 日期:2019-03-02 23:59:00+00:00,资产:100000.00,持仓比例:0.000000%,持仓产品:
[2019-12-24 15:41:35.372880] INFO: dma_float_exit_alg: 日期:2019-03-03 23:59:00+00:00,资产:99871.71,持仓比例:64.194880%,持仓产品:LTC / USDT
[2019-12-24 15:41:35.410091] INFO: dma_float_exit_alg: 日期:2019-03-04 23:59:00+00:00,资产:97085.37,持仓比例:63.167278%,持仓产品:LTC / USDT
[2019-12-24 15:41:35.445014] INFO: dma_float_exit_alg: 加仓操作...
[2019-12-24 15:41:35.445281] INFO: dma_float_exit_alg: 日期:2019-03-05 23:59:00+00:00,资产:106114.18,持仓比例:66.301217%,持仓产品:LTC / USDT
[2019-12-24 15:41:35.480165] INFO: dma_float_exit_alg: 加仓操作...

...

[2019-12-24 15:41:51.127684] INFO: dma_float_exit_alg: 日期:2019-12-09 23:59:00+00:00,资产:160908.44,持仓比例:0.000000%,持仓产品:
[2019-12-24 15:41:51.256723] INFO: dma_float_exit_alg: 日期:2019-12-10 23:59:00+00:00,资产:160592.28,持仓比例:98.386190%,持仓产品:BTC / USDT, EOS / USDT
[2019-12-24 15:41:51.334755] INFO: dma_float_exit_alg: 日期:2019-12-11 23:59:00+00:00,资产:159748.71,持仓比例:98.377668%,持仓产品:BTC / USDT, EOS / USDT
[2019-12-24 15:41:51.378688] INFO: dma_float_exit_alg: 日期:2019-12-12 23:59:00+00:00,资产:159770.24,持仓比例:98.377887%,持仓产品:BTC / USDT, EOS / USDT
[2019-12-24 15:41:51.418849] INFO: dma_float_exit_alg: 日期:2019-12-13 23:59:00+00:00,资产:161364.86,持仓比例:0.000000%,持仓产品:
[2019-12-24 15:41:51.453142] INFO: dma_float_exit_alg: 日期:2019-12-14 23:59:00+00:00,资产:161364.86,持仓比例:0.000000%,持仓产品:
[2019-12-24 15:41:51.486555] INFO: dma_float_exit_alg: 日期:2019-12-15 23:59:00+00:00,资产:161364.86,持仓比例:0.000000%,持仓产品:
[2019-12-24 15:41:51.524502] INFO: dma_float_exit_alg: 日期:2019-12-16 23:59:00+00:00,资产:161364.86,持仓比例:0.000000%,持仓产品:

风险指标:

[2019-12-24 15:44:30.216254] INFO: dma_float_exit_alg: 策略收益: 61.365%, 策略年化收益: 44.495%, 策略波动率: 50.878%, 夏普比率: 0.981, 最大回撤: 39.403%

我们可以看到,这个策略收益是非常可观的,最大回撤也减少了很多。

为者常成,行者常至