请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  数理科学 帖子:3364680 新帖:1

研究模块启用多回测进行参数调优和因子分组介绍

舞蹈家2发表于:5 月 9 日 19:38回复(1)

从研究中启用回测能同时开启多个回测,不仅可以省去在回测模块一次次重复设定的麻烦,还可以将回测结果调回研究模块进行因子分组和参数优化分析,功能十分强大,本文主要介绍一下如何使用这个功能。

聚宽平台打通研究模块和回测模块的功能已经上线一段时间了,但是还有一些同学不太清楚具体该怎么使用,为此本文从零开始简单介绍一下如何使用这个功能,希望能帮助到有类似困惑的同学。已经对这个功能较为熟悉的同学可以直接参阅第三部分或本文参考的其他社区优秀帖子。在本文末尾链接的量化课堂文章里有泛用性更广、功能更强的多回测框架,在熟悉本文前三部分的内容之后可以进一步研究使用该框架。

一、研究中启用回测

刚使用聚宽平台的同学大概还没使用过或者不太熟悉回测功能,因此在一开始没有回测的情况下,需要点击页面上方“我的策略”里的“策略列表”,新建任何一个策略并点击运行回测,这样我们就得到了一个策略和它的一个回测结果。

在“回测详情”页面的上方地址栏末尾就能找到回测ID如:backtestId = daa0d1764a2f0a36e3339e78dc676435

回测地址截图.png

在回测列表里也能找到历史回测的结果,同样的方法也能获得历史回测的ID。

再点击“编辑策略”回到策略页面,同样在上方地址栏末尾就能找到策略ID如:algorithmId = dd99237f48b472aa0d0beee15324b9a2

对于已有策略也可以用同样的方式获取策略ID。

利用策略ID在研究里调用创建回测函数:

create_backtest(algorithm_id, start_date, end_date, frequency="day", initial_cash=10000,  
                    initial_positions=None, extras=None, name=None, code="", benchmark=None)

这里的参数:

  • algorithm_id:策略ID,即前文地址栏等号后面的代码
  • start_date:回测的开始日期
  • end_date:回测的结束日期
  • frequency:回测频率,默认为按天回测,还可以改为'minute',按分钟回测。
  • initial_cash:初始资金
  • extras:额外参数,一个 dict, 用于设置全局的 g 变量,如 extras={'x':1, 'y':2},则回测中 g.x = 1, g.y = 2,需要注意的是,该参数的值是在 initialize 函数执行之后才设置给 g 变量的,所以这会覆盖掉 initialize 函数中 g 变量同名属性的值
  • name:回测名,用以区分不同参数下的不同回测,建议设置。
  • code: 回测用代码,建议在回测模块编译好再使用,默认使用回测模块策略内代码。
  • benchmark:设置基准,如沪深300:'000300.XSHG'
  • initial_positions:初始持仓。持仓会根据价格换成现金加到初始资金中,如果没有给定价格则默认获取股票最近的价格。格式如下:
initial_positions = [
        {
            'security':'000001.XSHE',
            'amount':'100',
        },
        {
            'security':'000063.XSHE',
            'amount':'100',
            'avg_cost': '1.0'
        }
        ]

函数返回回测ID,即 backtest_id

create_backtest 函数示例如下:

