户: ctp行情每秒推送两笔数据,而我的ctp行情回调有时候每秒显示一笔,是不是漏了,vnpy更是隔了几秒才有一个tick显示,难道vnpy接口慢?tick: 2026-04-29 23:08:47+08:00 518222 ag2606 18052.0 tick: 2026-04-29 23:08:51.500000+08:00 518256 ag2606 18048.0 tick: 2026-04-29 23:08:52+08:00 518256 ag2606 18048.0 tick: 2026-04-29 23:08:57+08:00 518316 ag2606 18044.0 tick: 2026-04-29 23:09:02+08:00 518422 ag2606 18044.0 tick: 2026-04-29 23:09:06.500000+08:00 518465 ag2606 18046.0 tick: 2026-04-29 23:09:07+08:00 518468 ag2606 18046.0 tick: 2026-04-29 23:09:11.500000+08:00 518573 ag2606 18036.0 tick: 2026-04-29 23:09:12+08:00 518592 ag2606 18036.0 这是vnpy的tick输出 ag2606 23:09:16 18030 9 ag2606 23:09:17 18034 41 ag2606 23:09:18 18032 35 ag2606 23:09:18 18035 14 ag2606 23:09:19 18035 1 ag2606 23:09:19 18029 70 ag2606 23:09:20 18025 15 ag2606 23:09:20 18031 35 ag2606 23:09:21 18022 35 ag2606 23:09:21 18028 43 ag2606 23:09:22 18029 10 ag2606 23:09:22 18025 8 ag2606 23:09:23 18026 14 ag2606 23:09:24 18027 25 ag2606 23:09:25 18026 21 ag2606 23:09:26 18030 19 ag2606 23:09:26 18026 14 ag2606 23:09:27 18028 27 ag2606 23:09:27 18027 15 ag2606 23:09:28 18028 2 ag2606 23:09:29 18029 25 这是C++的输出 # run/collect_vnpy.py from realtime.bar_agg import BarAggregator from realtime.features import FeatureEngine from realtime.signal_engine import SignalEngine import time from datetime import datetime from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy_ctp import CtpGateway from vnpy.trader.object import SubscribeRequest from vnpy.trader.event import EVENT_TICK from ctp.collector import TickCollector from vnpy.trader.object import SubscribeRequest from vnpy.trader.constant import Exchange # ========= 配置 ========= SETTING = { "用户名": "***", "密码": "***", "经纪商代码": "9999", "交易服务器": "tcp://182.254.243.31:40001", "行情服务器": "tcp://182.254.243.31:40011", "产品名称": "", "授权编码": "", } SYMBOL = "ag2606" # 🔥改成当前主力 EXCHANGE = "SHFE" # ========= 初始化 ========= collector = TickCollector(flush_size=500) event_engine = EventEngine() main_engine = MainEngine(event_engine) main_engine.add_gateway(CtpGateway) bar_agg = BarAggregator(window_sec=3) feat_engine = FeatureEngine(window=20) sig_engine = SignalEngine() # ========= Tick回调 ========= def on_bar(bar): features = feat_engine.update(bar) signal = sig_engine.on_feature(features) if signal: print(f"[SIGNAL] {bar['datetime']} -> {signal} | price={bar['close']}") bar_agg.on_bar = on_bar def on_tick(event): t = event.data tick = { "symbol": t.symbol, "datetime": t.datetime, "price": t.last_price, "volume": t.volume, "bid_price1": t.bid_price_1, "ask_price1": t.ask_price_1, "bid_vol1": t.bid_volume_1, "ask_vol1": t.ask_volume_1, } bar_agg.update(tick) # def on_tick(event): # t = event.data # 🔥 统一成你策略需要的字段格式 # tick = { # "symbol": t.symbol, # "datetime": t.datetime, # # "price": t.last_price, # "volume": t.volume, # 注意:vn.py这里也是累计量 # # "open_interest": t.open_interest, # L1 # "bid_price1": t.bid_price_1, # "ask_price1": t.ask_price_1, # "bid_vol1": t.bid_volume_1, # "ask_vol1": t.ask_volume_1, # L2(有就用,没有会是0) # "bid_price2": getattr(t, "bid_price_2", 0), # "bid_price3": getattr(t, "bid_price_3", 0), # "bid_price4": getattr(t, "bid_price_4", 0), # "bid_price5": getattr(t, "bid_price_5", 0), # "ask_price2": getattr(t, "ask_price_2", 0), # "ask_price3": getattr(t, "ask_price_3", 0), # "ask_price4": getattr(t, "ask_price_4", 0), # "ask_price5": getattr(t, "ask_price_5", 0), # "bid_vol2": getattr(t, "bid_volume_2", 0), # "bid_vol3": getattr(t, "bid_volume_3", 0), # "bid_vol4": getattr(t, "bid_volume_4", 0), # "bid_vol5": getattr(t, "bid_volume_5", 0), # # "ask_vol2": getattr(t, "ask_volume_2", 0), # "ask_vol3": getattr(t, "ask_volume_3", 0), # "ask_vol4": getattr(t, "ask_volume_4", 0), # "ask_vol5": getattr(t, "ask_volume_5", 0), # } # 打印少量确认 if int(time.time()) % 5 == 0: print("tick:", tick["datetime"],tick["volume"],tick["symbol"], tick["price"]) collector.on_tick(tick) # 注册事件 event_engine.register(EVENT_TICK, on_tick) # ========= 启动 ========= print("Connecting CTP...") main_engine.connect(SETTING, "CTP") # 等待连接 time.sleep(5) # 订阅行情 req = SubscribeRequest( symbol="ag2606", exchange=Exchange.SHFE ) main_engine.subscribe(req, "CTP") print("Subscribed:", SYMBOL) # ========= 主循环 ========= try: while True: time.sleep(1) except KeyboardInterrupt: print("Stopping...") collector.flush() 这是vnpy的python程序 #include "stdafx.h" #include "main.h" #include unordered_map g_KlineHash; // 不同合约的k线存储表 using namespace std; int main() { system("COLOR 0A"); logfile = fopen("syslog.txt", "w"); LOG("logfile opend");//zfkcode string g_chFrontaddr = getConfig("config", "FrontAddr"); //string g_chInstrumentID = getConfig("config", "InstrumentID");//zfkcode cout << endl<< "got g_chFrontaddr = " << g_chFrontaddr << "\n" << endl; //定义派生交易类指向CTraderApi对象指针,指向内存中分配给创建的实例空间 CTraderApi *pUserApi = new CTraderApi;//-------------------------- //对象初始化 实际是调用基类初始化函数,返回指向交易基类对象指针 pUserApi->CreateFtdcTraderApi(".\\flow\\"); LOG(pUserApi->GetApiVersion()); cout << endl; //开始想错了,这是创建交易类对象实例sh,这个交易类实例重写了很多TraderAPI中的函数 //创建交易控制类实例sh,将已经创建的交易基类实例交给sh CSimpleHandler sh(pUserApi); //注册回调 cir:pUserApi->RegisterSpi(&sh); pUserApi->SubscribePrivateTopic(THOST_TERT_QUICK); pUserApi->SubscribePublicTopic(THOST_TERT_QUICK); pUserApi->RegisterFront(const_cast(g_chFrontaddr.c_str())); pUserApi->Init(); WaitForSingleObject(g_hEvent, INFINITE); //_getch(); while (true) { LOG("请确定连接模式:\n"); LOG("1.直连模式\n"); LOG("2.中继服务器操作员模式(一对多模式)\n"); LOG("3.中继服务器非操作员模式(多对多模式)\n"); int mode_num; cin >> mode_num; switch (mode_num) { case 1://直连模式 { sh.ReqAuthenticate(); WaitForSingleObject(g_hEvent, INFINITE); sh.ReqUserLogin(); WaitForSingleObject(g_hEvent, INFINITE); break; } case 2://操作员模式 { sh.ReqAuthenticate(); WaitForSingleObject(g_hEvent, INFINITE); sh.ReqUserLogin(); WaitForSingleObject(g_hEvent, INFINITE); sh.SubmitUserSystemInfo(); break; } case 3://非操作员模式 { sh.ReqAuthenticate(); WaitForSingleObject(g_hEvent, INFINITE); sh.RegisterUserSystemInfo(); sh.ReqUserLogin(); WaitForSingleObject(g_hEvent, INFINITE); break; } default: LOG("选择的模式有误,请重新输入!\n"); _getch(); system("cls"); } break; } _getch(); LOG("日期:"); LOG(pUserApi->GetTradingDay()); cout << endl; while (true) { loop:int input1; system("cls"); LOG("201.上报用户终端信息\n"); LOG("110,交易查询合约行情收行情\n"); LOG("115,收一合约行情在\n"); LOG("101.用户登录请求\n"); LOG("102.客户端认证\n"); LOG("103.请求登出\n"); LOG("1.结算单确认请求\n"); LOG("2.用户口令更新请求\n"); LOG("3.资金账户口令更新请求\n"); LOG("/////////////报单////////////\n"); LOG("4.报单模块\n"); LOG("////////////查询/////////////\n"); LOG("5.查询模块\n"); LOG("/////////////行权&做市商////////////\n"); LOG("6.行权&做市商\n"); LOG("///////////出入金/////////////\n"); LOG("7.转账指令\n"); LOG("///////////铜期权测试////////////\n"); LOG("8.铜期权测试\n"); LOG("9.版本6.3.13新加接口\n"); LOG("0.清空界面\n"); LOG("100.退出程序\n"); LOG("请输入你需要的操作序号:"); cin >> input1; switch (input1) { case 201: { sh.SubmitUserSystemInfo(); _getch(); break; } case 110: { //获取行情前置服务器地址 string g_chFrontMdaddr = getConfig("config", "FrontMdAddr"); //输出回显 cout << "g_chFrontMdaddr = " << g_chFrontMdaddr << "\n" << endl; //记录入log LOG("g_chFrontMdaddr = ");//zfkcode LOG(g_chFrontMdaddr.c_str());//zfkcode //创建(分配内存给?)CThostFtdcMdApi行情类对象,设置行情类指针指向该实例,指明订阅流flow输出文件夹zfkcode CThostFtdcMdApi *pUserMdApi = CThostFtdcMdApi::CreateFtdcMdApi(".\\flow\\md"); //(在指定空间)创建一对创建一行情类的派生类CSimpleMDHandler(eorr不是构造行情类)实例ash,包含之前创建的基类实例// zfkcode CSimpleMdHandler ash(pUserMdApi);//以刚刚创建的实例为基础创建派生类实例(复制内存?zfkcode //回调基类实例成员函数注册派生类实例(接口?zfkcode应该是初始化对象ash) pUserMdApi->RegisterSpi(&ash); //回调基类实例成员函数注册行情前置机 pUserMdApi->RegisterFront(const_cast(g_chFrontMdaddr.c_str())); //回调基类实例成员函数初始化 pUserMdApi->Init(); WaitForSingleObject(xinhao, INFINITE); sh.ReqQryInstrument();//查询合约 //调用交易类对象查询zfkcode WaitForSingleObject(xinhao, INFINITE); //ash.SubscribeSpecificMarketData();//zfkcode //zfk remark ash.SubscribeMarketData();//订阅行情 _getch();//任意键 pUserMdApi->Release(); break; } case 115: { //获取行情前置服务器地址 string g_chFrontMdaddr = getConfig("config", "FrontMdAddr"); //输出回显 cout << "g_chFrontMdaddr = " << g_chFrontMdaddr << "\n" << endl; //记录入log LOG("g_chFrontMdaddr = ");//zfkcode LOG(g_chFrontMdaddr.c_str());//zfkcode //创建(分配内存给?)CThostFtdcMdApi行情类对象,设置行情类指针指向该实例,指明订阅流flow输出文件夹zfkcode CThostFtdcMdApi* pUserMdApi = CThostFtdcMdApi::CreateFtdcMdApi(".\\flow\\md"); //(在指定空间)创建一对创建一行情类的派生类CSimpleMDHandler(eorr不是构造行情类)实例ash,包含之前创建的基类实例// zfkcode CSimpleMdHandler ash(pUserMdApi);//以刚刚创建的实例为基础创建派生类实例(复制内存?zfkcode //回调基类实例成员函数注册派生类实例(接口?zfkcode应该是初始化对象ash) pUserMdApi->RegisterSpi(&ash); //回调基类实例成员函数注册行情前置机 pUserMdApi->RegisterFront(const_cast(g_chFrontMdaddr.c_str())); //回调基类实例成员函数初始化 pUserMdApi->Init(); WaitForSingleObject(xinhao, INFINITE); //sh.ReqQryInstrument();//查询合约 //调用交易类对象查询zfkcode //WaitForSingleObject(xinhao, INFINITE); ash.SubscribeSpecificMarketData();//zfkcode:订阅合约代码在config.ini中读取 //zfk remark //ash.SubscribeMarketData();//订阅行情 _getch();//任意键 pUserMdApi->Release(); break; } case 101: { sh.ReqUserLogin(); _getch(); break; } case 102: { sh.ReqAuthenticate(); _getch(); break; } case 103: { sh.ReqUserLogout(); _getch(); goto cir; } case 1://结算单确认请求 { sh.ReqSettlementInfoConfirm();//结算单确认请求 WaitForSingleObject(g_hEvent, INFINITE); _getch(); system("cls"); break; } case 2://用户口令更新请求 { sh.ReqUserPasswordUpdate();//用户口令更新请求 WaitForSingleObject(g_hEvent, INFINITE); _getch(); system("cls"); break; } case 3://资金账户口令更新请求 { sh.ReqTradingAccountPasswordUpdate();//资金账户口令更新请求 WaitForSingleObject(g_hEvent, INFINITE); _getch(); system("cls"); break; } case 4://报单录入请求 { orderinsert:system("cls"); int orderinsert_num; LOG("1.市价买10\n"); LOG("3.市价卖10\n"); LOG("111.市价买平今10\n"); LOG("333.市价卖平今10\n"); LOG("777.市价买平昨天10\n"); LOG("999.市价卖平昨天10\n"); LOG("4.报入一笔立即单\n"); LOG("5.撤销上一笔报单\n"); LOG("6.报入预埋单-限价单立即单\n"); LOG("7.撤销预埋单-(上一个预埋单)\n"); LOG("8.报入预埋撤单\n"); LOG("9.撤销预埋撤单-(上一个预埋撤单)\n"); LOG("10.报入条件单\n"); LOG("11.撤销条件单-(上一个条件单)\n"); LOG("25.大商所止损单\n"); LOG("26.大商所止盈单\n"); LOG("27.FOK全成全撤\n"); LOG("28.FAK部成部撤\n"); LOG("29.市价单\n"); LOG("30.套利指令\n"); LOG("31.互换单\n"); LOG("0.返回上一层\n"); cin >> orderinsert_num; switch (orderinsert_num) { case 0: goto loop; case 1://市价买10手 { sh.ZfkReqOrderInsert_like_AnyPrice_buy_10(); _getch(); break; } case 3://市价买10手 { sh.ZfkReqOrderInsert_like_AnyPrice_sell_10(); _getch(); break; } case 111://市价买平今10手 { sh.ZfkReqOrderInsert_like_AnyPriceBuyCloseToday_10(); _getch(); break; } case 333://市价卖平今10手 { sh.ZfkReqOrderInsert_like_AnyPriceSellCloseToday_10(); _getch(); break; } case 777://市价买平10手昨仓 { sh.ZfkReqOrderInsert_like_AnyPriceBuyCloseYestoday_10(); _getch(); break; } case 999://市价卖平10手昨仓 { sh.ZfkReqOrderInsert_like_AnyPriceSellCloseYestoday_10(); _getch(); break; } case 4://报入普通立即单 { sh.ReqOrderInsert_Ordinary(); _getch(); break; } case 5://撤销上一笔报单 { sh.ReqOrderAction_Ordinary();//撤销上一笔报单 _getch(); break; } case 6://报入预埋单 { sh.ReqParkedOrderInsert();//报入预埋单 _getch(); break; } case 7://删除预埋单 { sh.ReqRemoveParkedOrder();//删除预埋单 _getch(); break; } case 8://报入预埋撤单 { sh.ReqParkedOrderAction();//报入预埋撤单 _getch(); break; } case 9://删除预埋单撤单 { sh.ReqRemoveParkedOrderAction();//删除预埋单撤单 _getch(); break; } case 10://报入条件单 { it:LOG("1.最新价大于条件价\n"); LOG("2.最新价大于等于条件价\n"); LOG("3.最新价小于条件价\n"); LOG("4.最新价小于等于条件价\n"); LOG("5.卖一价大于条件价\n"); LOG("6.卖一价大于等于条件价\n"); LOG("7.卖一价小于条件价\n"); LOG("8.卖一价小于等于条件价\n"); LOG("9.买一价大于条件价\n"); LOG("10.买一价大于等于条件价\n"); LOG("11.买一价小于条件价\n"); LOG("12.买一价小于等于条件价\n"); LOG("13.返回上一层\n"); LOG("请输入你需要报入的条件单类型:\n"); int num; cin >> num; if (num < 1 || num>13) { LOG("输入的序号有误,请重新输入.\n"); _getch(); goto it; } else if (num == 13) { goto orderinsert; } else { sh.ReqOrderInsert_Condition(num);//报入条件单 _getch(); break; } } case 11://报入条件单撤单 { sh.ReqOrderAction_Condition();//报入条件单撤单 _getch(); break; } case 25://大商所止损单 { sh.ReqOrderInsert_Touch(); _getch(); break; } case 26://大商所止盈单 { sh.ReqOrderInsert_TouchProfit(); _getch(); break; } case 27://FOK全成全撤 { sh.ReqOrderInsert_VC_CV(); _getch(); break; } case 28://FAK部成部撤 { sh.ReqOrderInsert_VC_AV(); _getch(); break; } case 29://市价单 { sh.ReqOrderInsert_AnyPrice(); _getch(); break; } case 30://套利指令 { sh.ReqOrderInsert_Arbitrage(); _getch(); break; } case 31://互换单 { sh.ReqOrderInsert_IsSwapOrder(); _getch(); break; } default: LOG("请输入正确的序号\n"); _getch(); goto orderinsert; } goto orderinsert; } case 5://查询界面 { search:system("cls"); int choose_num; LOG("11.查询成交\n"); LOG("12.查询预埋单\n"); LOG("13.查询预埋撤单\n"); LOG("14.查询报单\n"); LOG("15.撤单对应查询编号\n"); LOG("16.请求查询资金账户\n");//ReqQryTradingAccount LOG("17.请求查询投资者持仓\n");//ReqQryInvestorPosition LOG("18.请求查询投资者持仓明细\n");//ReqQryInvestorPositionDetail LOG("19.请求查询交易所保证金率\n");//ReqQryExchangeMarginRate LOG("20.请求查询合约保证金率\n");//ReqQryInstrumentMarginRate LOG("21.请求查询合约手续费率\n");//ReqQryInstrumentCommissionRate LOG("22.请求查询做市商合约手续费率\n");//ReqQryMMInstrumentCommissionRate LOG("23.请求查询做市商期权合约手续费\n"); //ReqQryMMOptionInstrCommRate LOG("24.请求查询报单手续费\n");//ReqQryInstrumentOrderCommRate LOG("25.请求查询期权合约手续费\n");//ReqQryOptionInstrCommRate LOG("26.请求查询合约\n");//ReqQryInstrument LOG("27.请求查询投资者结算结果\n");//ReqQrySettlementInfo LOG("28.请求查询转帐流水\n");//ReqQryTransferSerial LOG("29.请求查询询价\n"); LOG("30.请求查询报价\n"); LOG("31.请求查询执行宣告\n"); LOG("32.请求查询转帐银行\n"); LOG("33.请求查询交易通知\n"); LOG("34.请求查询交易编码\n"); LOG("35.请求查询结算信息确认\n"); LOG("36.请求查询产品组\n"); LOG("37.请求查询投资者单元\n"); LOG("38.期货发起查询银行余额请求\n"); LOG("39.请求查询经纪公司交易参数\n"); LOG("0.返回上一层\n"); LOG("请输入选择的序号:\n"); cin >> choose_num; switch (choose_num) { case 11://请求查询成交 { sh.ReqQryTrade();//请求查询成交 _getch(); break; } case 12://请求查询服务器预埋单 { sh.ReqQryParkedOrder();//请求查询服务器预埋单 _getch(); break; } case 13://请求查询服务器预埋撤单 { sh.ReqQryParkedOrderAction();//请求查询服务器预埋撤单 _getch(); break; } case 14://请求查询报单 { sh.ReqQryOrder();//请求查询报单 _getch(); break; } case 15://撤销查询的报单回报 { action:int action_num; LOG("请输入需要撤单的序号:\n"); cin >> action_num; LOG("%d\n", action_num); if (action_num < 1 || action_num > vector_OrderSysID.size()) { LOG("输入的序号有误,请重新输入.\n"); _getch(); goto action; } sh.ReqOrderAction_forqry(action_num);//撤销查询的报单回报 _getch(); break; } case 16://请求查询资金账户 { sh.ReqQryTradingAccount();//请求查询资金账户 _getch(); break; } case 17://请求查询投资者持仓 { sh.ReqQryInvestorPosition();//请求查询投资者持仓 _getch(); break; } case 18://请求查询投资者持仓明细 { sh.ReqQryInvestorPositionDetail();//请求查询投资者持仓明细 _getch(); break; } case 19://请求查询交易所保证金率 { sh.ReqQryExchangeMarginRate();//请求查询交易所保证金率 _getch(); break; } case 20://请求查询合约保证金率 { sh.ReqQryInstrumentMarginRate();//请求查询合约保证金率 _getch(); break; } case 21://请求查询合约手续费率 { sh.ReqQryInstrumentCommissionRate();//请求查询合约手续费率 _getch(); break; } case 22://请求查询做市商合约手续费率 { sh.ReqQryMMInstrumentCommissionRate();//请求查询做市商合约手续费率 _getch(); break; } case 23://请求查询做市商期权合约手续费 { sh.ReqQryMMOptionInstrCommRate();//请求查询做市商合约手续费率 _getch(); break; } case 24://请求查询报单手续费 { sh.ReqQryInstrumentOrderCommRate();//请求查询报单手续费 _getch(); break; } case 25://请求查询期权合约手续费 { sh.ReqQryOptionInstrCommRate();//请求查询期权合约手续费 _getch(); break; } case 26://请求查询合约 { sh.ReqQryInstrument();//请求查询合约 _getch(); break; } case 27://请求查询投资者结算结果 { sh.ReqQrySettlementInfo();//请求查询投资者结算结果 _getch(); break; } case 28://请求查询转帐流水 { sh.ReqQryTransferSerial();//请求查询投资者结算结果 _getch(); break; } case 29://请求查询询价 { sh.ReqQryForQuote();//请求查询询价 _getch(); break; } case 30://请求查询报价 { sh.ReqQryQuote();//请求查询报价 _getch(); break; } case 31://请求查询执行宣告 { sh.ReqQryExecOrder();//请求查询执行宣告 _getch(); break; } case 32://请求查询转帐银行 { sh.ReqQryTransferBank(); _getch(); break; } case 33://请求查询交易通知 { sh.ReqQryTradingNotice(); _getch(); break; } case 34://请求查询交易编码 { sh.ReqQryTradingCode(); _getch(); break; } case 35://请求查询结算信息确认 { sh.ReqQrySettlementInfoConfirm(); _getch(); break; } case 36://请求查询产品组 { sh.ReqQryProductGroup(); _getch(); break; } case 37://请求查询投资者单元 { sh.ReqQryInvestUnit(); _getch(); break; } case 38://期货发起查询银行余额请求 { sh.ReqQueryBankAccountMoneyByFuture(); _getch(); break; } case 39://请求查询经纪公司交易参数 { sh.ReqQryBrokerTradingParams(); _getch(); break; } case 0: { goto loop; } default: { LOG("请输入正确的序号\n"); _getch(); goto search; } } goto search; } case 6://行权&做市商 { Exec:system("cls"); int num_xingquan; LOG("32.执行宣告录入请求\n"); LOG("33.执行宣告操作请求\n"); LOG("34.放弃行权\n"); LOG("35.询价录入请求\n"); LOG("36.做市商报价录入请求\n"); LOG("37.做市商报价撤销请求\n"); LOG("0.返回上一层") LOG("请选择你需要的编码"); cin >> num_xingquan; switch (num_xingquan) { case 32://执行宣告录入请求 { sh.ReqExecOrderInsert(0); _getch(); break; } case 33://执行宣告操作请求 { sh.ReqExecOrderAction(); _getch(); break; } case 34://放弃行权 { sh.ReqExecOrderInsert(1); _getch(); break; } case 35://询价录入请求 { string g_chFrontMdaddr = getConfig("config", "FrontMdAddr"); cout << "g_chFrontMdaddr = " << g_chFrontMdaddr << "\n" << endl; CThostFtdcMdApi *pUserMdApi = CThostFtdcMdApi::CreateFtdcMdApi(); CSimpleMdHandler ash(pUserMdApi); pUserMdApi->RegisterSpi(&ash); pUserMdApi->RegisterFront(const_cast(g_chFrontMdaddr.c_str())); pUserMdApi->Init(); WaitForSingleObject(xinhao, INFINITE); ash.SubscribeMarketData();//行情订阅询价请求 sh.ReqForQuoteInsert();//交易请求询价 _getch(); pUserMdApi->Release(); break; } case 36://做市商报价录入请求 { sh.ReqQuoteInsert(); _getch(); break; } case 37://做市商报价撤销请求 { sh.ReqQuoteAction(); _getch(); break; } case 0: { goto loop; } default: LOG("输入的编码有误,请重新输入\n"); _getch(); //goto Exec; } goto Exec; } case 7://期货资金转账 { futrue:system("cls"); int num_future; LOG("38.期货发起银行资金转期货请求\n"); LOG("39.期货发起期货资金转银行请求\n"); LOG("0.返回上一层\n"); LOG("请输入你需要的操作序号:"); cin >> num_future; switch (num_future) { case 38://期货发起银行资金转期货请求 { sh.ReqFromBankToFutureByFuture(); _getch(); break; } case 39://期货发起期货资金转银行请求 { sh.ReqFromFutureToBankByFuture(); _getch(); break; } case 0: { goto loop; } default: LOG("输入的编码有误,请重新输入\n"); _getch(); //goto futrue; } goto futrue; } case 8://铜期权测试 { qiquan:system("cls"); int num_qiquan; LOG("//////////////铜期权测试////////////\n"); LOG("1.期权自对冲录入请求\n"); LOG("2.期权自对冲操作请求\n"); LOG("3.请求查询期权自对冲\n"); LOG("0.返回上一层\n"); LOG("请选择你需要的编码:\n"); cin >> num_qiquan; switch (num_qiquan) { case 1: { sh.ReqOptionSelfCloseInsert(); _getch(); break; } case 2: { sh.ReqOptionSelfCloseAction(); _getch(); break; } case 3: { sh.ReqQryOptionSelfClose(); _getch(); break; } case 0: goto loop; break; default: LOG("输入的序号有误,请重新输入。\n\n"); _getch(); goto qiquan; } goto qiquan; } case 9://版本6.3.13新加接口 { NewVersion: system("cls"); int num_Newversion; LOG("新版本测试\n"); LOG("1.查询用户当前支持的认证模式(目前暂不启用)\n"); LOG("2.用户发出获取图形验证码请求(目前暂不启用)\n"); LOG("3.用户发出获取短信验证码请求(目前暂不启用)\n"); LOG("4.用户发出带有图片验证码的登陆请求(目前暂不启用)\n"); LOG("5.用户发出带有短信验证码的登陆请求(目前暂不启用)\n"); LOG("6.用户发出带有动态口令的登陆请求(目前暂不启用)\n"); LOG("0.返回上一层\n"); LOG("请选择你需要的编码:\n"); cin >> num_Newversion; switch (num_Newversion) { case 1://查询用户当前支持的认证模式 { sh.ReqUserAuthMethod(); _getch(); break; } case 2://用户发出获取图形验证码请求 { sh.ReqGenUserCaptcha(); _getch(); break; } case 3://用户发出获取短信验证码请求 { sh.ReqGenUserText(); _getch(); break; } case 4://用户发出带有图片验证码的登陆请求 { sh.ReqUserLoginWithCaptcha(); _getch(); break; } case 5://用户发出带有短信验证码的登陆请求 { sh.ReqUserLoginWithText(); _getch(); break; } case 6://用户发出带有动态口令的登陆请求 { sh.ReqUserLoginWithOTP(); _getch(); break; } case 0: goto loop; break; default: LOG("输入的序号有误,请重新输入。\n\n"); _getch(); goto NewVersion; } goto NewVersion; } case 0: system("cls"); break; case 100: pUserApi->Release(); exit(-1); } } return 0; } C++的主程序,从demo改来的 你的观察很仔细,但从代码逻辑来看,**vn.py 并没有慢,也没有漏行情**,你看到的“隔几秒一个 tick”的现象,是 **Python 脚本里有意设置的打印过滤条件** 造成的假象,并不是接口性能问题。 --- ## 1. 为什么 vn.py 的 tick 输出看着这么稀疏? 在你的 `collect_vnpy.py` 的 `on_tick` 函数里,有这样一段: ```python # 打印少量确认 if int(time.time()) % 5 == 0: print("tick:", tick["datetime"], tick["volume"], tick["symbol"], tick["price"]) ``` 这里的逻辑是:**只有当当前秒数的整数部分能被 5 整除时,才打印这一条 tick**。 也就是说,tick 本身是全量推送到回调函数里的,**每一个 tick 都正常触发了 `on_tick`**,只是你只选择性地打印了“整 5 秒时刻”到来的 tick。 你的输出非常吻合这个规律: - `23:08:47`、`23:08:52`、`23:08:57`、`23:09:02`、`23:09:07`、`23:09:12` 都是被 5 整除的秒数(47? 47不能被5整除,但 time.time() 返回的是当前机器时间,并不等于 tick 的 datetime;可能是 tick 的 datetime 是交易所时间,而 time.time() 是本地系统时间,两者之间存在微小偏移,导致有时在 47 秒时本地时间的整数秒也恰好是 5 的倍数,比如本地时间 23:08:45 或 23:08:50**。仔细看,47 确实出现了,可能是机器时间与交易所时间不同步,但这恰恰说明了过滤条件是本地 time.time() 作用于所有到来的 tick,所以会产生一些看起来“非 5 整数秒”的打印,但总体仍是每隔 5 秒左右打印一两条。) 你在 C++ 程序里则是每个 tick 都直接输出(`LOG(...)` 或 `printf`),所以看到的密度是每秒 2 条——这才是真实的行情推送速率。 --- ## 2. vn.py 的行情接口本身并不慢 vn.py 底层使用的 `vnpy_ctp` 网关本质上是 **调用 CTP 官方 C++ API**,网络接收、回调触发都是 C++ 层面完成的,性能损失极小。 - 在 C++ 里你能收到每秒 2 条 tick,vn.py 里同样能收到。 - 你的 `on_tick` 回调是事件引擎驱动的,每个 tick 都会生成一个 `EVENT_TICK` 事件并排队处理。对于每秒钟 2 个事件,Python 处理是完全无压力的(通常延迟 <1 毫秒)。 - 你看到的输出延迟,纯粹是 `print` 的条件限制,外加 `print` 本身可能被缓冲(不过这不是重点)。 **所以,不存在“vnpy 接口慢”或“漏行情”的问题。** --- ## 3. 如何验证没有丢 tick? 你可以用下面的方法快速验证全量 tick 是否都到达: ### 方法一:打印每一个 tick(临时测试) 把刚才那段条件打印改成无条件打印: ```python # 暂时注释条件,直接输出 # if int(time.time()) % 5 == 0: print("tick:", tick["datetime"], tick["volume"], tick["symbol"], tick["price"]) ``` 然后运行几秒钟,你会立刻看到每秒 2 条的密集输出,和 C++ 程序一致。 **注意**:这样可能会输出非常多,测试完记得改回去或增加计数后再打印。 ### 方法二:用计数器验证 在 `on_tick` 外面设一个全局计数,并在回调里累加,每秒或每 10 秒输出一次计数: ```python tick_count = 0 last_time = time.time() def on_tick(event): global tick_count, last_time tick_count += 1 if time.time() - last_time >= 10: print(f"Received {tick_count} ticks in last 10 seconds") tick_count = 0 last_time = time.time() # ... 原有处理 ... ``` 你会看到输出类似 `Received 20 ticks in last 10 seconds`,说明完全没丢。 ### 方法三:检查相邻 tick 时间戳 记录上一个 tick 的 `datetime`,看差值是否为 500ms 左右(因为是 500ms 快照)。如果大部分差值为 0.5 秒,就说明行情是连续的。 --- ## 4. C++ 程序为什么输出密集?只是打印策略不同 你的 C++ 程序在收到行情时是**无条件直接打印**的(例如 `LOG(...)` 或直接 `cout`),所以每个 tick 都会被输出。 这和 Python 程序每隔 5 秒才打印一次,本质上只是**展示频率不同**,不反映接口性能差异。 --- ## 5. 额外提醒:确认行情前置地址与订阅方式 - 你的 Python 程序里连接的是 `tcp://182.254.243.31:40011`,C++ 里可能用的是同一个(从 `config.ini` 读取)。如果是同一个地址,那行情来源完全一致。 - Python 里订阅的是 `ag2606`,C++ 里也是,所以没有差异。 - 唯一容易忽略的是:**CTP 行情有不同推送模式**(例如快速行情和非快速)。你的 C++ 程序里明确订阅了 `THOST_TERT_QUICK`(即快速模式),而 vn.py 的 CtpGateway 默认也是使用快速模式(在行情回调里会去判断 `TERT_QUICK`)。只要没有改动过 `vnpy_ctp` 源码,两者也会完全一致。 --- ## 总结 - ✅ vn.py 没有慢,tick 回调是实时触发的。 - ✅ 你看到的“隔几秒一个 tick”是因为 `if int(time.time()) % 5 == 0` 的打印过滤。 - ✅ 真实行情仍然是每秒 2 个 tick,和 C++ 程序收到的完全一样。 - 🔧 想验证的话,去掉过滤条件或者添加计数即可。 如果你对 vn.py 内部事件处理的延迟有更量化的要求,也可以运行 `vnpy` 自带的 `examples/candle_chart` 之类的示例,观察 tick 时间戳连续性,结果会是令人放心的。 用户: Exception in thread Thread-1 (_run): Traceback (most recent call last): File "/root/.local/share/uv/python/cpython-3.13.13-linux-x86_64-gnu/lib/python3.13/threading.py", line 1044, in _bootstrap_inner self.run() ~~~~~~~~^^ File "/root/.local/share/uv/python/cpython-3.13.13-linux-x86_64-gnu/lib/python3.13/threading.py", line 995, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/root/ag_ctp_quant/.venv/lib/python3.13/site-packages/vnpy/event/engine.py", line 62, in _run self._process(event) ~~~~~~~~~~~~~^^^^^^^ File "/root/ag_ctp_quant/.venv/lib/python3.13/site-packages/vnpy/event/engine.py", line 75, in _process [handler(event) for handler in self._handlers[event.type]] ~~~~~~~^^^^^^^ File "/root/ag_ctp_quant/run/collect_vnpy.py", line 122, in on_tick bar_agg.update(tick) ~~~~~~~~~~~~~~^^^^^^ File "/root/ag_ctp_quant/realtime/bar_agg.py", line 29, in update f"bar: {bar['datetime']} " ^^^ NameError: name 'bar' is not defined The `NameError` occurs because the variable `bar` is referenced inside the `update` method of `realtime/bar_agg.py` but has not been defined in the local or class scope. Without seeing the source code, the most probable cause is a missing `self.` prefix: if your class maintains the current bar as an instance attribute (e.g., `self.bar`), the line should be: ```python f"bar: {self.bar['datetime']} " ``` instead of: ```python f"bar: {bar['datetime']} " ``` **How to fix:** 1. Open `realtime/bar_agg.py`. 2. Locate line 29 (or the line that contains `f"bar: {bar['datetime']} "`). 3. Check the surrounding context. If the class stores the in‑progress bar as `self.bar`, change `bar` to `self.bar`. If the bar is held under a different name (e.g., `_current_bar`, `bar_data`), use that name with `self.` if it’s an instance variable. 4. If the code is generating multiple bars inside a loop, make sure the loop variable is properly named and accessible in that scope. **Additional suggestion:** The error happens inside an event handler running in a thread. You can wrap the handler body in a `try‑except` block to log failures without crashing the thread: ```python def on_tick(self, tick): try: bar_agg.update(tick) except Exception: logger.exception("Error processing tick") ``` This will keep the event engine running even if the bar aggregator encounters an unexpected tick or bug. 用户: # run/collect_vnpy.py from realtime.bar_agg import BarAggregator from realtime.features import FeatureEngine from realtime.signal_engine import SignalEngine import time from datetime import datetime from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy_ctp import CtpGateway from vnpy.trader.object import SubscribeRequest from vnpy.trader.event import EVENT_TICK from ctp.collector import TickCollector from vnpy.trader.object import SubscribeRequest from vnpy.trader.constant import Exchange # ========= 配置 ========= SETTING = { "用户名": "****", "密码": "****", "经纪商代码": "9999", "交易服务器": "tcp://182.254.243.31:40001", "行情服务器": "tcp://182.254.243.31:40011", "产品名称": "", "授权编码": "", } SYMBOL = "ag2606" # 🔥改成当前主力 EXCHANGE = "SHFE" # ========= 初始化 ========= collector = TickCollector(flush_size=500) event_engine = EventEngine() main_engine = MainEngine(event_engine) main_engine.add_gateway(CtpGateway) bar_agg = BarAggregator(window_sec=3) feat_engine = FeatureEngine(window=20) sig_engine = SignalEngine() # ========= Tick回调 ========= def on_bar(bar): features = feat_engine.update(bar) signal = sig_engine.on_feature(features) if signal: print(f"[SIGNAL] {bar['datetime']} -> {signal} | price={bar['close']}") bar_agg.on_bar = on_bar last_tick = None def on_tick(event): global last_tick t = event.data # ✅ 原始tick(只放交易所数据)i raw_tick = { "symbol": t.symbol, "datetime": t.datetime, "price": t.last_price, "volume": t.volume, "bid_price1": t.bid_price_1, "ask_price1": t.ask_price_1, "bid_vol1": t.bid_volume_1, "ask_vol1": t.ask_volume_1, "buy_vol": 0, "sell_vol": 0, } # ✅ 用于计算的tick(拷贝一份) tick = raw_tick.copy() # 🔥 === v2关键:推断主动成交方向 === if last_tick: vol_diff = raw_tick["volume"] - last_tick["volume"] # 防止跨日错误 if vol_diff < 0: vol_diff = 0 if vol_diff > 0: if raw_tick["price"] >= last_tick["ask_price1"]: tick["buy_vol"] = vol_diff tick["sell_vol"] = 0 elif raw_tick["price"] <= last_tick["bid_price1"]: tick["buy_vol"] = 0 tick["sell_vol"] = vol_diff else: tick["buy_vol"] = vol_diff * 0.5 tick["sell_vol"] = vol_diff * 0.5 else: tick["buy_vol"] = 0 tick["sell_vol"] = 0 else: tick["buy_vol"] = 0 tick["sell_vol"] = 0 #打印 tick(检查订单流是否生效) print(tick["buy_vol"], tick["sell_vol"]) # 🔥 关键:只保存“干净tick” last_tick = raw_tick # 推送给bar # 打印少量确认 if int(time.time()) % 5 == 0: print("tick:", tick["datetime"],tick["volume"],tick["symbol"], tick["price"]) collector.on_tick(tick) bar_agg.update(tick) # 注册事件 event_engine.register(EVENT_TICK, on_tick) # ========= 启动 ========= print("Connecting CTP...") main_engine.connect(SETTING, "CTP") # 等待连接 time.sleep(5) # 订阅行情 req = SubscribeRequest( symbol="ag2606", exchange=Exchange.SHFE ) main_engine.subscribe(req, "CTP") print("Subscribed:", SYMBOL) # ========= 主循环 ========= try: while True: time.sleep(1) except KeyboardInterrupt: print("Stopping...") collector.flush() 你的脚本整体思路非常清晰:CTP 行情接入 → Tick 方向推断 → Bar 聚合 → 特征计算 → 信号生成。不过实现中有几个地方可能会引发运行时错误或让逻辑出现预期外的行为,值得改进。 --- ### 1. 致命错误:`Exchange` 未导入 你使用了 `Exchange.SHFE`,但没有导入 `Exchange` 类。 ```python from vnpy.trader.object import SubscribeRequest from vnpy.trader.constant import Exchange # 这一行缺失 ``` **后果**:`NameError: name 'Exchange' is not defined`,脚本无法运行。 --- ### 2. 多余的 `SubscribeRequest` 实例 你创建了两次 `SubscribeRequest`,第一次没有使用,可以删除: ```python # 删除这个 req = SubscribeRequest( symbol="ag2606", exchange=Exchange.SHFE ) ``` --- ### 3. Tick 主动买卖方向推断的风险 ```python if raw_tick["price"] >= last_tick["ask_price1"]: tick["buy_vol"] = vol_diff elif raw_tick["price"] <= last_tick["bid_price1"]: tick["sell_vol"] = vol_diff else: tick["buy_vol"] = vol_diff * 0.5 tick["sell_vol"] = vol_diff * 0.5 ``` **潜在问题**: - 使用了 **上一笔 tick 的盘口** 作为判断基准,而当前盘口可能已经移动。 - 当成交价位于中间(恰好等于上一笔的买一或卖一?)时,逻辑会漏掉边界情况:比如 `price == last_tick["bid_price1"]` 应该算主动卖(你的 `elif` 条件正确处理了 `<=`,但 `== ask` 会走 `>=`,也就是主动买,这是合理的)。 - **跨日 / 行情断档** 时,`vol_diff` 可能变负,你强制设为 0,但 `last_tick` 的数据已经“过期”,会导致方向推断完全错误。 建议在检测到 `vol_diff < 0` 或时间跨度过大时,直接重置 `last_tick`,并对这一笔 tick 不做推断。 --- ### 4. 控制台打印过于频繁 这里有 **两处打印**: - `print(tick["buy_vol"], tick["sell_vol"])` —— 每笔 tick 都打印,瞬间刷屏。 - `if int(time.time()) % 5 == 0: print(...)` —— 每秒内的所有 tick 都会在 5 倍秒数时打印,依然很多。 建议改成 **每秒只打印一次**,或用 `logging` 控制级别。 --- ### 5. `last_tick` 没有初始防御 `on_tick` 第一次进入时 `last_tick = None`,你做了判断,使用 `else: tick["buy_vol"] = 0`,没问题。但后续所有 tick 都依赖 `last_tick`,如果 `last_tick` 被意外置为 `None`(比如异常处理),就会报错。建议增强健壮性。 --- ### 6. Bar 聚合器只传 `tick` 字典 `bar_agg.update(tick)` 需要 `tick` 包含 `datetime`、`price`、`volume`、`buy_vol`、`sell_vol` 等字段,目前你是满足的。但注意 `datetime` 是 CTP 的 `datetime` 对象,可能存在时区问题,确保后续特征计算能正确处理。 --- ### 改进后的关键代码示例 ```python from vnpy.trader.constant import Exchange import time from datetime import datetime # 补全 import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s') logger = logging.getLogger(__name__) last_tick = None last_print_time = 0 def on_tick(event): global last_tick, last_print_time t = event.data raw_tick = { "symbol": t.symbol, "datetime": t.datetime, "price": t.last_price, "volume": t.volume, "bid_price1": t.bid_price_1, "ask_price1": t.ask_price_1, "bid_vol1": t.bid_volume_1, "ask_vol1": t.ask_volume_1, "buy_vol": 0.0, "sell_vol": 0.0, } # 推断主动成交量 if last_tick is None: tick = raw_tick.copy() last_tick = raw_tick return # 第一笔不做聚合,等待下一条 vol_diff = raw_tick["volume"] - last_tick["volume"] if vol_diff < 0: # 跨日或数据异常,重置 last_tick last_tick = raw_tick return if vol_diff > 0: if raw_tick["price"] >= last_tick["ask_price1"]: raw_tick["buy_vol"] = vol_diff elif raw_tick["price"] <= last_tick["bid_price1"]: raw_tick["sell_vol"] = vol_diff else: raw_tick["buy_vol"] = vol_diff * 0.5 raw_tick["sell_vol"] = vol_diff * 0.5 tick = raw_tick.copy() last_tick = raw_tick # 控制打印频率(每秒最多一条) now = int(time.time()) if now - last_print_time >= 1: logger.info(f"tick: {tick['datetime']} vol:{tick['volume']} price:{tick['price']} " f"buy:{tick['buy_vol']:.0f} sell:{tick['sell_vol']:.0f}") last_print_time = now collector.on_tick(tick) bar_agg.update(tick) ``` --- ### 建议的后续优化 1. **BarAggregator** 的 `window_sec=3` 对于白银这样的合约是否合理?如果做短线信号可以,长周期特性可能需要更长时间窗口。 2. **FeatureEngine** 和 **SignalEngine** 目前未展示实现,确保它们能处理 `None` 值(比如历史数据不足时)。 3. 加入 **断线重连** 逻辑,否则 CTP 断开后脚本会静默停止。 4. 考虑将 `TickCollector` 的 `flush` 与数据库写入加上异常保护,避免单次写库失败导致数据丢失。 --- 你需要我继续深入 review `BarAggregator` 或 `SignalEngine` 的设计吗?可以贴出对应代码,我帮你检查一致性和潜在逻辑漏洞。 用户: :~/ag_ctp_quant# cat realtime/bar_agg.py from datetime import datetime, timedelta class BarAggregator: def __init__(self, window_sec=3, on_bar=None): self.window = timedelta(seconds=window_sec) self.on_bar = on_bar self.current_bar = None self.bar_start = None def update(self, tick): dt = tick["datetime"] if self.bar_start is None: self.bar_start = dt self.current_bar = self._new_bar(tick) return # 是否跨bar if dt - self.bar_start >= self.window: # 推送旧bar if self.on_bar: # bar生成完 成 调试信息 print( f"bar: {bar['datetime']} " f"buy={bar['buy_vol']} " f"sell={bar['sell_vol']} " f"tick={bar['tick_count']}" ) self.on_bar(self.current_bar) # 新bar self.bar_start = dt self.current_bar = self._new_bar(tick) return # 更新bar b = self.current_bar b["high"] = max(b["high"], tick["price"]) b["low"] = min(b["low"], tick["price"]) b["close"] = tick["price"] b["volume"] += tick["volume"] # 🔥 保留最后盘口 b["bid_price1"] = tick["bid_price1"] b["ask_price1"] = tick["ask_price1"] b["bid_vol1"] = tick["bid_vol1"] b["ask_vol1"] = tick["ask_vol1"] b["buy_vol"] += tick.get("buy_vol", 0) b["sell_vol"] += tick.get("sell_vol", 0) b["tick_count"] += 1 def _new_bar(self, tick): return { "datetime": tick["datetime"], "open": tick["price"], "high": tick["price"], "low": tick["price"], "close": tick["price"], "volume": tick["volume"], "bid_price1": tick["bid_price1"], "ask_price1": tick["ask_price1"], "bid_vol1": tick["bid_vol1"], "ask_vol1": tick["ask_vol1"], "buy_vol": 0, "sell_vol": 0, "tick_count": 0, } 就是这个文件,哪里要改 这段代码里有个明显的**变量名错误**:在推送旧 bar 的 `print` 中使用了未定义的 `bar`,应该改为 `self.current_bar`(或 `b`)。 具体位置在 `update` 方法中: ```python if dt - self.bar_start >= self.window: if self.on_bar: # 这里 bar 未定义,应改为 self.current_bar print( f"bar: {bar['datetime']} " # ❌ bar 未定义 f"buy={bar['buy_vol']} " f"sell={bar['sell_vol']} " f"tick={bar['tick_count']}" ) self.on_bar(self.current_bar) ... ``` **修正写法**(直接引用已有的 `self.current_bar`): ```python bar = self.current_bar print( f"bar: {bar['datetime']} " f"buy={bar['buy_vol']} " f"sell={bar['sell_vol']} " f"tick={bar['tick_count']}" ) ``` 或者直接使用 `self.current_bar`。 --- ### 其他可优化的点(非错误,但可改进): 1. **Bar 对齐** 当前新 bar 的开始时间直接设为触发切分的 `tick["datetime"]`,会导致相邻 bar 之间可能存在时间空隙(当两个 tick 间隔超过 `window` 时)。如果你希望 bar 在时间轴上连续且对齐,可以用: ```python self.bar_start = dt - timedelta(seconds=(dt.timestamp() % self.window.total_seconds())) ``` 这会向下取整到最近的窗口起点。 2. **状态更新前应复制或暂存旧 bar** 在调用 `self.on_bar` 之后才创建新 bar,没有问题。但注意打印和回调使用的是同一个字典对象,如果在 `on_bar` 内部修改了字典,可能影响后续新 bar 的初始值。可以考虑传递字典的副本:`self.on_bar(self.current_bar.copy())`。 3. **初始 bar 的计数** `_new_bar` 中 `buy_vol`、`sell_vol`、`tick_count` 初始化为 0,正确。但要注意 tick 可能缺少某些字段,`get` 兜底合理。 目前**最需要立刻修改的就是打印里的变量名错误**,否则运行会抛出 `NameError`。 用户: ~/ag_ctp_quant# ./runcollect_vnpy.sh Connecting CTP... 2026-05-06 01:55:40.782 | INFO | MainEngine | 连接登录 -> CTP 2026-05-06 01:55:40.813 | INFO | CTP | 行情服务器连接成功 2026-05-06 01:55:40.816 | INFO | CTP | 交易服务器连接成功 2026-05-06 01:55:40.822 | INFO | CTP | 行情服务器登录成功 2026-05-06 01:55:40.826 | INFO | CTP | 交易服务器登录成功 2026-05-06 01:55:40.835 | INFO | CTP | 结算信息确认成功 2026-05-06 01:55:41.304 | INFO | CTP | 合约信息查询成功 Subscribed: ag2606 2026-05-06 01:55:45.783 | INFO | MainEngine | 订阅行情 -> CTP:SubscribeRequest(symbol='ag2606', exchange=) Exception in thread Thread-1 (_run): Traceback (most recent call last): File "/root/.local/share/uv/python/cpython-3.13.13-linux-x86_64-gnu/lib/python3.13/threading.py", line 1044, in _bootstrap_inner self.run() ~~~~~~~~^^ File "/root/.local/share/uv/python/cpython-3.13.13-linux-x86_64-gnu/lib/python3.13/threading.py", line 995, in run self._target(*self._args, **self._kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/root/ag_ctp_quant/.venv/lib/python3.13/site-packages/vnpy/event/engine.py", line 62, in _run self._process(event) ~~~~~~~~~~~~~^^^^^^^ File "/root/ag_ctp_quant/.venv/lib/python3.13/site-packages/vnpy/event/engine.py", line 75, in _process [handler(event) for handler in self._handlers[event.type]] ~~~~~~~^^^^^^^ File "/root/ag_ctp_quant/run/collect_vnpy.py", line 136, in on_tick if now - last_print_time >= 3: ^^^^^^^^^^^^^^^ UnboundLocalError: cannot access local variable 'last_print_time' where it is not associated with a value ^CStopping... ^CTraceback (most recent call last): File "/root/.local/share/uv/python/cpython-3.13.13-linux-x86_64-gnu/lib/python3.13/threading.py", line 1543, in _shutdown _thread_shutdown() KeyboardInterrupt: 你遇到的错误是由于在 `on_tick` 方法内引用了变量 `last_print_time`,但该变量尚未被赋值或未被正确传递。具体来看: ```python if now - last_print_time >= 3: ^^^^^^^^^^^^^^^ UnboundLocalError: cannot access local variable 'last_print_time' where it is not associated with a value ``` ### 错误原因 - Python 将 `last_print_time` 视为 **局部变量**,因为可能在 `on_tick` 方法内部存在对该变量的赋值操作(例如 `last_print_time = ...`),但在执行 `if` 判断时,这个赋值操作还没运行,导致变量未定义就引用了。 - 更常见的情况是:代码想使用一个**实例属性** `self.last_print_time`,但忘记加 `self`,直接写了 `last_print_time`,从而被当作局部变量处理。如果完全没有其他地方初始化,就会出现这个错误。 ### 修复方法 请检查你的 `collect_vnpy.py` 文件,通常在策略类(或数据收集类)的 `__init__` 中初始化打印时间记录,并在 `on_tick` 中通过 `self.last_print_time` 访问。 **正确的写法示例:** ```python class MyStrategy: def __init__(self, ...): ... self.last_print_time = 0 # 初始化,记录上次打印的 unix 时间 def on_tick(self, tick): now = time.time() if now - self.last_print_time >= 3: # 使用 self. print(f"Tick: {tick.symbol} {tick.last_price}") self.last_print_time = now # 更新也需 self. ``` ### 注意事项 1. 如果你原本使用了全局变量 `last_print_time`,请在函数开头用 `global last_print_time` 声明,但不推荐在策略中这样做。 2. 如果代码里确实有赋值语句却写在了判断之后,请将初始化移至 `__init__` 或确保赋值在引用之前执行。 修改后重新运行脚本,错误应该消失。如果还有其他地方引用了未绑定的本地变量,同理修正即可。