# coding :utf-8
#
# The MIT License (MIT)
#
# Copyright (c) 2021-2029 XuHaiJiang/QFF
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import importlib.util
import inspect
from functools import partial
from datetime import datetime
from typing import Optional, Callable
from qff.tools.logs import log
from qff.tools.date import is_trade_day, get_real_trade_date, util_date_valid, get_pre_trade_day
from qff.frame.context import context, strategy
from qff.frame.portfolio import Portfolio
from qff.frame.const import RUN_TYPE
from qff.frame.backtest import back_test_run
from qff.frame.simtrade import sim_trade_run
from qff.price.cache import ContextData
__all__ = ['set_benchmark', 'set_order_cost', 'set_slippage', 'run_daily', 'run_file',
'set_universe', 'pass_today']
context_data = ContextData()
def _getattr(m, func_name):
try:
func = getattr(m, func_name)
except AttributeError:
func = None
return func
def _wrap_strategy_func(func_obj, include_data=False):
if func_obj is not None:
spec = inspect.getfullargspec(func_obj).args
if include_data:
if len(spec) != 2 or spec[0] != 'context' or spec[1] != 'data':
raise ValueError(f'策略函数定义的参数不正确!')
return partial(func_obj, context, context_data)
else:
if len(spec) != 1 or spec[0] != 'context':
raise ValueError(f'策略函数定义的参数不正确!')
return partial(func_obj, context)
else:
return None
def _load_strategy_file(path):
"""
装载策略文件
:param path: 策略文件的路径
:return (boolean): 返回加载是否成功
"""
# 1、导入策略文件
try:
spec = importlib.util.spec_from_file_location('strategy', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
except Exception as e:
log.error('策略文件导入失败!{}'.format(e))
log.error('策略文件路径!{}'.format(path))
return False
# 2、给strategy策略对象赋函数指针
strategy.initialize = _wrap_strategy_func(_getattr(module, 'initialize'))
strategy.before_trading_start = _wrap_strategy_func(_getattr(module, 'before_trading_start'))
strategy.handle_data = _wrap_strategy_func(_getattr(module, 'handle_data'), include_data=True)
strategy.after_trading_end = _wrap_strategy_func(_getattr(module, 'after_trading_end'))
strategy.on_strategy_end = _wrap_strategy_func(_getattr(module, 'on_strategy_end'))
strategy.process_initialize = _wrap_strategy_func(_getattr(module, 'process_initialize'))
if strategy.initialize is None:
log.error("***策略文件缺少初始化函数initialize!***")
return False
return True
[文档]def run_daily(func, run_time="every_bar", append=True):
# type: (Callable, str,bool) -> None
"""
设置定时运行的策略函数。**回测环境/模拟专用API**
指定每天要运行的函数, 可以在具体交易日的某一分钟执行。
:param func: 一个自定义的函数,此函数必须接受context参数。例如自定义函数名 :code:`market_open(context)`。
:param run_time: 具体执行时间,一个字符串格式的时间:
- 交易时间内的任意时间点,上午“09:30-11:30”, 下午“13:01-15:00”,例如"10:00", "14:00";
- **every_bar**,运行时间和您设置的运行频率一致,按天会在交易日的开盘时调用一次,按分钟会在交易时间每分钟运行。执行后将替代handle_data()策略框架函数.
- **before_open**,设置func在开盘前运行,执行后将替代before_trading_start()策略框架函数.
- **after_close**,设置func在收盘后运行,执行后将替代after_trading_end()策略框架函数.
:param append: 如果run_time已注册运行函数,新函数是在原函数前面运行还是后面运行。append=True: 则新函数是在原函数后面运行。
.. note::
一个策略中尽量不要同时使用run_daily和handle_data,更不能使用run_daily(handle_data, "xx:xx")
run_daily中的函数只能有一个参数context,具体示例如下:
:example:
::
def initialize(context):
run_daily(func, run_time='10:00')
def func(context):
print(context.current_dt)
print('-'*50)
# 参数 func 必须是一个全局的函数, 不能是类的成员函数, 并且func是可重入函数,即可重复调用。
"""
def register_strategy_func(strategy_func, _func, _append=True):
obj = strategy_func
if obj is None:
obj = _func
elif callable(obj):
obj = [obj, _func] if _append else [_func, obj]
elif isinstance(obj, list):
if _append:
obj.append(_func)
else:
obj.insert(0, _func)
return obj
log.debug('调用run_daily' + str(locals()).replace('{', '(').replace('}', ')'))
if not callable(func):
raise ValueError("run_daily函数输入的func参数不是函数对象")
func = _wrap_strategy_func(func, run_time == "every_bar")
if run_time == "before_open":
strategy.before_trading_start = register_strategy_func(strategy.before_trading_start, func, append)
elif run_time == "after_close":
strategy.after_trading_end = register_strategy_func(strategy.after_trading_end, func, append)
elif run_time == "every_bar":
strategy.handle_data = register_strategy_func(strategy.handle_data, func, append)
else:
try:
datetime.strptime(run_time, '%H:%M')
if run_time < '09:30' or run_time > '15:00' or ('11:30' < run_time < '13:30'):
raise ValueError
if run_time + ':00' not in strategy.run_daily.keys():
strategy.run_daily[run_time + ':00'] = func # 加上秒是为了防止在tick策略中多次执行
else:
strategy.run_daily[run_time + ':00'] = \
register_strategy_func(strategy.run_daily[run_time + ':00'], func, append)
except ValueError:
log.error("run_daily函数输入的run_time参数不合法")
return
return
def _set_backtest_period(start=None, end=None):
"""
设置回测周期开始时间和结束时间,默认最近60天数据
:param start: 回测开始日期
:param end: 回测结束日期
:return: None
"""
if end is None:
end = datetime.now().strftime('%Y-%m-%d')
if not is_trade_day(end):
end = get_real_trade_date(end)
context.end_date = get_pre_trade_day(end, 1)
elif util_date_valid(end):
context.end_date = end if is_trade_day(end) \
else get_real_trade_date(end, towards=-1)
else:
print('set_backtest_period函数参数日期格式设置错误!')
return ValueError
if start is None:
context.start_date = get_pre_trade_day(end, 60)
elif util_date_valid(start):
context.start_date = start if is_trade_day(start) \
else get_real_trade_date(start, towards=1)
else:
print('set_backtest_period函数参数日期格式设置错误!')
raise ValueError
if context.start_date >= context.end_date:
print("回测日期参数设置错误!")
raise ValueError
context.current_dt = context.start_date + " 09:00:00" # 必需要设置,回测以该时间作为启动日期
[文档]def set_order_cost(open_tax=0,
close_tax=0.001,
open_commission=0.0002,
close_commission=0.0002,
min_commission=5):
# type: (float, float, float, float, float) -> None
"""
设置佣金和印花税率。
指定每笔交易要收取的手续费, 系统会根据用户指定的费率计算每笔交易的手续费
:param open_tax: 买入时印花税,默认值为0
:param close_tax: 卖出时印花税,默认值为千分之一
:param open_commission: 买入时佣金,默认值为万分之二
:param close_commission: 卖出时佣金,默认值为万分之二
:param min_commission: 最低佣金,不包含印花税,默认值为5
"""
context.trade_cost.open_tax = open_tax
context.trade_cost.close_tax = close_tax
context.trade_cost.open_commission = open_commission
context.trade_cost.close_commission = close_commission
context.trade_cost.min_commission = min_commission
[文档]def set_benchmark(security: str):
"""
设置基准
默认我们选定了沪深300指数的每日价格作为判断您策略好坏和一系列风险值计算的基准.
您也可以使用set_benchmark指定其他指数
:param security: 指数基准
:return: None
"""
context.benchmark = security
return
[文档]def set_slippage(slippage=0.00246):
# type: (float) -> None
"""
设置固定滑点
当您下单后, 真实的成交价格与下单时预期的价格总会有一定偏差, 因此我们加入了滑点模式来帮您更好的模拟真实市场的表现. 我们暂时只支持固定滑点。同时,我们也支持为交易品种和特定的交易标的设置滑点。
:param slippage: 固定滑点值,默认0.00246
"""
context.slippage = slippage
return
# context.universe = ['000001', '601567', '000166', '601636'] 测试使用
[文档]def set_universe(security_list):
# type: (list) -> None
"""
设定股票值(history函数专用)
设置或者更新此策略要操作的股票池 context.universe. 请注意:
**该函数现在只用于设定history函数的默认security_list, 除此之外并无其他用处。**
:param security_list: 证券标的列表
"""
if isinstance(security_list, str):
security_list = [security_list]
context.universe = security_list
[文档]def pass_today() -> None:
"""
跳过当日(回测专用)
在分钟执行策略中,调用此函数可用跳过当日剩余的每分钟策略运行,以提高回测效率。
"""
if context.run_type == RUN_TYPE.BACK_TEST:
context.pass_today = True
return
[文档]def run_file(strategy_file: str,
run_type: str = 'bt',
resume: bool = False,
freq: str = 'day',
cash: int = 1000000,
start: Optional[str] = None,
end: Optional[str] = None,
name: Optional[str] = None,
output_dir: Optional[str] = None,
log_level: str = 'info',
trace: bool = False):
"""
运行策略文件,并初始化环境参数。
:param strategy_file: 待运行策略文件路径
:param run_type: 指定策略运行方式。bt-回测; sim-实盘模拟, 默认值为bt
:param resume: 是否执行恢复运行, True-恢复以前的策略执行,False-重新开始执行策略,默认False.
:param freq: 策略执行频率,有效值为 'day','min','tick',默认值为 'day'
:param cash: 账户初始资金,默认值1000000
:param start: 回测开始日期,默认为结束日期前60个交易日
:param end: 回测结束日期, 默认为上一个交易日
:param name: 策略名称
:param output_dir: 指定结果数据输出目录
:param log_level: 控制台日志输出的级别, 有效值为 'debug', 'info', 'warning', 'error',默认值为‘info’
:param trace: 策略运行过程中是否进行交互,模拟交易时自动有效
:return: None
:example:
使用方法:一般在策略文件中的尾部加入以下代码
.. code-block:: python
if __name__ == '__main__':
run_file(__file__, 0, start='2022-06-01', end='2022-08-31')
"""
log.set_level(log_level)
log.debug('调用run_file' + str(locals()).replace('{', '(').replace('}', ')'))
if not _load_strategy_file(strategy_file):
print("输入的策略文件路径加载失败!")
return
context.strategy_file = strategy_file
if resume:
if strategy.process_initialize is not None:
strategy.process_initialize()
if context.run_type == RUN_TYPE.BACK_TEST:
back_test_run(trace)
else:
sim_trade_run()
else:
context.log_file = log.file_name
strategy.initialize()
if freq in ['day', 'min', 'tick']:
context.run_freq = freq
else:
print('参数freq运行频率设置错误!')
return
context.portfolio = Portfolio(cash)
if name is None:
name = os.path.basename(strategy_file).split('.')[0]
context.strategy_name = name
if output_dir is not None:
if os.path.exists(output_dir):
context.output_dir = output_dir
else:
print("output_dir参数指定的目录不存在!")
return
context.run_start = datetime.now()
if run_type == 'bt':
_set_backtest_period(start, end)
back_test_run(trace)
elif run_type == 'sim':
sim_trade_run()
else:
log.error('输入的参数run_type错误!')