code = """
# 导入函数库
from jqdata import *

# 初始化函数,设定基准等等
def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    # 输出内容到日志 log.info()
    log.info('初始函数开始运行且全局只运行一次')
    # 过滤掉order系列API产生的比info级别低的log
    # log.set_level('order', 'info')

    ### 股票相关设定 ###
    # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
    set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')

    ## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
      # 开盘前运行
    run_daily(before_market_open, time='09:00', reference_security='000300.XSHG') 
      # 开盘时运行
    run_daily(market_open, time='09:30', reference_security='000300.XSHG')
      # 收盘后运行
    run_daily(after_market_close, time='15:30', reference_security='000300.XSHG')

## 开盘前运行函数     
def before_market_open(context):
    # 输出运行时间
    log.info('函数运行时间(before_market_open):' str(context.current_dt.time()))

    # 给微信发送消息(添加模拟交易,并绑定微信生效)
    send_message('美好的一天~')

    # 要操作的股票:平安银行(g.为全局变量)
    g.security = '000001.XSHE'

## 开盘时运行函数
def market_open(context):
    log.info('函数运行时间(market_open):' str(context.current_dt.time()))
    security = g.security
    # 获取股票的收盘价
    close_data = attribute_history(security, 5, '1d', ['close'])
    # 取得过去五天的平均价格
    MA5 = close_data['close'].mean()
    # 取得上一时间点价格
    current_price = close_data['close'][-1]
    # 取得当前的现金
    cash = context.portfolio.available_cash

    # 如果上一时间点价格高出五天平均价1%, 则全仓买入
    if current_price > 1.01*MA5:
        # 记录这次买入
        log.info("价格高于均价 1%%, 买入 %s" % (security))
        # 用所有 cash 买入股票
        order_value(security, cash)
    # 如果上一时间点价格低于五天平均价, 则空仓卖出
    elif current_price < MA5 and context.portfolio.positions[security].closeable_amount > 0:
        # 记录这次卖出
        log.info("价格低于均价, 卖出 %s" % (security))
        # 卖出所有股票,使这只股票的最终持有量为0
        order_target(security, 0)

## 收盘后运行函数  
def after_market_close(context):
    log.info(str('函数运行时间(after_market_close):' str(context.current_dt.time())))
    #得到当天所有成交记录
    trades = get_trades()
    for _trade in trades.values():
        log.info('成交记录:' str(_trade))
    log.info('一天结束')
    log.info('##############################################################')

"""


algorithm_id = "xxxx" #用自己的策略ID
extra_vars = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
initial_positions = [
    {
        'security':'000001.XSHE',
        'amount':'100',
    },
    {
        'security':'000063.XSHE',
        'amount':'100',
        'avg_cost': '1.0'
    },
]

params = {
    "algorithm_id": algorithm_id,
    "start_date": "2015-10-01",
    "end_date": "2016-07-31",
    "frequency": "day",
    "initial_cash": "1000000",
    "initial_positions": initial_positions,
    "extras": extra_vars,
}

created_bt_id = create_backtest(code=code, **params)

二、研究中调用回测结果

在研究中还可以调用回测结果进行参数优化等进一步分析工作,用到的函数为:

gt = get_backtest(backtest_id)

这里的backtest_id即回测ID,在上一部分示例中即为created_bt_id。

结果调用方法:

  • gt.get_status():获取回测状态. 返回一个字符串,其含义分别为:
    • none: 未开始
    • running: 正在进行
    • done: 完成
    • failed: 失败
    • canceled: 取消
    • paused: 暂停
    • deleted: 已删除
  • gt.get_params():获得回测参数. 返回一个 dict, 包含调用 create_backtest 时传入的所有信息. (注: algorithm_id,initial_positions,extras 只有在研究中创建的回测才能取到)
  • gt.get_results():获得收益曲线. 返回一个 list,每个交易日是一个 dict,键的含义如下:
    • time: 时间
    • returns: 收益
    • benchmark_returns: 基准收益
    • 如果没有收益则返回一个空的 list
  • gt.get_positions():获得持仓详情. 返回一个 list,每个交易日为一个 dict,键的含义为:
    • time: 时间
    • security: 证券代码
    • security_name: 证券名称
    • amount: 持仓数量
    • price: 股票价格
    • avg_cost: 买入股票平均每股所花的钱
    • closeable_amount: 可平仓数量
    • 如果没有持仓则返回一个空的 list
  • gt.get_orders():获得交易详情. 返回一个 list,每个交易日为一个 dict,键的含义为:
    • time: 时间
    • security: 证券代码
    • security_name: 证券名称
    • action: 交易类型, 开仓('open')/平仓('close')
    • amount: 下单数量
    • filled: 成交数量
    • price: 平均成交价格
    • commission: 交易佣金
    • 如果没有持仓则返回一个空的 list
  • gt.get_records():获得所有 record 记录. 返回一个 list,每个交易日为一个 dict,键是 time 以及调用 record() 函数时设置的值.
  • gt.get_risk():获得总的风险指标. 返回一个 dict,键是各类收益指标数据,如果没有风险指标则返回一个空的 dict.
  • gt.get_period_risks():获得分月计算的风险指标. 返回一个 dict,键是各类指标, 值为一个 pandas.DataFrame. 如果没有风险指标则返回一个空的 dict.

