VeighNa量化社区
你的开源社区量化交易平台 | vn.py | vnpy
  游客无法访问帖子内容,请登录后访问。
Member
avatar
加入于:
帖子: 12
声望: 4

[知乎专栏经验发布] (https://www.zhihu.com/column/c_1760768090802171904)


距离上一篇”完结”过去了快一年,本来以为这个系列就这样了。

然后 AI 来了。

不是那种”帮你补全几行代码”的 AI,而是真的能从零开始、一个文件一个文件帮你写出来的 AI。看着 Claude 一步步理解我的需求,读懂 VNPY 的源码,处理 PySide6 的信号槽、QSS 样式、Pydantic 数据模型……说实话,有点震撼。

于是趁着春节前的这段时间,我做了一个实验:用 AI 辅助,从 VNPY 3.9 重构到 4.3,看看能走多远。

结果是:大约一周时间,从项目搭建到功能完善,我没有手写一行代码。所有的代码都是在和 AI 的对话中产生的——我负责提需求、审代码、做决策,AI 负责实现。

为什么要重构
VNPY 从 3.9 到 4.3 变化很大,核心模块拆分成了独立包(vnpy-ctastrategy、vnpy-ctp 等),架构上差异太多,简单迁移行不通,不如重新来过。

正好,V1 积累了不少想法但一直没动手改的地方,这次借 AI 之手一并实现了。

V2 主要变化
变化 说明
基础框架 VNPY 3.9 → 4.3
数据库 SQLite/MySQL → ArcticDB (LMDB)
策略架构 三层数据模型 Params/State/Vars
双模交易 同一策略支持全自动 + 辅助半自动
多账户 实盘/模拟/7×24 环境并存
主力合约 自动识别 + 换月持仓保护
AI 助手 实验性接入,目标是策略信号辅助
界面 PySide6 + QFluentWidgets 全面重写
详细内容就不在这里展开了,感兴趣的朋友请移步 GitHub 仓库查看 README。

开源
项目已开源,MIT 协议,可自由使用和修改:

GitHub:https://github.com/48645970/guanlan

欢迎 Star,欢迎提 Issue,欢迎交流。

感谢
感谢 VNPY 团队提供了优秀的量化交易框架,让这一切有了基础。

感谢 Claude —— 这个项目的”联合开发者”。一周时间里,从框架搭建到细节打磨,每一行代码都经过了我们之间的对话。AI 不会疲倦,不会敷衍,能记住上下文,能读懂源码,能理解”这里不太对”是什么意思。这种体验,一年前完全无法想象。

我们正站在一个很有意思的时代节点上。

最后
祝大家新年快乐,交易顺利。

主窗口
description

多账户管理,自动登录

description

合约管理,收藏,手续费设置(策略中算手续费),定期从外部获取后计算最大持仓定为主力

description

交易持仓
description

绩效查看,按账户统计
description

数据管理,一键批量导入通达信数据(日线、分钟线)
description

策略执行
使用数据模拟定义参数和状态显示,配合限制输入框,对输入内容格式、最大最小值做限制
界面参数、状态值,使用模型指定的中文标题显示
Params、State 显示在窗口
Variable 仅做为中间变量存储,但不显示
扩展 on_ready,在在 on_start 后触发,不同的是,交易状态 self.trading == True
扩展 on_reset,在画面点击重置时触发

description

description

description

Administrator
avatar
加入于:
帖子: 4583
声望: 337

很漂亮啊,是用QFluentWidgets开发的吗?帮你加个精华

Member
avatar
加入于:
帖子: 12
声望: 4

用Python的交易员 wrote:

很漂亮啊,是用QFluentWidgets开发的吗?帮你加个精华
是的,一眼就认出来了

Member
avatar
加入于:
帖子: 1
声望: 0

很好

Member
avatar
加入于:
帖子: 15
声望: 1

非常赞 能分享吗?谢谢

Member
avatar
加入于:
帖子: 2026
声望: 156

海外社区那边也有用户开发了一套基于QFluentWidgets的版本,有兴趣的同学可以参考看看:https://github.com/veighna-global/vnpy_evo

Member
avatar
加入于:
帖子: 12
声望: 4

其实美化VNPY比较简单,可以看另外一个 批量导入通达信分钟数据

基本上,就是把VNPY下载回来,找到每一个模块的ui目录,把里面的代码复制过来
然后把 QWidgets.Q**,改为成QFluentWidgets.**控件代替

可以独立使用的像data_recorder,data_manager,可以独立出来,用的时候优先找编辑好的版本,没有的话运行py

def run_modules(name: str):

    exe = f"{name}.exe"
    py = f"{name}.py"

    if os.path.exists(exe):

        try:
            os.startfile(exe)
        except FileNotFoundError as err:
            infobar.error(title="启动错误", content=f"{err.strerror}", duration=-1)

    elif os.path.exists(py):

        shell = ["python", py]
        subprocess.Popen(shell, shell=True)
Member
avatar
加入于:
帖子: 12
声望: 4

数据记录模块,也是比较容易单拎出来的
我的逻辑是:账户里面找标识为行情的号,再加入标记为收藏的品种
增加了关闭窗口隐藏,托盘图标后台记录


import sys
from datetime import datetime
from typing import Any, List

from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon, QAction
from PySide6.QtWidgets import QSpacerItem, QApplication, QSystemTrayIcon, QMenu
from qfluentwidgets import LineEdit, SpinBox, TextEdit, BodyLabel, PrimaryPushButton, setTheme, Theme, Action, \
    FluentIcon, RoundMenu

from vnpy_ctp import CtpGateway
from vnpy.trader.engine import MainEngine
from guanlan_futures.common.modules import accounts
from vnpy.event import Event, EventEngine
from vnpy_datarecoder.engine import RecorderEngine, APP_NAME, EVENT_RECORDER_LOG, EVENT_RECORDER_UPDATE, \
    EVENT_RECORDER_EXCEPTION
from vnpy.trader.event import EVENT_CONTRACT
from vnpy.trader.object import ContractData
from guanlan_futures.common import resource
from guanlan_widgets import MicaWindow


# from guanlan_futures.common import resource


class DataRecoderWindow(MicaWindow):
    signal_log: QtCore.Signal = QtCore.Signal(Event)
    signal_update: QtCore.Signal = QtCore.Signal(Event)
    signal_contract: QtCore.Signal = QtCore.Signal(Event)
    signal_exception: QtCore.Signal = QtCore.Signal(Event)

    def __init__(self):
        super().__init__()

        self.setWindowIcon(QIcon(':/futures/icon/recorder.png'))
        self.setWindowTitle("行情记录")
        self.titleBar.minBtn.hide()
        self.titleBar.maxBtn.hide()
        self.setMicaEffectEnabled(True)

        self.event_engine = EventEngine()
        self.main_engine = MainEngine(self.event_engine)
        self.main_engine.add_engine(RecorderEngine)

        self.recorder_engine: RecorderEngine = self.main_engine.get_engine(APP_NAME)

        self.init_ui()
        self.init_tray_icon()
        self.register_event()
        self.recorder_engine.put_event()

        # 连接服务器
        self.connect_ctp()

    def closeEvent(self, event):
        # 忽略退出事件,而是隐藏到托盘
        event.ignore()
        self.hide()

    def init_tray_icon(self):
        # 配置系统托盘
        self.tray_icon = QSystemTrayIcon(self)
        self.tray_icon.setIcon(QIcon(':/futures/icon/recorder.png'))
        self.tray_icon.setToolTip("观澜量化 - 行情记录")
        self.tray_icon.show()

        # 创建托盘的右键菜单
        self.tray_menu = RoundMenu(parent=self)
        self.tray_menu.addAction(Action(FluentIcon.ZOOM, "显示", triggered=self.show))
        self.tray_menu.addAction(Action(FluentIcon.CLOSE, "退出", triggered=self.quit))

        # 配置菜单并显示托盘
        self.tray_icon.setContextMenu(self.tray_menu)  # 把tpMenu设定为托盘的右键菜单

    def quit(self):
        # 真正的退出
        self.recorder_engine.close()
        self.main_engine.close()
        sys.exit()

    def connect_ctp(self):
        # 找账户配置为行情的
        self.gateway_name = ""
        setting = {}

        for key, value in accounts.account_data.items():
            if value['md_data'] == "1":
                self.gateway_name = key
                setting = value
                break

        gateway = self.main_engine.add_gateway(CtpGateway, self.gateway_name)
        self.main_engine.connect(setting, self.gateway_name)

    def init_ui(self) -> None:
        """"""

        self.symbol_line: LineEdit = LineEdit()

        self.interval_spin: SpinBox = SpinBox()
        self.interval_spin.setMinimum(1)
        self.interval_spin.setMaximum(60)
        self.interval_spin.setValue(self.recorder_engine.timer_interval)
        self.interval_spin.setSuffix("  秒")
        self.interval_spin.valueChanged.connect(self.set_interval)

        contracts: List[ContractData] = self.main_engine.get_all_contracts()
        self.vt_symbols: list = [contract.vt_symbol for contract in contracts]

        self.symbol_completer: QtWidgets.QCompleter = QtWidgets.QCompleter(self.vt_symbols)
        self.symbol_completer.setFilterMode(Qt.MatchContains)
        self.symbol_line.setCompleter(self.symbol_completer)

        add_bar_button: PrimaryPushButton = PrimaryPushButton(text="添加", parent=self)
        add_bar_button.clicked.connect(self.add_bar_recording)

        remove_bar_button: PrimaryPushButton = PrimaryPushButton(text="移除", parent=self)
        remove_bar_button.clicked.connect(self.remove_bar_recording)

        add_tick_button: PrimaryPushButton = PrimaryPushButton(text="添加", parent=self)
        add_tick_button.clicked.connect(self.add_tick_recording)

        remove_tick_button: PrimaryPushButton = PrimaryPushButton(text="移除", parent=self)
        remove_tick_button.clicked.connect(self.remove_tick_recording)

        self.bar_recording_edit: TextEdit = TextEdit()
        self.bar_recording_edit.setReadOnly(True)

        self.tick_recording_edit: TextEdit = TextEdit()
        self.tick_recording_edit.setReadOnly(True)

        self.log_edit: TextEdit = TextEdit()
        self.log_edit.setReadOnly(True)

        # Set layout
        grid: QtWidgets.QGridLayout = QtWidgets.QGridLayout()
        grid.addWidget(BodyLabel(text="K线记录", parent=self), 0, 0)
        grid.addWidget(add_bar_button, 0, 1)
        grid.addWidget(remove_bar_button, 0, 2)
        grid.addWidget(BodyLabel(text="Tick记录", parent=self), 1, 0)
        grid.addWidget(add_tick_button, 1, 1)
        grid.addWidget(remove_tick_button, 1, 2)

        self.symbol_line.setMinimumWidth(300)
        form: QtWidgets.QFormLayout = QtWidgets.QFormLayout()
        form.addRow(BodyLabel(text="本地代码", parent=self), self.symbol_line)
        form.addRow(BodyLabel(text="写入间隔", parent=self), self.interval_spin)

        hbox: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout()
        hbox.addLayout(form)
        # hbox.addWidget(BodyLabel(text="     ", parent=self))
        hbox.addItem(QSpacerItem(50, 0))
        hbox.addLayout(grid)
        hbox.addStretch(1)
        hbox.setContentsMargins(0, 10, 0, 0)

        grid2: QtWidgets.QGridLayout = QtWidgets.QGridLayout()
        grid2.addWidget(BodyLabel(text="K线记录列表", parent=self), 0, 0)
        grid2.addWidget(BodyLabel(text="Tick记录列表", parent=self), 0, 1)
        grid2.addWidget(self.bar_recording_edit, 1, 0)
        grid2.addWidget(self.tick_recording_edit, 1, 1)
        grid2.addWidget(self.log_edit, 2, 0, 1, 2)
        grid2.setContentsMargins(0, 10, 0, 0)

        vbox: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout()
        vbox.addLayout(hbox)
        vbox.addLayout(grid2)
        vbox.setContentsMargins(20, 40, 12, 12)

        self.setLayout(vbox)

    def register_event(self) -> None:
        """"""
        self.signal_log.connect(self.process_log_event)
        self.signal_contract.connect(self.process_contract_event)
        self.signal_update.connect(self.process_update_event)
        self.signal_exception.connect(self.process_exception_event)

        self.event_engine.register(EVENT_CONTRACT, self.signal_contract.emit)
        self.event_engine.register(EVENT_RECORDER_LOG, self.signal_log.emit)
        self.event_engine.register(EVENT_RECORDER_UPDATE, self.signal_update.emit)
        self.event_engine.register(EVENT_RECORDER_EXCEPTION, self.signal_exception.emit)

    def process_log_event(self, event: Event) -> None:
        """"""
        timestamp: str = datetime.now().strftime("%H:%M:%S")
        msg: str = f"{timestamp}\t{event.data}"
        self.log_edit.append(msg)

    def process_update_event(self, event: Event) -> None:
        """"""
        data: Any = event.data

        self.bar_recording_edit.clear()
        bar_text: str = "\n".join(data["bar"])
        self.bar_recording_edit.setText(bar_text)

        self.tick_recording_edit.clear()
        tick_text: str = "\n".join(data["tick"])
        self.tick_recording_edit.setText(tick_text)

    def process_contract_event(self, event: Event) -> None:
        """"""
        contract: ContractData = event.data
        self.vt_symbols.append(contract.vt_symbol)

        model: QtCore.QAbstractItemModel = self.symbol_completer.model()
        model.setStringList(self.vt_symbols)

    def process_exception_event(self, event: Event) -> None:
        """"""
        exc_info = event.data
        raise exc_info[1].with_traceback(exc_info[2])

    def add_bar_recording(self) -> None:
        """"""
        vt_symbol: str = self.symbol_line.text()
        self.recorder_engine.add_bar_recording(vt_symbol, self.gateway_name)


    def add_tick_recording(self) -> None:
        """"""
        vt_symbol: str = self.symbol_line.text()
        self.recorder_engine.add_tick_recording(vt_symbol)

    def remove_bar_recording(self) -> None:
        """"""
        vt_symbol: str = self.symbol_line.text()
        self.recorder_engine.remove_bar_recording(vt_symbol)

    def remove_tick_recording(self) -> None:
        """"""
        vt_symbol: str = self.symbol_line.text()
        self.recorder_engine.remove_tick_recording(vt_symbol)

    def set_interval(self, interval) -> None:
        """"""
        self.recorder_engine.timer_interval = interval


if __name__ == '__main__':

    setTheme(Theme.DARK)

    # 初始化应用和窗口
    app = QApplication(sys.argv)
    win = DataRecoderWindow()
    win.show()

    # 运行应用
    sys.exit(app.exec())
Member
avatar
加入于:
帖子: 12
声望: 4

可执行版本已发,学习研究用,一切使用后果由您自行承担。

Member
avatar
加入于:
帖子: 15
声望: 1

厉害!赞 赞 赞

Member
avatar
加入于:
帖子: 15
声望: 0

已关注知乎~

Member
avatar
加入于:
帖子: 12
声望: 4

距离上一篇”完结”过去了快一年,本来以为这个系列就这样了。

然后 AI 来了。

不是那种”帮你补全几行代码”的 AI,而是真的能从零开始、一个文件一个文件帮你写出来的 AI。看着 Claude 一步步理解我的需求,读懂 VNPY 的源码,处理 PySide6 的信号槽、QSS 样式、Pydantic 数据模型……说实话,有点震撼。

于是趁着春节前的这段时间,我做了一个实验:用 AI 辅助,从 VNPY 3.9 重构到 4.3,看看能走多远。

结果是:大约一周时间,从项目搭建到功能完善,我没有手写一行代码。所有的代码都是在和 AI 的对话中产生的——我负责提需求、审代码、做决策,AI 负责实现。

为什么要重构
VNPY 从 3.9 到 4.3 变化很大,核心模块拆分成了独立包(vnpy-ctastrategy、vnpy-ctp 等),架构上差异太多,简单迁移行不通,不如重新来过。

正好,V1 积累了不少想法但一直没动手改的地方,这次借 AI 之手一并实现了。

V2 主要变化
变化 说明
基础框架 VNPY 3.9 → 4.3
数据库 SQLite/MySQL → ArcticDB (LMDB)
策略架构 三层数据模型 Params/State/Vars
双模交易 同一策略支持全自动 + 辅助半自动
多账户 实盘/模拟/7×24 环境并存
主力合约 自动识别 + 换月持仓保护
AI 助手 实验性接入,目标是策略信号辅助
界面 PySide6 + QFluentWidgets 全面重写
详细内容就不在这里展开了,感兴趣的朋友请移步 GitHub 仓库查看 README。

开源
项目已开源,MIT 协议,可自由使用和修改:

GitHub:https://github.com/48645970/guanlan

欢迎 Star,欢迎提 Issue,欢迎交流。

感谢
感谢 VNPY 团队提供了优秀的量化交易框架,让这一切有了基础。

感谢 Claude —— 这个项目的”联合开发者”。一周时间里,从框架搭建到细节打磨,每一行代码都经过了我们之间的对话。AI 不会疲倦,不会敷衍,能记住上下文,能读懂源码,能理解”这里不太对”是什么意思。这种体验,一年前完全无法想象。

我们正站在一个很有意思的时代节点上。

最后
祝大家新年快乐,交易顺利。

© 2015-2022 上海韦纳软件科技有限公司
备案服务号:沪ICP备18006526号

沪公网安备 31011502017034号

【用户协议】
【隐私政策】
【免责条款】