我是Mr.看海,我在尝试用信号处理的知识积累和思考方式做量化交易,应用深度学习和AI实现股票自动交易,目的是实现财务自由~
目前我正在开发基于miniQMT的量化交易系统——看海量化交易系统(KhQuant 是其核心框架)。
一、 引言
大家好,我是 Mr.看海。上次跟同花顺 Supermind 对比完 KhQuant 之后,基本验证了khQuant的有效性,不过也留下了成交价应该保留几位小数的疑问。这次我们找来了聚宽,看看 KhQuant 在不同的平台上表现是不是一样稳健和准确,顺便解决小数位保留的问题。
策略还是老样子,用的双均线,参数(时间、本金、股票、手续费、滑点这些)我都调成跟上一篇一模一样。
不过呢,按理说一样的策略一样的参数,结果应该差不多吧?但我用聚宽跑第一次的时候,就发现一个有意思(或者说有点坑)的地方了。聚宽在调取函数全仓买入的时候,算钱的方式有点让人困惑——可能导致理论上花的钱超出了本金。
所以这篇文章,我就跟大家详细聊聊这次对比的过程:先展示一下最初的聚宽策略和它那个"透支"的问题,然后分析原因并给出"打补丁"的修正方法,接着介绍 KhQuant 是怎么处理的,最后再看看修正之后,KhQuant 和聚宽的回测结果到底对不对得上。当然,也会顺便分享一些我作为 KhQuant 开发者,从这次经历中得到的思考。
看海回测系统回测过程
二、 初步测试与问题暴露
我们继续沿用双移动平均线(DMA)策略,这个策略我们在上一篇文章中有详细介绍,核心逻辑就是 MA5 上穿 MA20 买入,下穿卖出。
2.1. 策略实现(初步问题版本)
在聚宽上实现这个策略,核心的 market_open
函数(每天开盘时执行)里的买卖逻辑大概是这样:
# --- 聚宽策略核心逻辑 (初步问题版本) ---
def market_open(context):
# ... (省略获取数据和计算MA5, MA20的代码) ...
# 获取持仓和现金信息
stocklist = list(context.portfolio.positions.keys())
cash = context.portfolio.available_cash
# 金叉且空仓时买入
if MA5 > MA20 and len(stocklist) == 0:
log.info("金叉信号,买入 %s" % (g.security))
# 【问题点】直接用所有可用现金下单
order_value(g.security, cash)
# 死叉且持仓时卖出
elif MA20 > MA5 and context.portfolio.positions[g.security].total_amount > 0:
log.info("死叉信号,卖出 %s" % (g.security))
order_target(g.security, 0)
# ... (省略其他日志代码) ...
(完整的初步代码见文末附录 7.1)
上面代码看着很简单直接,对吧?但问题就出在 order_value(g.security, cash)
这一句。
2.2. "资金透支"现象
当我把初始资金设为 100 万,运行上面这个初步版本的策略时,回测报告里的第一笔交易记录就让我皱起了眉头:
日期 | 时间 | 标的 | 交易类型 | 成交数量 | 成交价 | 成交额 | 手续费 |
---|---|---|---|---|---|---|---|
2024-04-30 | 09:30:00 | 华林证券(002945.XSHE) | 买入 | 85100 | 11.81 | 1,005,031.00 | 100.50 |
看到问题了吗?市价单买了 85100 股,成交价 11.81,光是成交额就已经 1,005,031 元了,超过了我的 100 万本金!这还没算 100.50 元的手续费呢。实际总花费是 1,005,131.50 元。
这就奇怪了,我明明只有 100 万,怎么能花出去超过 100 万的钱呢?虽然聚宽的回测系统似乎"容忍"了这种情况(交易记录显示成交了,但实际账户现金可能已经变成负数),但这显然不符合真实的交易规则。用这样的逻辑进行回测,得到的资金曲线、收益率等结果,其准确性就值得怀疑了。这样的问题,在上一篇对比的同花顺Supermind中是没有出现的。
三、 问题诊断与策略修正
为什么会出现这种情况呢?
3.1. 问题诊断
我推测原因在于聚宽的 order_value(security, cash)
函数的行为机制。这里的 cash
参数传入的是 context.portfolio.available_cash
,也就是当前账户立即可用的现金。聚宽系统在收到这个指令后,会根据这个 cash
值和当时的市价(或者某个参考价)来计算理论上能买的最大股数。
但关键在于,这个计算过程似乎没有充分预估并扣除交易中实际会产生的成本,尤其是:
-
滑点:我们设置了 1% 的滑点,这意味着实际买入成交价可能会比计算时参考的价格要高。
-
佣金:买入操作本身需要支付手续费。
order_value
函数可能只基于参考价和可用现金算出了一个目标股数,但没考虑到用这个股数、按可能因滑点而变高的价格成交、并且再支付一笔佣金之后,总花费会超过最初传入的 cash
值。
3.2. 策略"打补丁"
知道了问题所在,解决方案也就明确了:不能完全依赖 order_value
函数去自动处理"全仓买入"的资金计算。我们需要在策略层面自己控制,确保下单金额不会超过实际可用资金。
最简单直接的方法就是,在调用 order_value
之前,手动预留一部分比例的资金作为交易成本的缓冲。比如,我们预计滑点和手续费加起来可能最多消耗掉 0.5% 的资金(基于1%双边滑点和万一佣金估算),那我们就只用 99.5% 的可用现金去下单。
修正后的核心买入逻辑变成这样:
# --- 聚宽策略核心逻辑 (修正版本) ---
def market_open(context):
# ... (省略获取数据和计算MA5, MA20的代码) ...
# 获取持仓和现金信息
stocklist = list(context.portfolio.positions.keys())
cash = context.portfolio.available_cash
# 金叉且空仓时买入
if MA5 > MA20 and len(stocklist) == 0:
log.info("金叉信号,买入 %s" % (g.security))
# 【修正点】预留资金缓冲
# 使用 99.5% 的可用现金计算下单金额
adjusted_cash = cash * g.reserve_ratio # g.reserve_ratio 在 initialize 中设为 0.995
log.info("可用资金: {:.2f}, 预估下单金额: {:.2f}".format(cash, adjusted_cash))
# 用调整后的金额下单
order_value(g.security, adjusted_cash)
# 死叉且持仓时卖出 (逻辑不变)
elif MA20 > MA5 and context.portfolio.positions[g.security].total_amount > 0:
log.info("死叉信号,卖出 %s" % (g.security))
# 卖出所有股票,使这只股票的最终持有量为0
order_target(g.security, 0)
# ... (省略其他日志代码) ...
(完整的修正后代码见文末附录 7.2)
这种"打补丁"的方式虽然不是最精确的(最精确的做法是根据实时价格、费率、滑点估算最大可买股数,然后用 order
按股数下单),但它简单有效,能够解决 order_value
在这个场景下的问题,让我们得到一个更符合实际的回测结果。
四、 KhQuant 的实现方式
那么,在我自己开发的 KhQuant 框架中,是怎么处理类似情况的呢?
4.1. 内置成本计算与下单量控制
KhQuant 在设计时就考虑到了交易成本对实际可交易数量的影响。当策略发出买入或卖出信号时(比如通过 generate_signal
函数),框架底层在生成最终的交易指令前,会调用一个 calculate_max_buy_volume
(买入时) 或类似的函数 (卖出时)。
这个函数会严格根据当前的可用资金、股票价格、预设的滑点比例、佣金费率和印花税率(卖出时),反算出在确保总花费(或总收入扣除费用后)不超过/不少于限制的情况下,能够买入或卖出的最大整数股数(通常是 100 股的整数倍)。
核心的策略处理函数 khHandlebar
大概是这样调用信号生成逻辑的:
# --- KhQuant 策略核心逻辑 ---
def khHandlebar(data: Dict) -> List[Dict]:
# ... (省略获取数据和计算MA5, MA20的代码) ...
# 获取持仓信息
has_position = stock_code in data.get("__positions__", {}) # ...
# 金叉且空仓时买入
if ma_short > ma_long andnot has_position:
buy_reason = f"金叉信号: MA5({ma_short:.2f}) > MA20({ma_long:.2f})"
# generate_signal 内部会调用成本计算逻辑来确定实际下单量
signals = generate_signal(data, stock_code, current_price, 1.0, 'buy', buy_reason)
# 死叉且持仓时卖出
elif ma_short < ma_long and has_position:
sell_reason = f"死叉信号: MA5({ma_short:.2f}) < MA20({ma_long:.2f})"
signals = generate_signal(data, stock_code, current_price, 1.0, 'sell', sell_reason)
# ...
return signals
(完整的 KhQuant 代码见文末附录 7.3)
这种机制的好处是,策略开发者只需要表达交易意图(比如"全仓买入",即比例 1.0),而底层的框架会自动处理好所有繁琐但关键的成本计算和下单量限制,从源头上避免了"资金透支"这类问题,保证了回测的严谨性。
另外需要说明的是,根据上一篇文章中关于 A 股实际价格精度的讨论,以及为了本次与修正后的聚宽结果进行精确对比,我在这次 KhQuant 的回测中,将成交价的处理调整为保留两位小数,从而和聚宽的成交价精度保持一致(后续都采用两位小数的处理方式),这也是 KhQuant 作为本地框架灵活性的体现。
五、 修正后的结果对比
好了,现在我们有了修正后的聚宽策略(解决了 order_value
的问题)和调整了价格精度的 KhQuant 策略。接下来就是对比环节了!
5.1. 运行效率
简单说一下运行时间:
-
聚宽 (修正后): 在云端运行,耗时约 4 秒。
-
KhQuant: 在我的本地开发机上,耗时约 2 秒。
对于日线级别的策略,两者速度都很快。
5.2. 主要绩效指标
我们直接看数据对比表格:
指标 | 聚宽 JoinQuant (修正后) | KhQuant 结果 (价格两位小数) | 对比结果 & 说明 |
---|---|---|---|
总收益率 (%) | +15.45 | +15.45 | ✅完全一致 |
年化收益率 (%) | +29.24 | +29.24 | ✅完全一致 |
基准收益率 (%) | +8.64 | +8.64 | ✅完全一致 |
基准年化收益率 (%) | (未直接显示) | +15.95 | ✅推测一致 (聚宽基准收益率与 KhQuant 一致) |
最大回撤 (%) | 19.69 | 19.69 | ✅完全一致 |
夏普比率 (年化) | 0.743 | 0.82 | ❓差异较大 (存在差异,与无风险利率设置有关) |
索提诺比率 | 0.844 | 2.05 | ❓差异较大 (存在差异,与无风险利率设置有关) |
贝塔 | 1.179 | 1.19 | ✅比较接近 |
阿尔法 | 0.112 | 0.11 | ✅基本一致 (非常接近) |
年化波动率 (%) | (未直接显示) | 33.84 | ✅推测一致 (根据夏普和收益率推算应接近) |
胜率 (%) | 33.33 | 33.33 | ✅完全一致 |
盈亏比 | 3.002 | 3.00 | ✅基本一致 |
可以看到,像总收益率、年化收益率、基准收益率以及最大回撤这些衡量策略表现和核心风险的指标,两个平台的结果完全一致。这说明在对聚宽的下单逻辑进行修正,并且 KhQuant 也相应调整价格精度(两位小数)后,双方在模拟策略基础盈亏和主要风险特征上达到了高度的同步。这无疑是对 KhQuant 回测引擎核心准确性的一次有力印证。
一些衍生风险指标存在差别,比如夏普比率和索提诺比率。我查看聚宽文档后,发现其采取的无风险收益率为0.04,我们采用的是十年期国债收益率,而夏普比率和索提诺比率的公式与无风险收益率息息相关。关于夏普比率和索提诺比率的计算正确性在上一篇与同花顺的Supermind对比中已充分证明了。
5.3. 收益曲线
对比修正后的聚宽收益曲线和 KhQuant 的收益曲线,可以看到它们的整体形态、波动节奏几乎是一模一样的。
5.4. 交易记录
最后,我们来对一下交易流水这个"铁证":
聚宽 JoinQuant 交易记录 (修正后):
日期 | 时间 | 标的 | 交易类型 | 下单类型 | 成交数量 | 成交价 | 成交额 | 平仓盈亏 | 手续费 |
---|---|---|---|---|---|---|---|---|---|
2024-04-30 | 09:30:00 | 华林证券(002945.XSHE) | 买 | 市价单 | 84600 | 11.81 | 999,126.00 | 0.00 | 99.91 |
2024-05-22 | 09:30:00 | 华林证券(002945.XSHE) | 卖 | 市价单 | -84600 | 11.28 | -954,288.00 | -44,838.00 | 1,049.72 |
2024-07-22 | 09:30:00 | 华林证券(002945.XSHE) | 买 | 市价单 | 92200 | 10.34 | 953,348.00 | 0.00 | 95.33 |
2024-08-13 | 09:30:00 | 华林证券(002945.XSHE) | 卖 | 市价单 | -92200 | 9.97 | -919,234.00 | -34,114.00 | 1,011.16 |
2024-09-11 | 09:30:00 | 华林证券(002945.XSHE) | 买 | 市价单 | 95200 | 9.65 | 918,680.00 | 0.00 | 91.87 |
2024-10-30 | 09:30:00 | 华林证券(002945.XSHE) | 卖 | 市价单 | -95200 | 12.14 | -1,155,728.00 | 237,048.00 | 1,271.30 |
KhQuant 交易记录 (价格两位小数):
交易时间 | 证券代码 | 交易方向 | 成交价格 | 成交数量 | 成交金额 | 手续费 |
---|---|---|---|---|---|---|
2024-04-30 09:30:00 | 002945.SZ | 买入 | 11.81 | 84600 | 999,126.00 | 99.91 |
2024-05-22 09:30:00 | 002945.SZ | 卖出 | 11.28 | 84600 | 954,288.00 | 1049.72 |
2024-07-22 09:30:00 | 002945.SZ | 买入 | 10.34 | 92200 | 953,348.00 | 95.33 |
2024-08-13 09:30:00 | 002945.SZ | 卖出 | 9.97 | 92200 | 919,234.00 | 1011.16 |
2024-09-11 09:30:00 | 002945.SZ | 买入 | 9.65 | 95200 | 918,680.00 | 91.87 |
2024-10-30 09:30:00 | 002945.SZ | 卖出 | 12.14 | 95200 | 1,155,728.00 | 1271.30 |
分析: 每一笔交易的成交数量、成交价格、成交金额、手续费都完全一致。特别是第一笔买入,修正后的聚宽成交了 84600 股,花费 999,126.00 元 + 99.91 元 = 999,225.91 元,没有透支,这与 KhQuant 精确计算的结果完全吻合。
这次"像素级"的匹配,有力地证明了:
-
聚宽
order_value
的问题可以通过策略层面的修正来规避(虽然这种处理方式并不优雅)。 -
KhQuant 调整价格精度以匹配实际规则的能力得到了验证,通过与两个平台的对比验证了框架的正确性。
六、 总结与思考
这次 KhQuant 与聚宽的对比测试,虽然中间出了点小插曲,但最终结果令人满意,也很有启发。
此次遇到的问题再次提醒我们,不能迷信任何平台,尤其是涉及到钱的时候。写策略的时候一定要多留个心眼,对关键的函数最好做点测试验证,发现不对劲的地方要知道怎么在自己的代码里去修正,这样才能保证回测结果靠谱。
而 KhQuant 这次能和修正后的聚宽结果对得这么严丝合缝,也说明了我当初设计它的一些想法是对路的。比如,在框架底层就内置了精确的成本计算和下单量控制逻辑,这样用户写策略时就不用太操心这些细节。这种把复杂但关键的细节封装在底层,同时保证核心逻辑透明可控,再加上跟 Python 生态结合的便利性,用户遇到bug也可自由自主处理修正,就是我做 KhQuant 想要达到的效果。
我的目标是打造一个真正让使用者放心、用得顺手、能够支撑复杂 AI 策略研究的量化工具。如果你对 KhQuant 感兴趣,或者在量化道路上有什么想法,都欢迎继续关注我的公众号"看海的城堡",我们一起交流,共同进步!
七、 下一步计划与内测说明
这次与聚宽的对比,连同上次与 Supermind 的验证,进一步增强了我对 KhQuant 核心准确性的信心。但这只是阶段性的成果。
要打造一个真正成熟可靠的量化系统,路还很长。我的下一步计划主要有:
-
扩展策略库验证: 会用更多样化的策略来测试 KhQuant,确保它在不同逻辑下的表现。
-
功能迭代与优化: 根据测试中发现的细节问题(比如性能瓶颈、易用性等)和大家的反馈,持续改进 KhQuant。
-
集成Deepseek等大语言模型:考虑将大语言模型内置到软件中,轻松实现策略生成和调整,为没有编程基础和编译环境的用户提供便利。(这个功能会在初版软件发布后再后优化添加)
关于内测:
在完成更充分的测试验证后,我计划启动"看海量化交易系统 (KhQuant)"的小范围 Beta 内测。
-
优先通道: 为了感谢一直以来支持我的朋友,特别是通过我公众号"看海的城堡"推荐渠道开通 MiniQMT 账户的朋友们,我会优先邀请你们参与内测,提前体验并反馈宝贵意见。
-
最终会公开: 也请其他朋友放心,内测只是为了打磨产品。最终软件会公开发布,核心框架代码也计划开源,让所有人都能用上并参与进来。
我的目标是做一款真正好用、开放的工具,严格的测试和大家的反馈是成功的关键。
八、 关于开通 MiniQMT
MiniQMT 是什么?
简单说,它是迅投(QMT)交易系统提供的一个编程接口(API),很多券商都在用 QMT。通过 MiniQMT,我们可以用 Python 代码连接到券商服务器,查行情、跑策略、下单交易。
对于量化交易者来说,它是一个常用且通常免费(可能需要满足券商一定的资产要求)的实盘交易接口,稳定性和速度都不错。
KhQuant 和 MiniQMT 的关系
我开发的"看海量化交易系统 (KhQuant)"就是基于 MiniQMT 接口来连接真实券商账户进行交易的。所以,如果你想用 KhQuant 进行实盘交易(或者未来可能的回测数据获取),就需要一个 MiniQMT 账户。
如何开通?
如果你还没有 MiniQMT 账户,又对 KhQuant 感兴趣,或者想支持一下我的开发工作,可以关注我的公众号"看海的城堡",在公众号页面下方有开通 MiniQMT 的指引。
走推荐渠道开户不是必须的,但对我来说是一种鼓励和支持,也能确保你开的账户类型和 KhQuant 是兼容的。再次感谢大家的关注!
九、 免责声明
本文所有内容仅供学习和技术交流使用,不构成任何投资建议。所述策略及回测结果仅为历史数据模拟,不代表未来实际表现。平台特性分析基于当前观察,可能随平台更新而变化。投资者据此操作,风险自担。
十、 附录:完整策略代码
10.1. 聚宽策略代码 (初步问题版本)
# 导入函数库
from jqdata import *
# 初始化函数,设定基准等等
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 输出内容到日志 log.info()
log.info('初始函数开始运行且全局只运行一次')
# 过滤掉order系列API产生的比error级别低的log
# log.set_level('order', 'error')
### 股票相关设定 ###
# 股票类每笔交易时的手续费是:买入时佣金万分之一,卖出时佣金万分之一加千分之一印花税, 每笔交易佣金最低扣5块钱
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0001, close_commission=0.0001, min_commission=5), type='stock')
# 为股票设定滑点为百分比滑点
set_slippage(PriceRelatedSlippage(0.01),type='stock') # 滑点设置为 1%
## 运行函数
run_daily(before_market_open, time='before_open', reference_security='000300.XSHG')
run_daily(market_open, time='open', reference_security='000300.XSHG')
run_daily(after_market_close, time='after_close', reference_security='000300.XSHG')
## 开盘前运行函数
def before_market_open(context):
# 输出运行时间
log.info('函数运行时间(before_market_open):'+str(context.current_dt.time()))
# 要操作的股票:华林证券
g.security = '002945.XSHE'# 注意聚宽代码后缀为 .XSHE
## 开盘时运行函数
def market_open(context):
log.info('函数运行时间(market_open):'+str(context.current_dt.time()))
security = g.security
# 获取股票的收盘价数据 (聚宽默认获取前复权数据)
close_data20 = get_bars(security, count=20, unit='1d', fields=['close'])
close_data5 = get_bars(security, count=5, unit='1d', fields=['close'])
# 计算均线
MA20 = close_data20['close'].mean()
MA5 = close_data5['close'].mean()
log.info("5日均线: {:.2f}, 20日均线: {:.2f}".format(MA5, MA20))
# 获取当前账户当前持仓市值
market_value = sum(position.value for position in context.portfolio.positions.values())
# 获取账户持仓股票列表
stocklist = list(context.portfolio.positions.keys())
# 取得当前的可用现金
cash = context.portfolio.available_cash
# 如果5日均线大于20日均线,且账户当前无持仓,则全仓买入股票
if MA5 > MA20 and len(stocklist) == 0:
# 记录这次买入
log.info("5日均线大于20日均线, 买入 %s" % (g.security))
# 【问题点】用所有 available_cash 买入股票
order_value(security, cash) # 使用可用现金下单
# 如果5日均线小于20日均线,且账户当前有股票市值,则清仓股票
# 修正:检查持仓是否存在且大于0
elif MA20 > MA5 and g.security in context.portfolio.positions and context.portfolio.positions[g.security].total_amount > 0:
# 记录这次卖出
log.info("5日均线小于20日均线, 卖出 %s" % (g.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('##############################################################')
10.2. 聚宽策略代码 (修正后版本)
# 导入函数库
from jqdata import *
# 初始化函数,设定基准等等
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 输出内容到日志 log.info()
log.info('初始函数开始运行且全局只运行一次')
# 过滤掉order系列API产生的比error级别低的log
# log.set_level('order', 'error')
### 股票相关设定 ###
# 股票类每笔交易时的手续费是:买入时佣金万分之一,卖出时佣金万分之一加千分之一印花税, 每笔交易佣金最低扣5块钱
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0001, close_commission=0.0001, min_commission=5), type='stock')
# 为股票设定滑点为百分比滑点
set_slippage(PriceRelatedSlippage(0.01),type='stock') # 滑点设置为 1%
# 全局变量,预留资金比例系数(用于应对滑点和手续费)
# 稍微预留一点资金,比如0.5%,避免因滑点和手续费导致理论花费超过可用现金
g.reserve_ratio = 0.995
## 运行函数
run_daily(before_market_open, time='before_open', reference_security='000300.XSHG')
run_daily(market_open, time='open', reference_security='000300.XSHG')
run_daily(after_market_close, time='after_close', reference_security='000300.XSHG')
## 开盘前运行函数
def before_market_open(context):
# 输出运行时间
log.info('函数运行时间(before_market_open):'+str(context.current_dt.time()))
# 要操作的股票:华林证券
g.security = '002945.XSHE'# 注意聚宽代码后缀为 .XSHE
## 开盘时运行函数
def market_open(context):
log.info('函数运行时间(market_open):'+str(context.current_dt.time()))
security = g.security
# 获取股票的收盘价数据 (聚宽默认获取前复权数据)
close_data20 = get_bars(security, count=20, unit='1d', fields=['close'])
close_data5 = get_bars(security, count=5, unit='1d', fields=['close'])
# 计算均线
MA20 = close_data20['close'].mean()
MA5 = close_data5['close'].mean()
log.info("5日均线: {:.2f}, 20日均线: {:.2f}".format(MA5, MA20))
# 获取当前账户当前持仓市值
# 修正:使用更健壮的方式检查持仓
position = context.portfolio.positions.get(security)
market_value = position.value if position else0
has_position = position isnotNoneand position.total_amount > 0
# 取得当前的可用现金
cash = context.portfolio.available_cash
# 如果5日均线大于20日均线,且账户当前无持仓,则全仓买入股票
if MA5 > MA20 andnot has_position:
# 记录这次买入
log.info("5日均线大于20日均线, 买入 %s" % (g.security))
# 修改点:预留部分资金作为交易成本缓冲,避免透支
# 使用调整后的比例系数 (0.995) 来计算下单金额
adjusted_cash = cash * g.reserve_ratio
log.info("可用资金: {:.2f}, 预估下单金额: {:.2f}".format(cash, adjusted_cash))
# 用调整后的资金金额下单
order_value(security, adjusted_cash)
# 如果5日均线小于20日均线,且账户当前有股票市值,则清仓股票
elif MA20 > MA5 and has_position:
# 记录这次卖出
log.info("5日均线小于20日均线, 卖出 %s" % (g.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('##############################################################')
10.3. KhQuant 策略代码 (ths.py
)
# coding: utf-8
from typing import Dict, List
import numpy as np
from xtquant import xtdata
import datetime
import logging
import os
import json
# 从 khQTTools 导入信号生成等辅助函数
# 注意:实际使用时需要确保 khQTTools.py 在 Python Path 中
# 或者将其中的函数直接复制到本文件
try:
from khQTTools import generate_signal, calculate_max_buy_volume
except ImportError:
# 如果无法导入,定义占位函数或直接实现简单版本
logging.warning("khQTTools not found. Using placeholder functions.")
def generate_signal(data, stock_code, current_price, target_percent, action, reason):
# 这是一个简化的占位符,实际的 generate_signal 会更复杂
# 它需要计算目标仓位、当前仓位、可用资金等
# 并最终返回包含交易指令的字典列表
logging.info(f"Placeholder generate_signal called for {action} {stock_code}")
# 实际实现中需要调用 calculate_max_buy_volume 等
# 这里仅作示意,不产生实际信号
return []
def calculate_max_buy_volume(available_cash, price, commission_rate, slippage_rate, min_commission):
# 这是一个简化的占位符
logging.info("Placeholder calculate_max_buy_volume called.")
# 粗略估算,不考虑最小手数和资金限制
estimated_cost_per_share = price * (1 + slippage_rate) * (1 + commission_rate)
if estimated_cost_per_share == 0: return0
max_volume = int(available_cash / estimated_cost_per_share)
# 实际还需考虑最小佣金和手数对计算的影响
return max(0, max_volume // 100 * 100) # 假设最小单位100股
# 全局变量
position = {} # 仅用于记录操作 (实际KhQuant框架会管理真实持仓)
params = {}
risk_params = {}
stock_list = []
def init(stocks=None, data=None):
"""策略初始化"""
global position, params, risk_params, stock_list
# 初始化持仓记录 (仅为示例,实际由框架管理)
position = {}
stock_code = stocks[0] if stocks else'002945.SZ'
stock_list = [stock_code]
# 初始化策略参数
params = {
"short_ma_period": 5, # 短期均线周期
"long_ma_period": 20, # 长期均线周期
"stock_code": stock_code # 交易标的
}
# 初始化风控参数 (简化示例)
risk_params = {
"max_position": 1.0# 最大持仓比例(全仓)
}
logging.info(f"策略初始化完成,交易标的:{stock_code}")
def calculate_ma(code: str, short_period: int, long_period: int, current_date: str = None) -> tuple:
"""计算移动平均线"""
try:
# 确保日期格式为 YYYYMMDD
end_date_formatted = current_date.replace('-', '')
# 获取足够长的历史数据以计算最长均线
# 注意:这里的 start_time 需要足够早,或者根据 long_period 动态计算
history_data = xtdata.get_market_data(
field_list=["close"],
stock_list=[code],
period="1d",
count=long_period + 1, # 获取 long_period + 1 天的数据
end_time=end_date_formatted,
dividend_type='front',
fill_data=True# 确保数据填充
)
# 检查返回的数据是否足够
closes_series = history_data['close']
if code notin closes_series or len(closes_series[code]) < long_period + 1:
logging.warning(f"获取 {code} 在 {end_date_formatted} 前的历史数据不足 {long_period + 1} 条")
returnNone, None
# 获取收盘价列表,排除最后一天(当天)的数据
closes = closes_series[code][:-1]
# 再次检查数据长度
if len(closes) < long_period:
logging.warning(f"排除当天后,{code} 在 {end_date_formatted} 前的历史数据不足 {long_period} 条")
returnNone, None
# 计算均线
ma_long = round(np.mean(closes[-long_period:]), 2)
ma_short = round(np.mean(closes[-short_period:]), 2)
return ma_short, ma_long
except Exception as e:
logging.error(f"计算 {code} 均线时发生错误 ({end_date_formatted}): {str(e)}")
returnNone, None
def khHandlebar(data: Dict) -> List[Dict]:
"""策略主逻辑,在每个K线或Tick数据到来时执行"""
signals = []
# 获取当前时间信息
current_time = data.get("__current_time__", {})
current_date_str = current_time.get("date", "")
ifnot current_date_str:
logging.error("无法获取当前日期")
return signals
# 获取股票代码
stock_code = params["stock_code"]
# 获取当前价格 (假设为收盘价,实际应根据数据类型获取)
stock_data = data.get(stock_code, {})
current_price = stock_data.get("close", 0)
if current_price == 0:
# 尝试获取昨收价或其他参考价
current_price = stock_data.get("lastPrice", 0)
if current_price == 0:
logging.warning(f"无法获取 {stock_code} 在 {current_date_str} 的有效价格")
# return signals # 或者使用昨日收盘价等
# 计算均线
ma_short, ma_long = calculate_ma(
stock_code,
params["short_ma_period"],
params["long_ma_period"],
current_date_str # 使用 YYYY-MM-DD 格式
)
# 如果均线计算失败,则不产生信号
if ma_short isNoneor ma_long isNone:
logging.warning(f"日期 {current_date_str}, 标的 {stock_code}: 均线计算失败,跳过交易逻辑")
return signals
logging.info(f"日期 {current_date_str}, 标的 {stock_code}: MA5={ma_short:.2f}, MA20={ma_long:.2f}")
# 获取持仓信息 (实际由 KhQuant 框架传入)
positions_info = data.get("__positions__", {})
has_position = stock_code in positions_info and positions_info[stock_code].get("volume", 0) > 0
# 交易逻辑: 使用 generate_signal 生成信号
if ma_short > ma_long andnot has_position:
# 金叉且无持仓,生成全仓买入信号
buy_reason = f"金叉信号: MA5({ma_short:.2f}) > MA20({ma_long:.2f})"
# generate_signal 内部会调用 calculate_max_buy_volume 确保资金充足
signals = generate_signal(data, stock_code, current_price, 1.0, 'buy', buy_reason)
if signals: logging.info(f"生成买入信号: {buy_reason}")
elif ma_short < ma_long and has_position:
# 死叉且有持仓,生成全仓卖出信号
sell_reason = f"死叉信号: MA5({ma_short:.2f}) < MA20({ma_long:.2f})"
# generate_signal 内部会计算卖出数量 (全仓即卖出所有持仓)
signals = generate_signal(data, stock_code, current_price, 1.0, 'sell', sell_reason)
if signals: logging.info(f"生成卖出信号: {sell_reason}")
return signals
# KhQuant 通常还需要 khPreMarket, khPostMarket 等回调函数,这里省略
相关文章
【深度学习量化交易1】一个金融小白尝试量化交易的设想、畅享和遐想
【深度学习量化交易2】财务自由第一步,三个多月的尝试,找到了最合适我的量化交易路径
【深度学习量化交易3】为了轻松免费地下载股票历史数据,我开发完成了可视化的数据下载模块
【深度学习量化交易4】 量化交易历史数据清洗——为后续分析扫清障碍
【深度学习量化交易6】优化改造基于miniQMT的量化交易软件,已开放下载~(已完成数据下载、数据清洗、可视化模块)
【深度学习量化交易7】miniQMT快速上手教程案例集——使用xtQuant进行历史数据下载篇
【深度学习量化交易8】miniQMT快速上手教程案例集——使用xtQuant进行获取实时行情数据篇
【深度学习量化交易9】miniQMT快速上手教程案例集——使用xtQuant获取基本面数据篇
【深度学习量化交易10】miniQMT快速上手教程案例集——使用xtQuant获取板块及成分股数据篇
【深度学习量化交易11】miniQMT快速上手教程——使用XtQuant进行实盘交易篇(八千字超详细版本)
【深度学习量化交易12】基于miniQMT的量化交易框架总体构建思路——回测、模拟、实盘通吃的系统架构
【深度学习量化交易13】继续优化改造基于miniQMT的量化交易软件,增加补充数据功能,优化免费下载数据模块体验!
【深度学习量化交易14】正式开源!看海量化交易系统——基于miniQMT的量化交易软件
【深度学习量化交易15】基于miniQMT的量化交易回测系统已基本构建完成!AI炒股的框架初步实现
【深度学习量化交易17】触发机制设置——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易18】盘前盘后回调机制设计与实现——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易19】行情数据获取方式比测(1)——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易20】量化交易策略评价指标全解析——基于miniQMT的量化交易回测系统开发实记
【深度学习量化交易21】行情数据获取方式比测(2)——基于miniQMT的量化交易回测系统开发实记
【AI量化第22篇】如何轻松看懂回测结果——基于miniQMT的量化交易回测系统开发实记
【AI量化第23篇】数据下载/补充模块升级,并与回测系统正式集成——基于miniQMT的量化交易回测系统开发实记
【AI量化第24篇】KhQuant 策略框架深度解析:让策略开发回归本质——基于miniQMT的量化交易回测系统开发实记