示例如下:

gt = get_backtest(created_bt_id) #使用自己的回测ID

gt.get_status()        # 获取回测状态
gt.get_params()        # 获取回测参数
gt.get_results()       # 获取收益曲线
gt.get_positions()     # 获取所有持仓列表
gt.get_orders()        # 获取交易列表
gt.get_records()       # 获取所有record()记录
gt.get_risk()          # 获取总的风险指标
gt.get_period_risks()  # 获取分月计算的风险指标

# 此处函数的示例结果附在文章末尾

三、简单实例展示

一、MACD参数调优

这里我们借助MACD指标实际展示一下利用该功能进行参数调优:

  • 首先设定默认操作标的为'000016.XSHG'(上证50),MACD起始默认参数为目前普遍使用的(12,26,9),操作时间为2016年1月1日至2018年12月31日,共三年
  • 买入卖出操作遵循金叉买入、死叉卖出的基本原则,即DIF线向上穿过DEA线时买入、DIF线向下穿过DEA线时卖出。
  • 调优结果仅为示例,并不作为标准结果,因选取标的、时段不同,MACD调优结果不尽相同。

1.确定回测模块策略代码

首先在回测模块调试代码,使其在调参过程中也可正常运行。本示例中调好的代码如下:

code = '''
# 导入函数库
from jqlib.technical_analysis import *
import datetime as dt
import pandas as pd


#初始化函数,设定基准等等
def initialize(context):
    #选取上证50
    g.stock = '000016.XSHG'
    #设定标的本身作为基准
    set_benchmark(g.stock)
    #开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    #过滤日志
    log.set_level('order', 'error')


    #设置默认参数
    g.s = 12
    g.l = 26
    g.m = 9


    #开盘前运行
    run_daily(before_market_open, time='before_open', reference_security='000300.XSHG')
    #开盘时运行
    run_daily(market_open, time='open', reference_security='000300.XSHG')


#开盘前运行函数
def before_market_open(context):
    #设置滑点、手续费
    set_slip_fee(context)
    #获取MACD信息
    dif1, dea1, macd1 = MACD(g.stock, check_date = context.previous_date - dt.timedelta(days = 1), SHORT = g.s, LONG = g.l, MID = g.m)
    dif2, dea2, macd2 = MACD(g.stock, check_date = context.previous_date , SHORT = g.s, LONG = g.l, MID = g.m)
    #初始化信号变量
    g.signal = False
    #金叉买入、死叉卖出、否则不调仓
    if dif1 >= dea1 and dif2 <= dea2 :
        g.signal = 'sell'
    elif dif1 <= dea1 and dif2 >= dea2:
        g.signal = 'buy'
    else:
        g.signal = False


#根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
    #将滑点设置为0
    set_slippage(FixedSlippage(0)) 
    #根据不同的时间段设置手续费
    dt=context.current_dt

    if dt>datetime.datetime(2013,1, 1):
        set_commission(PerTrade(buy_cost=0.0003, 
                                sell_cost=0.0013, 
                                min_cost=5)) 

    elif dt>datetime.datetime(2011,1, 1):
        set_commission(PerTrade(buy_cost=0.001, 
                                sell_cost=0.002, 
                                min_cost=5))

    elif dt>datetime.datetime(2009,1, 1):
        set_commission(PerTrade(buy_cost=0.002, 
                                sell_cost=0.003, 
                                min_cost=5))

    else:
        set_commission(PerTrade(buy_cost=0.003, 
                                sell_cost=0.004, 
                                min_cost=5))


## 开盘时运行函数
def market_open(context):
    #根据信号买入卖出标的
    if g.signal == 'sell':
        order_target_value(g.stock, 0)
    elif g.signal == 'buy':
        order_target_value(g.stock, context.portfolio.total_value)
    else:
        pass


## 收盘后运行函数
def after_market_close(context):
    pass
'''

