简介:一份面向股票接口开发与量化学习者的 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(force=True) # 必填字段校验 required = ["action", "stock_code", "quantity"] for field in required: if field not in data: return jsonify({"code": 40001, "message": f"missing 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(host="127.0.0.1", port=8765, debug=False)这段代码的逻辑很直白: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(backend="win32").connect(title_re=".*同花顺.*", timeout=10) 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, action="buy", price=None): """执行一次下单操作。参数含义: - 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_type="Edit") code_edit.set_edit_text("") # 先清空 code_edit.set_edit_text(stock_code) # 再输入 # 第三步:定位到数量输入框,输入股数 qty_edit = main_win.child_window(title_re=".*数量.*", control_type="Edit") 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_type="Edit") price_edit.set_edit_text("") price_edit.set_edit_text(str(price)) # 第五步:点击买入/卖出按钮 if action == "buy": btn = main_win.child_window(title_re=".*买入.*", control_type="Button") else: btn = main_win.child_window(title_re=".*卖出.*", control_type="Button") btn.click() # 第六步:处理确认弹窗 time.sleep(0.5) confirm = main_win.child_window(title_re=".*确认.*", control_type="Button") if confirm.exists(timeout=3): 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(timeout=3)用的是“存在才点击”的写法,而不是强制等待点击,否则弹窗没出现时程序会卡死在找按钮上。
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 Map<String, 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, Map<String, Object> body) { try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers); ResponseEntity<Map> 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 的完整状态机
一个委托从提交到成交,在项目中至少有这几个状态需要管理:PENDING(JavaWeb 已受理)、ACCEPTED(Python 已接收)、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 股,必须一次性卖出全部零股;买入价格不能超过当前涨停价,卖出价格不能低于跌停价;当天买入的股票不能当天卖出(T+1)。这些规则属于“客户端会兜底但你不该依赖客户端”的边界,写在校验器里能让整个系统的行为更可预期。
这些约束还有一个作用:防止策略程序在特殊行情下产生极端委托。比如股票快速拉升时,策略算出买入价已经超过涨停价,如果不拦截直接发给同花顺,客户端会弹窗提示价格异常,还需要人工点掉,模拟盘也就罢了,实盘环境下这种弹窗会让程序卡住。
5. 实盘联调避坑指南:5 个高频翻车场景与排查方法
5.1 窗口定位失败:找不到同花顺主窗口
现象:Python 启动后执行下单,抛WindowNotFoundError,JavaWeb 侧看到的任务状态一直是 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(""),清空后再输入新值。如果清空后仍然出现残留,改用键盘组合键:先点击输入框,发送Ctrl+A全选,再直接键入新文本。
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 时带上同一个 ID;Python 端所有日志行也强制携带trace_id。这样排查问题时,只需要在 JavaWeb 日志里搜索订单号,拿到trace_id,再去 Python 日志里过滤同一 ID,整条链路的每一步耗时、参数、异常全部串在一起。这可能是整个项目里投入产出比最高的一笔开发量——曾经有一次 Python 端下单偶发慢 3 秒,我就是靠trace_id串日志定位到是同花顺客户端在特定行情下刷新持仓列表变慢,而不是代码本身有问题。
最后提醒一句:实盘接口这个方向,代码写得好不好是其次,安全习惯才是生死线。我见过不止一个开发者把实盘环境当成模拟盘来测试,一笔错误委托的损失能让整个项目失去信任。所以我的习惯是:本地开发永远连模拟盘,实盘部署必须双人复核配置文件,启动实盘交易模块前必须看一眼账户余额和当前连接的状态栏文字。这套流程麻烦,但值。希望帮到你。
本文还有配套的精品资源,点击获取