简介一份面向股票接口开发与量化学习者的 JavaWeb 结合 Python 的同花顺实盘接口源码适配项目开发与学习研究场景也适合完成课程设计或入门实盘接口联调无论是用于毕业设计还是业余量化探索。资源包共 736 个文件、约 7.36MB文件类型以 672 个 Java 文件为主配以 24 个 jar 依赖包、9 个 xml 配置、9 个 js、5 个 css 及多个 HTML 页面共同支撑前端展示另含 JSP 页面与 1 个 Python 脚本覆盖后端逻辑、动态页面、样式脚本和跨语言调用配置工程结构相对完整。源码已经过严格测试可直接运行并在此基础上延伸功能例如接入自身策略、调整交易参数、扩展监控界面或替换示例页面方便在真实模拟环境中观察 JavaWeb 与 Python 的分工协作。目前已有 688 人浏览学习资源包含前后端页面样式、Java 核心逻辑、接口调用示例与工程化配置便于理解同花顺实盘接口对接时的数据流转与模块划分。1. 用 JavaWeb 结合 Python 开发同花顺实盘接口先把技术栈的“分工”想清楚这个标题拆开看其实是在问一件事同花顺的实盘下单能力怎么用 JavaWeb 把外围系统用户、权限、策略配置、交易记录管起来同时用 Python 去干真正跟行情、策略、下单执行相关的活。我见过不少团队一开始用纯 Java 写策略写到最后发现 pandas、numpy 的生态用不上量化模型根本跑不动也见过有人用纯 Python 写全套 Web结果权限控制、事务管理、前端对接做得特别痛苦。混合栈的价值就在于把两边的优势各取所长JavaWeb 管业务和展示Python 管策略和交易执行中间用 HTTP 协议做松耦合对接。这套方案适合三类人一是已经有了 JavaWeb 交易后台、想把实盘下单能力接进来的团队二是做量化策略研究、需要把回测信号落成真实委托的个人开发者三是想拿这个项目练手、理解“业务系统与交易执行分离”架构的学生。2. 整体架构与通信方案JavaWeb 和 Python 之间到底怎么“说话”2.1 为什么不能把下单逻辑直接写进 JavaWeb很多第一次接触这个项目的人会问既然 JavaWeb 都能调第三方 HTTP 接口为什么不能直接用 Java 去操作同花顺客户端下单非要再包一层 Python答案很现实同花顺的实盘下单场景里几乎找不到一个官方的、稳定的 Java SDK。市场上常见的落地方式有两种一种是调用同花顺提供的中间件接口但这类中间件往往只提供 Windows 版本而且协议文档并不公开Java 这边解析起来很吃力另一种更普遍的做法是通过 Python 生态里的自动化库去驱动同花顺客户端界面执行下单比如模拟键盘鼠标操作、读取窗口控件、抓取委托列表。这条路能走通靠的是 Python 在 Windows GUI 自动化上积累的大量现成库Java 在这块的生态要弱得多。所以架构上最合理的选择是JavaWeb 不碰同花顺客户端它只负责接收前端用户的策略配置、买卖信号、资金和持仓展示请求然后把“我要买 100 股某某股票”这个指令封装成 JSON通过 HTTP POST 发给 Python 服务Python 服务拿到指令后解析参数、检查资金、调用自动化模块去同花顺客户端执行操作再把执行结果委托编号、状态、成交价格返回给 JavaWeb。两条进程各自独立部署Java 挂了不影响 Python 已经在执行的委托Python 崩溃了 JavaWeb 还能把信号存进队列等它恢复。这个“业务与执行分离”的思路是这个项目最核心的架构决策。2.2 通信协议选型REST 短连接 本地任务队列我一般会建议在本地部署场景下用 REST 短连接做主通信而不是 WebSocket 长连接或者消息队列。原因很实际同花顺实盘下单是一个低频操作用户一天可能也就下几十次单每次请求从发出到拿到回执只有几百毫秒用 HTTP 同步请求完全够用而且出错时排查起来最直观——打开日志看到一条 POST /api/trade/order 的记录响应体里带着成功或失败的原因链路非常清楚。用 WebSocket 看起来更“实时”但 JavaWeb 和 Python 服务都跑在同一台机器上时长连接维护的成本心跳、断线重连、消息顺序对这个小项目来说是净负担。不过信号不能直接裸调我通常会在 JavaWeb 侧加一个内存任务队列把用户提交的买卖请求先落库状态为 PENDING再异步发送给 Python。这样做的原因是同花顺客户端在极少数情况下会弹窗卡住如果 JavaWeb 同步阻塞等待用户在前端点完买入按钮后页面会一直转圈体验很差。异步化之后JavaWeb 立即返回“指令已受理”后台轮询 Python 回执更新订单状态前端再用 WebSocket 推送状态变化。任务队列的另外一个好处是天然具备重试能力Python 服务暂时没起来时任务留在队列里等服务恢复后重新投递不会丢单。2.3 Python 服务的技术骨架Flask 接收指令 独立执行线程Python 侧的服务我习惯用轻量级框架搭Flask 或者 FastAPI 都行看团队熟哪个。核心结构是一个 HTTP 接口接收 JavaWeb 的指令校验通过后把指令放进 Python 内部的线程安全队列由专门的下单执行线程串行消费。为什么不能直接在 HTTP 请求处理函数里去操作同花顺客户端因为同花顺客户端界面不支持并发操作——如果两个请求同时去点击买入按钮窗口焦点会打架轻则点击无效重则下错单。串行化之后所有委托排队执行每次操作完等待客户端界面刷新再执行下一个安全性才有保障。下单执行线程的核心逻辑是从队列取指令 → 激活同花顺客户端主窗口 → 切换到买入/卖出界面 → 输入证券代码和数量 → 点击买入/卖出按钮 → 等待委托确认弹窗 → 点击确认 → 读取委托编号 → 把结果写回 HTTP 响应。每一步之间必须加显式等待不能只靠固定 time.sleep要轮询窗口控件的状态变化否则界面响应慢的时候程序已经去点击下一个控件了必然翻车。3. 从信号到委托Python 端下单模块的落地代码与参数说明3.1 最小可跑的指令接收服务先给一个最小可跑的 Python 服务骨架它接收 JavaWeb 发来的下单指令把指令丢进队列并立即返回受理回执from flask import Flask, request, jsonify import queue import threading import uuid # JavaWeb - Python 的指令队列 order_queue queue.Queue() # 用于存储任务状态的内存字典生产环境可以换成 Redis task_status {} app Flask(__name__) app.route(/api/trade/order, methods[POST]) def receive_order(): 接收 JavaWeb 的下单指令校验后入队。 data request.get_json(forceTrue) # 必填字段校验 required [action, stock_code, quantity] for field in required: if field not in data: return jsonify({code: 40001, message: fmissing field: {field}}), 400 # 生成任务ID用于后续状态查询和日志串联 task_id uuid.uuid4().hex task { task_id: task_id, action: data[action], # buy / sell stock_code: data[stock_code], # 例如 600519 quantity: int(data[quantity]), # 股数A股一般为100整数倍 price: data.get(price, None), # 限价单价格None表示市价 entrust_type: data.get(entrust_type, limit), # limit / market } task_status[task_id] PENDING order_queue.put(task) return jsonify({code: 0, message: accepted, task_id: task_id}), 200 app.route(/api/trade/status/task_id, methods[GET]) def query_status(task_id): JavaWeb 轮询查询任务状态。 status task_status.get(task_id, NOT_FOUND) return jsonify({task_id: task_id, status: status}), 200 if __name__ __main__: # 本地服务绑定到回环地址即可 app.run(host127.0.0.1, port8765, debugFalse)这段代码的逻辑很直白receive_order是 JavaWeb 调用的唯一入口所有字段校验、任务入队都在这里完成。quantity转成 int 是防止前端传字符串价格字段在限价单时必须提供市价单时传 None。task_status内存字典在真实项目里要替换成 Redis 或数据库否则 Python 服务一重启JavaWeb 再查状态就只能得到 NOT_FOUND。order_queue用 Python 标准库的queue.Queue它是线程安全的下单执行线程可以直接get()阻塞等待不需要自己加锁。3.2 通过 Windows 自动化驱动同花顺客户端下单接下来是最关键的执行模块。这里以 Windows 环境下常见的 pywinauto 库为例另一个常见选择是 pyautogui但 pywinauto 对控件级别的定位更可靠代码要处理窗口激活、控件定位、输入和点击确认import time import pywinauto from pywinauto import Application, keyboard # 同花顺客户端主窗口标题不同版本可能有区别需要按实际环境调整 MAIN_WINDOW_TITLE 同花顺 def bring_trade_window_to_front(): 激活同花顺主窗口确保后续键鼠操作落在正确窗口上。 app Application(backendwin32).connect(title_re.*同花顺.*, timeout10) main_win app.top_window() main_win.set_focus() return main_win def switch_to_trade_panel(main_win): 切换到买入/卖出面板。常见做法是通过快捷键同花顺默认 F9 是交易。 main_win.type_keys({F9}) time.sleep(1.5) # 等待交易面板加载 def send_order(stock_code, quantity, actionbuy, priceNone): 执行一次下单操作。参数含义 - stock_code: 6位证券代码如 600519 - quantity: 委托股数必须是100的整数倍科创板、北交所除外 - action: buy买入, sell卖出 - price: 限价单价格为空时用市价单 # 第一步激活客户端 main_win bring_trade_window_to_front() switch_to_trade_panel(main_win) # 第二步定位到证券代码输入框并输入代码 code_edit main_win.child_window(title_re.*代码.*, control_typeEdit) code_edit.set_edit_text() # 先清空 code_edit.set_edit_text(stock_code) # 再输入 # 第三步定位到数量输入框输入股数 qty_edit main_win.child_window(title_re.*数量.*, control_typeEdit) qty_edit.set_edit_text() qty_edit.set_edit_text(str(quantity)) # 第四步定位到价格输入框限价单需要填价格 if price is not None: price_edit main_win.child_window(title_re.*价格.*, control_typeEdit) price_edit.set_edit_text() price_edit.set_edit_text(str(price)) # 第五步点击买入/卖出按钮 if action buy: btn main_win.child_window(title_re.*买入.*, control_typeButton) else: btn main_win.child_window(title_re.*卖出.*, control_typeButton) btn.click() # 第六步处理确认弹窗 time.sleep(0.5) confirm main_win.child_window(title_re.*确认.*, control_typeButton) if confirm.exists(timeout3): confirm.click() # 第七步等待委托编号出现轮询委托列表或弹窗文本 time.sleep(1.0) return {code: 0, message: order sent}这段代码里最容易踩坑的是child_window的匹配条件。不同版本的同花顺客户端控件标题可能是“证券代码”“代码”“股票代码”甚至可能是没有标题只靠control_type定位。所以我强烈建议在写死代码之前先用 pywinauto 自带的窗口探测器把目标控件的属性抓一遍确认title、class_name、control_type三个字段的真实值。set_edit_text()这一步是必须的它负责把输入框里的旧内容清空。很多人第一次写的时候只调用一次set_edit_text(stock_code)结果如果输入框里原本有上次交易留下的代码新代码就会追加在后面客户端直接报“证券代码不存在”。还有一点确认弹窗不一定每次都出现——如果资金充足且不触发风控规则部分版本客户端会直接提交委托。所以confirm.exists(timeout3)用的是“存在才点击”的写法而不是强制等待点击否则弹窗没出现时程序会卡死在找按钮上。3.3 JavaWeb 侧的任务投递与状态轮询JavaWeb 侧的核心是封装一个下单服务把用户请求落库后异步投递给 Python并定时轮询状态。关键代码示意// 订单状态下单服务 Service public class OrderDispatchService { // Python 服务的地址本地部署 private static final String PYTHON_ORDER_URL http://127.0.0.1:8765/api/trade/order; private final RestTemplate restTemplate new RestTemplate(); public String dispatchOrder(OrderRequest req) { // 1. 生成内部订单号 String orderId UUID.randomUUID().toString().replace(-, ); // 2. 落库状态为 PENDING orderMapper.insert(orderId, req.getAction(), req.getStockCode(), req.getQuantity(), req.getPrice(), PENDING); // 3. 构造发往 Python 的 JSON MapString, Object body new HashMap(); body.put(action, req.getAction()); body.put(stock_code, req.getStockCode()); body.put(quantity, req.getQuantity()); body.put(price, req.getPrice()); // 4. 异步发送发送失败不阻塞用户 asyncExecutor.submit(() - sendAndUpdateStatus(orderId, body)); return orderId; } private void sendAndUpdateStatus(String orderId, MapString, Object body) { try { HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityMapString, Object entity new HttpEntity(body, headers); ResponseEntityMap resp restTemplate.postForEntity(PYTHON_ORDER_URL, entity, Map.class); if (resp.getBody() ! null resp.getBody().get(code).equals(0)) { orderMapper.updateStatus(orderId, ACCEPTED, (String) resp.getBody().get(task_id)); } else { orderMapper.updateStatus(orderId, REJECTED, null); } } catch (Exception e) { // 网络异常或 Python 未启动保留 PENDING 状态等待重试 orderMapper.updateStatus(orderId, PENDING, null); } } }这里有一个细节sendAndUpdateStatus里捕获到异常时把状态保持为 PENDING而不是直接标记失败。我一般会配合一个定时任务每隔 30 秒扫描一次还停留在 PENDING 状态且创建时间超过 1 分钟的订单重新发起投递。这就是前面说的“任务队列天然具备重试能力”的落地形式。如果重试超过 3 次仍然失败再把状态置为 FAILED并通过 WebSocket 推送给前端提示用户检查 Python 服务是否在线。4. 实盘对接的细节账户、资金、委托状态与边界约束4.1 资金与持仓校验不能只靠同花顺客户端弹窗提示把下单指令发给 Python 之前JavaWeb 侧必须先做一道资金持仓前置校验。很多新手直接跳过这一步认为同花顺客户端在下单时会自动校验资金够不够、持仓够不够。实际操作中客户端确实会弹窗提示“可用资金不足”或“持仓不足”但那时候委托已经被拒用户体验很不好。更关键的是如果你的策略程序在循环下单一次拒绝不会中断后续指令可能连续弹十几个错误窗口把客户端界面卡死。所以我在 JavaWeb 侧会维护一个本地账户状态表定时从同花顺客户端抓取可用资金和持仓数据存下来。每次用户或策略发起委托时先查本地表做预校验买入时检查quantity * price available_cash卖出时检查quantity available_position。预校验通过后才把指令发给 Python。这样做的另一个好处是减少了对客户端界面的操作次数毕竟每次查询资金都会让客户端界面切来切去操作太频繁容易被同花顺识别为异常行为。4.2 委托状态同步从 ACCEPTED 到 FILLED 的完整状态机一个委托从提交到成交在项目中至少有这几个状态需要管理PENDINGJavaWeb 已受理、ACCEPTEDPython 已接收、SENT已在客户端提交、PARTIAL_FILLED部分成交、FILLED全部成交、REJECTED被拒绝、CANCELED已撤单。JavaWeb 和 Python 之间只靠一次 HTTP 回执能覆盖的只有 ACCEPTED 这一步后面的状态必须靠轮询获取。我常用的做法是Python 服务每 5 秒读取一次同花顺客户端的委托列表和成交列表把变化推送到 JavaWeb 的/api/trade/status/callback接口JavaWeb 收到后更新本地订单状态并触发持仓资金表的刷新。为什么轮询周期设成 5 秒而不是更短因为同花顺客户端的委托列表刷新本身有延迟读得太快拿到的还是旧快照白白增加 CPU 占用5 秒是一个实测下来比较稳的折中值。如果对状态实时性要求高可以缩短到 2 秒但不建议低于 1 秒。4.3 边界约束A股交易规则必须在代码里写死同花顺客户端会在界面层面拦截违反交易规则的委托但项目代码里还是要自己实现一套校验器因为客户端拦截只发生在操作那一刻而我们要在信号生成阶段就排除掉无效委托。至少这几条规则要写进代码买入数量必须是 100 股的整数倍科创板最低 200 股超过部分可以按 1 股递增卖出时如果持仓不足 100 股必须一次性卖出全部零股买入价格不能超过当前涨停价卖出价格不能低于跌停价当天买入的股票不能当天卖出T1。这些规则属于“客户端会兜底但你不该依赖客户端”的边界写在校验器里能让整个系统的行为更可预期。这些约束还有一个作用防止策略程序在特殊行情下产生极端委托。比如股票快速拉升时策略算出买入价已经超过涨停价如果不拦截直接发给同花顺客户端会弹窗提示价格异常还需要人工点掉模拟盘也就罢了实盘环境下这种弹窗会让程序卡住。5. 实盘联调避坑指南5 个高频翻车场景与排查方法5.1 窗口定位失败找不到同花顺主窗口现象Python 启动后执行下单抛WindowNotFoundErrorJavaWeb 侧看到的任务状态一直是 PENDING重试后还是失败。原因同花顺客户端启动后通常有多个顶层窗口主窗口标题也可能因为版本差异不是“同花顺”三个字另外 pywinauto 的connect(title_re...)匹配的是窗口标题如果客户端开的是自定义标题栏主题正则匹配就会失败。解决先写一段独立的探测脚本枚举所有顶层窗口的标题和类名确认实际值后调整title_re。我一般用pywinauto.findwindows.find_windows()打印全部窗口句柄找到交易主窗口对应的class_name之后改用class_name匹配比标题匹配稳定得多。5.2 输入框内容被追加而不是覆盖现象下单请求提交的是 600519实际客户端委托列表里出现的证券代码是 600519600519委托直接被拒。原因输入框在第二次赋值时没有清空旧值。set_edit_text在部分控件上等价于“追加文本”而不是“替换全文”。解决每次输入前先调一次set_edit_text()清空后再输入新值。如果清空后仍然出现残留改用键盘组合键先点击输入框发送CtrlA全选再直接键入新文本。5.3 委托确认弹窗偶尔不出现导致脚本卡死现象用main_win.child_window(...).click()时弹窗没出现脚本抛出ElementNotFoundError但 JavaWeb 已经收到了 Python 的异常回执订单状态变成 REJECTED实际上委托可能已经提交成功。原因客户端在资金充裕、没有触发风控的情况下可能省略确认弹窗直接提交。代码里如果把点击确认当成必选步骤就会在弹窗缺失时误报失败。解决改成“存在才点击”的模式配合超时判断。同时在下单完成后增加一个“验证步骤”——读取委托列表最新一条记录确认证券代码和数量与请求一致只有验证通过才返回成功。5.4 JavaWeb 重启导致内存队列丢失现象某次服务器重启后用户之前提交的委托没有任何回执JavaWeb 的订单表里全是 PENDING。原因前面用了内存队列OrderDispatchService里的异步线程池随着 JVM 一起销毁队列任务全部丢失。解决落库优先——用户提交请求后立即写库状态为 PENDING之后再异步投递。JVM 重启后启动一个ApplicationRunner扫描所有 PENDING 超过 1 分钟的订单重新投递到 Python。这样内存队列只扮演“缓冲”角色而不是“存储”角色。5.5 模拟盘与实盘环境切换误操作现象开发时一直连着同花顺模拟盘调试某次直接把配置文件里的环境标识改成“实盘”策略程序跑起来后发现真实资金账户被下单。原因同花顺客户端登录时选择的是实盘账户但代码里环境标识和客户端实际账号没有做一致性校验切换环境只改配置不验证。解决在 JavaWeb 启动时强制校验环境标识如果配置为“实盘”必须额外输入二次确认口令才能启动交易模块同时 Python 下单执行前也读取同一个环境配置文件检测到“实盘”时先弹出一分钟倒计时提示框超时无操作自动取消本次启动。这个习惯我在自己的项目里坚持了一年多没有一次误操作。6. 进阶技巧用历史行情回放把下单链路完整验证一遍实盘接口开发完第一件要做的事不是真金白银去下单而是做一个“回放验证模式”。这个模式的核心是把同花顺客户端的真实下单模块替换成一个模拟执行器JavaWeb 和 Python 的代码路径完全不变只有最底层的send_order函数被替换成写入本地 csv 文件。我一般会这样实现在 Python 端定义一个OrderExecutor抽象基类真实环境用THSEmulator回放模式用FileRecorderExecutor。回放模式下receive_order接口照常接收 JavaWeb 的指令校验逻辑全走队列照常消费只是最后一步不是点击客户端窗口而是把指令追加到orders_replay.csv然后直接返回成功状态。这样就能用历史行情数据生成买卖信号跑一遍完整的 JavaWeb → Python → 回执 → 状态更新链路看数据是否一致。回放验证能抓出两类最隐蔽的问题。第一类是状态机问题比如某个委托在回执返回成功但状态更新失败时订单表里会不会出现 FILLED 状态的订单却没有成交记录第二类是并发问题连续多笔委托同时到达时Python 串行队列是否真的按顺序处理JavaWeb 的异步投递是否出现乱序。这两类问题在真实下单时出现任何一次都代价极高回放模式能低成本地把它们暴露出来。另外一个我长期在用的习惯是给整个链路加一个链路追踪 ID。JavaWeb 生成内部订单号时同时生成一个trace_id写入日志头投递给 Python 时带上同一个 IDPython 端所有日志行也强制携带trace_id。这样排查问题时只需要在 JavaWeb 日志里搜索订单号拿到trace_id再去 Python 日志里过滤同一 ID整条链路的每一步耗时、参数、异常全部串在一起。这可能是整个项目里投入产出比最高的一笔开发量——曾经有一次 Python 端下单偶发慢 3 秒我就是靠trace_id串日志定位到是同花顺客户端在特定行情下刷新持仓列表变慢而不是代码本身有问题。最后提醒一句实盘接口这个方向代码写得好不好是其次安全习惯才是生死线。我见过不止一个开发者把实盘环境当成模拟盘来测试一笔错误委托的损失能让整个项目失去信任。所以我的习惯是本地开发永远连模拟盘实盘部署必须双人复核配置文件启动实盘交易模块前必须看一眼账户余额和当前连接的状态栏文字。这套流程麻烦但值。希望帮到你。本文还有配套的精品资源点击获取