2.在研究里利用回测进行调优

此处若以 遍历法 进行调优将得到最佳结果,但过程相当繁琐,有兴趣的同学可自己尝试一下。本文仅以示范目的,逐一对参数进行调优展示。

首先对第一个参数进行调优,一般以默认参数为中心,向两侧扩大范围尝试得到最优结果,跳过过程直接给出得到的最优参数为6,并且在进一步对后两个参数分别调优时发现,后两个参数不需改变,因此最优参数为(6,26,9)。运行下面代码可以看到右下角同时进行多个回测。

回测情况截图1.png

调优示例代码如下:

created_bt_ids = []
#以默认值为中心进行调优,每次最多进行10组回测
for i in range(-8, -3):
    algorithm_id = "04da82c6238644df6f542f4332d911bc" #用自己的策略ID
    #设置回测内全局变量参数
    extra_vars = {'stock': '000016.XSHG', 's': 12   i, 'l': 26, 'm': 9}

    params = {
        "algorithm_id": algorithm_id,
        "start_date": "2016-01-01",
        "end_date": "2018-12-31",
        "frequency": "day",
        "initial_cash": "1000000",
        "initial_positions": None,
        "extras": extra_vars, 
        "name" : 'stock: 000016.XSHG'   ', s:'   str(12   i)   ', l:26'   ', m:9' 
    }

    created_bt_ids.append(create_backtest(code = code, **params))


import pandas as pd
#先获取基准收益和交易日信息
gt = get_backtest(backtest_id = created_bt_ids[0])
res = gt.get_results()
b_return = []
t = []
for r in res:
    b_return.append(r['benchmark_returns'])
    t.append(r['time'])

#建立df存储数据   
data = pd.DataFrame(index = t)
data['b_return'] = b_return

#填入不同参数下的收益数据
for i in range(len(created_bt_ids)):
    gt = get_backtest(backtest_id = created_bt_ids[i])
    res = gt.get_results()
    name = gt.get_params()['name'] 
    s_return = []
    for r in res:
        s_return.append(r['returns'])

    data[name] = s_return

data.plot(figsize = (15,10))

MACD参数调优1.png

从上图可以看出红线即参数为(6,26,9)时收益曲线表现最好,同时大致能看出几乎在所有参数下,MACD在2016年表现还可以,能拉开与蓝线(基准收益)的差距,但是在2016年以后表现越来越差。

另外,我们再展示一下从第二个参数开始调优的情况,一样先固定另两个参数,调整第二个参数得到最优结果为15,再分别调整另两个参数后确定最优结果为(12,15,9),示例代码如下:

created_bt_ids = []
#以默认值为中心进行调优,每次最多进行10组回测
for i in range(-13, -8):
    algorithm_id = "04da82c6238644df6f542f4332d911bc" #用自己的策略ID
    extra_vars = {'stock': '000016.XSHG', 's': 12, 'l': 26   i, 'm': 9}

    params = {
        "algorithm_id": algorithm_id,
        "start_date": "2016-01-01",
        "end_date": "2018-12-31",
        "frequency": "day",
        "initial_cash": "1000000",
        "initial_positions": None,
        "extras": extra_vars, 
        "name" : 'stock: 000016.XSHG'   ', s:12'   ', l:'   str(26   i)   ', m:9' 
    }

    created_bt_ids.append(create_backtest(code = code, **params))


import pandas as pd
#先获取基准收益和交易日信息
gt = get_backtest(backtest_id = created_bt_ids[0])
res = gt.get_results()
b_return = []
t = []
for r in res:
    b_return.append(r['benchmark_returns'])
    t.append(r['time'])

#建立df存储数据   
data = pd.DataFrame(index = t)
data['b_return'] = b_return

#填入不同参数下的收益数据
for i in range(len(created_bt_ids)):
    gt = get_backtest(backtest_id = created_bt_ids[i])
    res = gt.get_results()
    name = gt.get_params()['name'] 
    s_return = []
    for r in res:
        s_return.append(r['returns'])

    data[name] = s_return


data.plot(figsize = (15,10))

MACD参数调优2.png

这一组整体效果比前一组稍微好一点,但是依然能得到MACD在2016年表现尚可,2016年以后表现不及基准(蓝线)的结论。同时, 两种调优顺序结果不同,也证明此处调优应采用遍历法 得到最优结果,目前得到的两组最优结果可能只是局部最优。

二、BP因子分组收益结果展示

这里我们再展示一下如何利用研究调用回测功能进行因子分组测试:

  • 选取因子为BP因子(市净率倒数),默认股票池为沪深300成分股,剔除当期停牌股票,因子经过去极值、中性化、标准化处理,操作时间为2016年1月1日至2018年12月31日,共三年
  • 将股票池内股票按BP值从小到大排序并分为10组,组内等权分配资金,对每一组进行回测观察收益曲线。

示例代码如下:

code2 = '''
# 导入函数库
from jqdata import *
from jqfactor import *
import datetime as dt
import numpy as np
import pandas as pd
import time


# 初始化函数,设定基准等等
def initialize(context):
    # 设定沪深300作为基准
    set_benchmark('000300.XSHG')
    # 开启动态复权模式(真实价格)
    set_option('use_real_price', True)
    log.set_level('order', 'error')


    #第几组
    g.group = 1


    ## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
      # 开盘前运行
    run_monthly(before_market_open, monthday = 1, time='before_open', reference_security='000300.XSHG')
      # 开盘时运行
    run_monthly(market_open, monthday = 1, time='open', reference_security='000300.XSHG')


## 开盘前运行函数
def before_market_open(context):
    #设置滑点、手续费
    set_slip_fee(context)
    #取沪深300作为股票池
    all_stocks = get_index_stocks('000300.XSHG', date = context.current_dt)
    feasible_stocks = set_feasible_stocks(context, all_stocks)
    #查询市净率、代码
    q = query(valuation.pb_ratio, valuation.code).filter(valuation.code.in_(feasible_stocks))
    factor = get_fundamentals(q, context.current_dt)
    #整理df
    factor.index = factor['code']
    factor['BP'] = 1/factor['pb_ratio']
    del factor['pb_ratio'], factor['code']

    #去极值
    factor = winsorize(factor, scale = 3, axis = 0)
    #中性化
    factor = neutralize(factor, how = ['sw_l1', 'market_cap'], date = context.current_dt, axis = 0, fillna = 'sw_l1')
    #标准化
    factor = standardlize(factor, axis = 0)

    #排序
    factor.sort(columns = ['BP'], ascending = True, inplace = True)

    n = int(len(factor)/10)
    #分组取样
    if g.group == 10:
        g.tobuy_list = factor.index[(g.group - 1) * n :]
    else:
        g.tobuy_list = factor.index[(g.group - 1) * n : g.group * n]


#1
#设置可行股票池,剔除(金融类、)st、停牌股票,输入日期
def set_feasible_stocks(context,s):
    #s = get_index_stocks('000905.XSHG', date=context.current_dt)
    #print '输入股票个数为:%s'%len(s)
    all_stocks = s
    #得到是否停牌信息的dataframe,停牌得1,未停牌得0
    suspended_info_df = get_price(list(all_stocks), end_date = context.current_dt, count = 1, frequency = 'daily', fields = 'paused')['paused'].T
    #过滤未停牌股票 返回dataframe
    suspended_index = suspended_info_df.iloc[:,0] == 1
    #得到当日停牌股票的代码list:
    suspended_stocks = suspended_info_df[suspended_index].index.tolist()

    #剔除停牌股票
    for stock in suspended_stocks:
        if stock in all_stocks:
            all_stocks.remove(stock)

    #print '剔除后股票个数为:%s'%len(all_stocks)

    return all_stocks   



# 根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
    # 将滑点设置为0
    set_slippage(FixedSlippage(0)) 
    # 根据不同的时间段设置手续费
    dt=context.current_dt

    if dt>datetime.datetime(2013,1, 1):
        set_commission(PerTrade(buy_cost=0.0003, 
                                sell_cost=0.0013, 
                                min_cost=5)) 

    elif dt>datetime.datetime(2011,1, 1):
        set_commission(PerTrade(buy_cost=0.001, 
                                sell_cost=0.002, 
                                min_cost=5))

    elif dt>datetime.datetime(2009,1, 1):
        set_commission(PerTrade(buy_cost=0.002, 
                                sell_cost=0.003, 
                                min_cost=5))

    else:
        set_commission(PerTrade(buy_cost=0.003, 
                                sell_cost=0.004, 
                                min_cost=5))


## 开盘时运行函数
def market_open(context):
    #调仓,先卖出股票
    for stock in context.portfolio.long_positions:
        if stock not in g.tobuy_list:
            order_target_value(stock, 0)
    #再买入新股票
    total_value = context.portfolio.total_value # 获取总资产
    for i in range(len(g.tobuy_list)):
        value = total_value / len(g.tobuy_list) # 确定每个标的的权重

        order_target_value(g.tobuy_list[i], value) # 调整标的至目标权重

    #查看本期持仓股数
    print(len(context.portfolio.long_positions))



## 收盘后运行函数
def after_market_close(context):
    pass

'''


created_bt_ids = []
#分十组进行回测
for i in range(1, 11):
    algorithm_id = "d888d5a266b0f28f167d52a97a73dd31" #用自己的策略ID
    extra_vars = {'group': i}

    params = {
        "algorithm_id": algorithm_id,
        "start_date": "2016-01-01",
        "end_date": "2018-12-31",
        "frequency": "day",
        "initial_cash": "3000000",
        "initial_positions": None,
        "extras": extra_vars, 
        "name" : 'BP group:'   str(i) 
    }
    created_bt_ids.append(create_backtest(code = code2, **params))


import pandas as pd
#先获取基准收益和交易日信息
gt = get_backtest(backtest_id = created_bt_ids[0])
res = gt.get_results()
b_return = []
t = []
for r in res:
    b_return.append(r['benchmark_returns'])
    t.append(r['time'])

#建立df存储数据   
data = pd.DataFrame(index = t)
data['b_return'] = b_return

#填入不同参数下的收益数据
for i in range(len(created_bt_ids)):
    gt = get_backtest(backtest_id = created_bt_ids[i])
    res = gt.get_results()
    name = gt.get_params()['name'] 
    s_return = []
    for r in res:
        s_return.append(r['returns'])

    data[name] = s_return


data.plot(figsize = (15,10), colormap = 'Spectral')

BP分组.png

上方棕色线是基准线,蓝紫色线是第10组,对比这两条线能看出,在2017年上半年BP因子相对大盘能拉开明显的差距,而下半年差距被缩小至不复存在。2016年和2018年这两年两条线几乎不能拉开差距,说明这两年BP因子单独使用时获取超额收益效用不大。但是同时注意到下方两条线是第1组和第2组的收益曲线,说明BP因子多空差距明显,在2016年至2018年三年间即使不能相对大盘获得显著超额收益,也能防止空头出现的大幅回撤,控制风险。

另外,下方代码给出了十分组夏普比率(左)和最大回撤(右)情况,能明显看出第1组夏普比率和最大回撤都是最差的,而第10组都是相对较优的,多空组合间分化还是比较明显的。

sharpe = []
max_drawdown = []
name = range(1, 11)
for i in range(len(created_bt_ids)):
    gt = get_backtest(backtest_id = created_bt_ids[i])
    sharpe.append(gt.get_risk()['sharpe'])
    max_drawdown.append(gt.get_risk()['max_drawdown']) 

sharpe = pd.Series(sharpe, index = name)
max_drawdown = pd.Series(max_drawdown, index = name)

sharpe.plot.bar()

max_drawdown.plot.bar()

BP夏普.pngBP最大回撤.png

四、已有的量化课堂多回测框架推荐

实际上聚宽量化课堂已经为用户准备了泛用性更广、功能更强的多回测框架。该框架能实现诸如,需要运行超过10个回测时可自动操作、轻松实现遍历法以及优化数据可视化等等功能。对该框架感兴趣的同学建议拓展阅读:【量化课堂】多回测运行和参数分析框架

全部回复

0/140

量化课程

    移动端课程