xtquant-big-convert 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {xtquant_big_convert-0.3.0/src/xtquant_big_convert.egg-info → xtquant_big_convert-0.3.2}/PKG-INFO +75 -5
  2. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/README.md +74 -4
  3. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/pyproject.toml +1 -1
  4. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/order_bigqmt.py +79 -1
  5. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/models.py +5 -0
  6. xtquant_big_convert-0.3.2/src/bigqmt_signal_trader/order_id.py +104 -0
  7. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/qmt_launcher.py +27 -0
  8. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/redis_rpc.py +121 -1
  9. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/version.py +1 -1
  10. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/xtquant_compat.py +198 -15
  11. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2/src/xtquant_big_convert.egg-info}/PKG-INFO +75 -5
  12. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant_big_convert.egg-info/SOURCES.txt +1 -0
  13. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/LICENSE +0 -0
  14. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/setup.cfg +0 -0
  15. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/BIGQMT_REDIS_DRYRUN.py +0 -0
  16. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/BIGQMT_ZMQ_BACKTEST.py +0 -0
  17. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/BIGQMT_ZMQ_DRYRUN.py +0 -0
  18. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/__init__.py +0 -0
  19. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/__main__.py +0 -0
  20. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/broker.py +0 -0
  21. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/client.py +0 -0
  22. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/data_feed.py +0 -0
  23. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/engine.py +0 -0
  24. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/models.py +0 -0
  25. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/protocol.py +0 -0
  26. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/qmt_runtime.py +0 -0
  27. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/server.py +0 -0
  28. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/strategy.py +0 -0
  29. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_backtest/zmq_server.py +0 -0
  30. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/README.md +0 -0
  31. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/__init__.py +0 -0
  32. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapter_factory.py +0 -0
  33. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/__init__.py +0 -0
  34. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/market_bigqmt.py +0 -0
  35. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/order_dryrun.py +0 -0
  36. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/position_bigqmt.py +0 -0
  37. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/position_sync_redis.py +0 -0
  38. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/redis_common.py +0 -0
  39. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/signal_redis.py +0 -0
  40. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/adapters/state_redis.py +0 -0
  41. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/app.py +0 -0
  42. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/code_utils.py +0 -0
  43. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/contracts.py +0 -0
  44. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/download_jobs.py +0 -0
  45. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/exec_events.py +0 -0
  46. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/formula_server.py +0 -0
  47. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/full_tick_cache.py +0 -0
  48. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/init_config.py +0 -0
  49. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/local_cache.py +0 -0
  50. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/logging_setup.py +0 -0
  51. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/price_engine.py +0 -0
  52. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/quote_push_channel.py +0 -0
  53. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/quote_subscription_manager.py +0 -0
  54. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/risk_guard.py +0 -0
  55. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/runner.py +0 -0
  56. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/runtime_bigqmt.py +0 -0
  57. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/sync.py +0 -0
  58. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/__init__.py +0 -0
  59. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/base.py +0 -0
  60. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/factory.py +0 -0
  61. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/mysql_transport.py +0 -0
  62. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/redis_transport.py +0 -0
  63. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/shm_transport.py +0 -0
  64. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/transports/zmq_transport.py +0 -0
  65. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader/whole_quote_session.py +0 -0
  66. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader_diagnostic.py +0 -0
  67. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader_dryrun.py +0 -0
  68. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader_redis_dryrun.py +0 -0
  69. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader_redis_rpc_runtime.py +0 -0
  70. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/bigqmt_signal_trader_strategy.py +0 -0
  71. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant/__init__.py +0 -0
  72. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant/xtconstant.py +0 -0
  73. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant/xtdata.py +0 -0
  74. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant/xttrader.py +0 -0
  75. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant/xttype.py +0 -0
  76. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant_big_convert.egg-info/dependency_links.txt +0 -0
  77. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant_big_convert.egg-info/entry_points.txt +0 -0
  78. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant_big_convert.egg-info/requires.txt +0 -0
  79. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/src/xtquant_big_convert.egg-info/top_level.txt +0 -0
  80. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/tests/test_single_file_build.py +0 -0
  81. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/tests/test_version_stamp.py +0 -0
  82. {xtquant_big_convert-0.3.0 → xtquant_big_convert-0.3.2}/tests/test_xtquant_shim_import.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xtquant-big-convert
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Big QMT RPC bridge and MiniQMT-compatible adapter layer (redis/zmq/mysql transports)
5
5
  Author: litaolemo
6
6
  License: MIT
@@ -461,10 +461,15 @@ python -m bigqmt_signal_trader.qmt_launcher restart --dir "D:\国金证券QMT交
461
461
  | `exe` | 直接起 `XtItClient.exe`,靠终端自身恢复会话 | 否 |
462
462
  | `login` | 起 exe 后向登录框输入账号密码 | 是,需 pywin32 |
463
463
 
464
- **关于「pywinauto/pyautogui 要求 Windows 处于登录状态」**:`login` 模式用的是
465
- `win32api.SendMessage` 直接投递到窗口句柄,不是 pyautogui 那种按屏幕坐标重放物理输入。
466
- 前者不要求窗口置于前台,锁屏下也能工作(会话还在即可,完全注销则不行)。密码从
467
- 环境变量 `BIGQMT_LOGIN_USER` / `BIGQMT_LOGIN_PASSWORD` 读,不走命令行参数——argv
464
+ **`login` 模式需要未锁屏的交互式桌面。** 它用的是 `keybd_event` / `mouse_event`
465
+ 物理输入(经 ctypes),不是 `SendMessage`——消息式输入投不到 Qt 对话框的焦点控件上,
466
+ 当别的窗口在前台时会静默失败,什么也不输入。物理输入要求对话框在最前,所以启动前会
467
+ 先把它置顶并核验;锁屏或 RDP 注销的会话直接抛 `QmtLauncherError` 而不是打一半密码。
468
+
469
+ > 需要**无人值守定时重启**的话,用 `linkmini` / `bat` / `exe` 三种模式,它们不碰登录框,
470
+ > 锁屏也能跑。只有 `login` 受这条限制。
471
+
472
+ 密码从环境变量 `BIGQMT_LOGIN_USER` / `BIGQMT_LOGIN_PASSWORD` 读,不走命令行参数——argv
468
473
  对同机任何进程可见。
469
474
 
470
475
  两个设计要点:
@@ -660,6 +665,67 @@ credit_positions = credit_trader.query_stock_positions(credit_acc)
660
665
 
661
666
  ---
662
667
 
668
+ ## 与 MiniQMT 的兼容性对照
669
+
670
+ 本项目的目标是让照着 MiniQMT (`xtquant`) 写的代码不改就能跑。下表列出**返回值契约**——类型不对不会报错,只会让判断悄悄反过来,所以单独列出来。
671
+
672
+ ### 返回值:与 MiniQMT 一致
673
+
674
+ | 接口 | 返回 | 说明 |
675
+ |---|---|---|
676
+ | `order_stock()` | `int` | 成功为正数,失败 `-1` |
677
+ | `order_stock_async()` | `int` | 请求序号 seq,结果走 `on_order_stock_async_response` |
678
+ | `cancel_order_stock()` | `int` | **`0` 成功,`-1` 失败**(不是 True/False) |
679
+ | `cancel_order_stock_sysid()` | `int` | 同上 |
680
+ | `cancel_order_stock_async()` | `int` | seq |
681
+ | `connect()` / `start()` | `int` | `0` 成功 |
682
+ | `subscribe()` / `unsubscribe()` | `int` | `0` 成功 |
683
+ | `query_stock_asset()` | 对象 | `.cash` / `.total_asset` 等属性 |
684
+ | `query_stock_positions()` | `list[对象]` | |
685
+ | `query_stock_orders()` / `query_stock_trades()` | `list[对象]` | |
686
+ | `subscribe_quote()` / `subscribe_whole_quote()` | `int` | 订阅号,传给 `unsubscribe_quote()` |
687
+ | `get_full_tick()` | `dict` | `{code: {...}}` |
688
+ | `get_market_data_ex()` | `dict[str, DataFrame]` | |
689
+
690
+ ### 订单号:既是 int 也是 str
691
+
692
+ MiniQMT 的 `order_id` 是 int(委托编号),`order_sysid` 是 str(柜台合同编号)。大 QMT **没有前者**——`get_trade_detail_data` 只给 `m_strOrderSysID` 这个字符串。
693
+
694
+ 所以这里的 `order_id` 是一个 int 子类,两种形态同时成立:
695
+
696
+ ```python
697
+ order_id = xt_trader.order_stock(acc, "600000.SH", 23, 100, 11, 10.0, "s", "")
698
+
699
+ isinstance(order_id, int) # True —— MiniQMT 写法照常
700
+ order_id > 0 # True
701
+ order_id == -1 # 失败时才 True
702
+
703
+ str(order_id) # '合同编号' —— 券商给的原始字符串
704
+ xt_trader.cancel_order_stock(acc, order_id) # 撤单送回的是原始字符串
705
+ ```
706
+
707
+ 合同编号是纯数字时(多数券商),int 值就是那个数字,两种形态完全一致;不是纯数字时 int 是一个稳定的正数替身,而撤单、打印用的仍是真实编号。
708
+
709
+ 把 order_id 存进数据库再取出来(变成普通 int)也能撤单——客户端记着最近 4096 个的对应关系。想要字符串就用 `.order_sysid`,它一直是 str。
710
+
711
+ 同样的规则适用于 `XtOrder.order_id`、`XtTrade.order_id`,以及回调对象 `XtOrderError` / `XtCancelError` / `XtOrderResponse` 里的 `order_id`。
712
+
713
+ ### 行为差异(不是返回值,但会咬人)
714
+
715
+ | 项目 | MiniQMT | 本项目 |
716
+ |---|---|---|
717
+ | `get_full_tick(["SH"])` | 全市场 | **默认只取股票**(1.08s);要全部传 `types=["all"]`(7.4s,含地方债等 26744 只) |
718
+ | `get_instrument_detail()` 查不到 | `None` | `{}`(两者都是 falsy,`if not detail` 通用) |
719
+ | `download_history_data()` | 无返回 | 返回 `{"finished": n, "total": n}`(多给的信息,可忽略) |
720
+ | 账户类型 | `StockAccount(id, "CREDIT")` 即可 | 还需服务端 `BIGQMT_ACCOUNT_TYPE = "CREDIT"`,**客户端的类型不会传到服务端** |
721
+ | 委托类型常量 | `xtconstant.order_type` | 内部会翻译成 `passorder` 的 opType(两套编号,专项两融 40–45 → 70–75) |
722
+
723
+ ### 本项目的扩展(MiniQMT 没有)
724
+
725
+ 这些不是兼容项,是多出来的:`order_stock_result()`(返回完整 dict 而非单个 id)、`order_stock_batch()`、`wait_async_orders()`、`ipo_subscribe_all()`、`sync_deployment()`、`get_deployment_info()`、`query_execution_snapshot()`、`local_cache_stats()`。
726
+
727
+ ---
728
+
663
729
  ## 环境要求与依赖安装
664
730
 
665
731
  本系统分两部分,各自需要自己的 Python 环境和依赖:
@@ -743,6 +809,8 @@ cd D:\国金证券QMT交易端
743
809
 
744
810
  ## 快速开始
745
811
 
812
+ > 第一次部署、只想要最短路径?直接看 [docs/DEPLOY_QUICKSTART.md](docs/DEPLOY_QUICKSTART.md)(单账号五步跑通 + 常见问题表)。
813
+
746
814
  > 前置:客户端已按上面「A. 客户端」装好包;服务端按「B. 服务端」装好所选传输的依赖。下面是从零跑通整套流程的步骤。
747
815
  >
748
816
  > 只想把配置生成出来的话,跑 [`bigqmt-init`](#配置向导bigqmt-init) 即可——第 3 步的两份配置它会替你写好,选单文件部署还会顺带把构建也做了。
@@ -1231,6 +1299,8 @@ python qmt-trader/scripts/qmt.py buy 600000.SH 100 --price 7.50 --dry-run
1231
1299
  ## 相关文档
1232
1300
 
1233
1301
  - [CHANGELOG.md](CHANGELOG.md) — **版本变更记录**(新增/修复/变更)
1302
+ - [docs/DEPLOY_QUICKSTART.md](docs/DEPLOY_QUICKSTART.md) — **单账号部署快速开始**(最短路径 + 部署期常见问题表)
1303
+ - [docs/LATENCY_REPORT.md](docs/LATENCY_REPORT.md) — **延迟测试报告**(传输层对比、FormulaServer 直连、下单链路、方法论)
1234
1304
  - [docs/RPC_API_REFERENCE.md](docs/RPC_API_REFERENCE.md) — **全部 RPC 方法参考**(参数、返回值、别名、大 QMT 能力边界)
1235
1305
  - [docs/FORMULA_SERVER_FASTPATH.md](docs/FORMULA_SERVER_FASTPATH.md) — FormulaServer(58600) 直连快速路径:协议、映射表、能力边界与回退行为
1236
1306
  - [docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md](docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md) — 全推行情订阅推送机制设计
@@ -426,10 +426,15 @@ python -m bigqmt_signal_trader.qmt_launcher restart --dir "D:\国金证券QMT交
426
426
  | `exe` | 直接起 `XtItClient.exe`,靠终端自身恢复会话 | 否 |
427
427
  | `login` | 起 exe 后向登录框输入账号密码 | 是,需 pywin32 |
428
428
 
429
- **关于「pywinauto/pyautogui 要求 Windows 处于登录状态」**:`login` 模式用的是
430
- `win32api.SendMessage` 直接投递到窗口句柄,不是 pyautogui 那种按屏幕坐标重放物理输入。
431
- 前者不要求窗口置于前台,锁屏下也能工作(会话还在即可,完全注销则不行)。密码从
432
- 环境变量 `BIGQMT_LOGIN_USER` / `BIGQMT_LOGIN_PASSWORD` 读,不走命令行参数——argv
429
+ **`login` 模式需要未锁屏的交互式桌面。** 它用的是 `keybd_event` / `mouse_event`
430
+ 物理输入(经 ctypes),不是 `SendMessage`——消息式输入投不到 Qt 对话框的焦点控件上,
431
+ 当别的窗口在前台时会静默失败,什么也不输入。物理输入要求对话框在最前,所以启动前会
432
+ 先把它置顶并核验;锁屏或 RDP 注销的会话直接抛 `QmtLauncherError` 而不是打一半密码。
433
+
434
+ > 需要**无人值守定时重启**的话,用 `linkmini` / `bat` / `exe` 三种模式,它们不碰登录框,
435
+ > 锁屏也能跑。只有 `login` 受这条限制。
436
+
437
+ 密码从环境变量 `BIGQMT_LOGIN_USER` / `BIGQMT_LOGIN_PASSWORD` 读,不走命令行参数——argv
433
438
  对同机任何进程可见。
434
439
 
435
440
  两个设计要点:
@@ -625,6 +630,67 @@ credit_positions = credit_trader.query_stock_positions(credit_acc)
625
630
 
626
631
  ---
627
632
 
633
+ ## 与 MiniQMT 的兼容性对照
634
+
635
+ 本项目的目标是让照着 MiniQMT (`xtquant`) 写的代码不改就能跑。下表列出**返回值契约**——类型不对不会报错,只会让判断悄悄反过来,所以单独列出来。
636
+
637
+ ### 返回值:与 MiniQMT 一致
638
+
639
+ | 接口 | 返回 | 说明 |
640
+ |---|---|---|
641
+ | `order_stock()` | `int` | 成功为正数,失败 `-1` |
642
+ | `order_stock_async()` | `int` | 请求序号 seq,结果走 `on_order_stock_async_response` |
643
+ | `cancel_order_stock()` | `int` | **`0` 成功,`-1` 失败**(不是 True/False) |
644
+ | `cancel_order_stock_sysid()` | `int` | 同上 |
645
+ | `cancel_order_stock_async()` | `int` | seq |
646
+ | `connect()` / `start()` | `int` | `0` 成功 |
647
+ | `subscribe()` / `unsubscribe()` | `int` | `0` 成功 |
648
+ | `query_stock_asset()` | 对象 | `.cash` / `.total_asset` 等属性 |
649
+ | `query_stock_positions()` | `list[对象]` | |
650
+ | `query_stock_orders()` / `query_stock_trades()` | `list[对象]` | |
651
+ | `subscribe_quote()` / `subscribe_whole_quote()` | `int` | 订阅号,传给 `unsubscribe_quote()` |
652
+ | `get_full_tick()` | `dict` | `{code: {...}}` |
653
+ | `get_market_data_ex()` | `dict[str, DataFrame]` | |
654
+
655
+ ### 订单号:既是 int 也是 str
656
+
657
+ MiniQMT 的 `order_id` 是 int(委托编号),`order_sysid` 是 str(柜台合同编号)。大 QMT **没有前者**——`get_trade_detail_data` 只给 `m_strOrderSysID` 这个字符串。
658
+
659
+ 所以这里的 `order_id` 是一个 int 子类,两种形态同时成立:
660
+
661
+ ```python
662
+ order_id = xt_trader.order_stock(acc, "600000.SH", 23, 100, 11, 10.0, "s", "")
663
+
664
+ isinstance(order_id, int) # True —— MiniQMT 写法照常
665
+ order_id > 0 # True
666
+ order_id == -1 # 失败时才 True
667
+
668
+ str(order_id) # '合同编号' —— 券商给的原始字符串
669
+ xt_trader.cancel_order_stock(acc, order_id) # 撤单送回的是原始字符串
670
+ ```
671
+
672
+ 合同编号是纯数字时(多数券商),int 值就是那个数字,两种形态完全一致;不是纯数字时 int 是一个稳定的正数替身,而撤单、打印用的仍是真实编号。
673
+
674
+ 把 order_id 存进数据库再取出来(变成普通 int)也能撤单——客户端记着最近 4096 个的对应关系。想要字符串就用 `.order_sysid`,它一直是 str。
675
+
676
+ 同样的规则适用于 `XtOrder.order_id`、`XtTrade.order_id`,以及回调对象 `XtOrderError` / `XtCancelError` / `XtOrderResponse` 里的 `order_id`。
677
+
678
+ ### 行为差异(不是返回值,但会咬人)
679
+
680
+ | 项目 | MiniQMT | 本项目 |
681
+ |---|---|---|
682
+ | `get_full_tick(["SH"])` | 全市场 | **默认只取股票**(1.08s);要全部传 `types=["all"]`(7.4s,含地方债等 26744 只) |
683
+ | `get_instrument_detail()` 查不到 | `None` | `{}`(两者都是 falsy,`if not detail` 通用) |
684
+ | `download_history_data()` | 无返回 | 返回 `{"finished": n, "total": n}`(多给的信息,可忽略) |
685
+ | 账户类型 | `StockAccount(id, "CREDIT")` 即可 | 还需服务端 `BIGQMT_ACCOUNT_TYPE = "CREDIT"`,**客户端的类型不会传到服务端** |
686
+ | 委托类型常量 | `xtconstant.order_type` | 内部会翻译成 `passorder` 的 opType(两套编号,专项两融 40–45 → 70–75) |
687
+
688
+ ### 本项目的扩展(MiniQMT 没有)
689
+
690
+ 这些不是兼容项,是多出来的:`order_stock_result()`(返回完整 dict 而非单个 id)、`order_stock_batch()`、`wait_async_orders()`、`ipo_subscribe_all()`、`sync_deployment()`、`get_deployment_info()`、`query_execution_snapshot()`、`local_cache_stats()`。
691
+
692
+ ---
693
+
628
694
  ## 环境要求与依赖安装
629
695
 
630
696
  本系统分两部分,各自需要自己的 Python 环境和依赖:
@@ -708,6 +774,8 @@ cd D:\国金证券QMT交易端
708
774
 
709
775
  ## 快速开始
710
776
 
777
+ > 第一次部署、只想要最短路径?直接看 [docs/DEPLOY_QUICKSTART.md](docs/DEPLOY_QUICKSTART.md)(单账号五步跑通 + 常见问题表)。
778
+
711
779
  > 前置:客户端已按上面「A. 客户端」装好包;服务端按「B. 服务端」装好所选传输的依赖。下面是从零跑通整套流程的步骤。
712
780
  >
713
781
  > 只想把配置生成出来的话,跑 [`bigqmt-init`](#配置向导bigqmt-init) 即可——第 3 步的两份配置它会替你写好,选单文件部署还会顺带把构建也做了。
@@ -1196,6 +1264,8 @@ python qmt-trader/scripts/qmt.py buy 600000.SH 100 --price 7.50 --dry-run
1196
1264
  ## 相关文档
1197
1265
 
1198
1266
  - [CHANGELOG.md](CHANGELOG.md) — **版本变更记录**(新增/修复/变更)
1267
+ - [docs/DEPLOY_QUICKSTART.md](docs/DEPLOY_QUICKSTART.md) — **单账号部署快速开始**(最短路径 + 部署期常见问题表)
1268
+ - [docs/LATENCY_REPORT.md](docs/LATENCY_REPORT.md) — **延迟测试报告**(传输层对比、FormulaServer 直连、下单链路、方法论)
1199
1269
  - [docs/RPC_API_REFERENCE.md](docs/RPC_API_REFERENCE.md) — **全部 RPC 方法参考**(参数、返回值、别名、大 QMT 能力边界)
1200
1270
  - [docs/FORMULA_SERVER_FASTPATH.md](docs/FORMULA_SERVER_FASTPATH.md) — FormulaServer(58600) 直连快速路径:协议、映射表、能力边界与回退行为
1201
1271
  - [docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md](docs/SUBSCRIBE_WHOLE_QUOTE_PUSH.md) — 全推行情订阅推送机制设计
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xtquant-big-convert"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Big QMT RPC bridge and MiniQMT-compatible adapter layer (redis/zmq/mysql transports)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -5,6 +5,8 @@ The passorder signature follows src/api/qmt_jq_trade.
5
5
 
6
6
  import hashlib
7
7
 
8
+ from xtquant import xtconstant as _xtconstant
9
+
8
10
  from ..code_utils import normalize_stock_code
9
11
  from ..exec_events import date_time_seconds
10
12
  from ..models import CancelResult, OrderSnapshot, OrderSubmitResult, SignalAction, TradeSnapshot
@@ -21,6 +23,75 @@ PRICE_TYPE_ALIASES = {
21
23
  }
22
24
 
23
25
 
26
+ # MiniQMT's order_type (xtconstant) and passorder's opType are two different
27
+ # numberings. They agree on 27-32 and diverge on the special-margin family:
28
+ #
29
+ # meaning xtconstant passorder opType (API ref 10.1)
30
+ # 融资买入 .. 直接还款 27-32 27-32 same
31
+ # 担保品买入 / 卖出 -- (CREDIT_BUY/SELL) 33 / 34
32
+ # 专项两融 40-45 70-75 DIFFERENT
33
+ #
34
+ # Sending 40 through unchanged would reach passorder as "期货组合开多", so the
35
+ # translation is not optional. Values are taken from xtconstant by NAME: PR #88
36
+ # asserted them as literals and encoded the same mistake in its tests, which is
37
+ # why they passed while the mapping was wrong.
38
+ _XC = _xtconstant
39
+
40
+ CREDIT_OPTYPE_BY_ORDER_TYPE = {
41
+ _XC.CREDIT_FIN_BUY: 27, # 融资买入
42
+ _XC.CREDIT_SLO_SELL: 28, # 融券卖出
43
+ _XC.CREDIT_BUY_SECU_REPAY: 29, # 买券还券
44
+ _XC.CREDIT_DIRECT_SECU_REPAY: 30, # 直接还券
45
+ _XC.CREDIT_SELL_SECU_REPAY: 31, # 卖券还款
46
+ _XC.CREDIT_DIRECT_CASH_REPAY: 32, # 直接还款
47
+ _XC.CREDIT_FIN_BUY_SPECIAL: 70, # 专项融资买入
48
+ _XC.CREDIT_SLO_SELL_SPECIAL: 71, # 专项融券卖出
49
+ _XC.CREDIT_BUY_SECU_REPAY_SPECIAL: 72, # 专项买券还券
50
+ _XC.CREDIT_DIRECT_SECU_REPAY_SPECIAL: 73, # 专项直接还券
51
+ _XC.CREDIT_SELL_SECU_REPAY_SPECIAL: 74, # 专项卖券还款
52
+ _XC.CREDIT_DIRECT_CASH_REPAY_SPECIAL: 75, # 专项直接还款
53
+ }
54
+
55
+ # Which side of the book each one is, for bookkeeping only -- the opType above
56
+ # is what actually goes to passorder. Repayment operations that move securities
57
+ # are classified by what they do to the holding.
58
+ _CREDIT_BUY_SIDE = frozenset({
59
+ _XC.CREDIT_FIN_BUY, _XC.CREDIT_BUY_SECU_REPAY,
60
+ _XC.CREDIT_FIN_BUY_SPECIAL, _XC.CREDIT_BUY_SECU_REPAY_SPECIAL,
61
+ })
62
+ _CREDIT_SELL_SIDE = frozenset({
63
+ _XC.CREDIT_SLO_SELL, _XC.CREDIT_SELL_SECU_REPAY,
64
+ _XC.CREDIT_DIRECT_SECU_REPAY,
65
+ _XC.CREDIT_SLO_SELL_SPECIAL, _XC.CREDIT_SELL_SECU_REPAY_SPECIAL,
66
+ _XC.CREDIT_DIRECT_SECU_REPAY_SPECIAL,
67
+ })
68
+
69
+
70
+ def credit_action_of(order_type):
71
+ """BUY / SELL for a credit order_type, or None if it is not one.
72
+
73
+ 直接还款 (32 / 45) moves cash rather than securities, so it has no side;
74
+ callers must pass an action for it explicitly.
75
+ """
76
+ try:
77
+ value = int(order_type)
78
+ except (TypeError, ValueError):
79
+ return None
80
+ if value in _CREDIT_BUY_SIDE:
81
+ return SignalAction.BUY.value
82
+ if value in _CREDIT_SELL_SIDE:
83
+ return SignalAction.SELL.value
84
+ return None
85
+
86
+
87
+ def credit_optype_of(order_type):
88
+ """passorder opType for a MiniQMT credit order_type, or None."""
89
+ try:
90
+ return CREDIT_OPTYPE_BY_ORDER_TYPE.get(int(order_type))
91
+ except (TypeError, ValueError):
92
+ return None
93
+
94
+
24
95
  def _action_from_offset_flag(offset_flag):
25
96
  return SignalAction.BUY.value if int(offset_flag or 0) == 48 else SignalAction.SELL.value
26
97
 
@@ -166,7 +237,14 @@ class BigQmtOrderGateway:
166
237
  def submit(self, request):
167
238
  passorder = self._require_passorder()
168
239
  action = str(request.action).upper()
169
- if action == SignalAction.BUY.value:
240
+ credit_optype = credit_optype_of(getattr(request, "order_type", None))
241
+ if credit_optype is not None:
242
+ # A credit operation carries more than a side: mapping it back to
243
+ # BUY/SELL would turn 融资买入 into an ordinary buy, which is the
244
+ # bug behind issue #103 -- worse than the rejection it replaced,
245
+ # because it places a real but different order.
246
+ op_type = credit_optype
247
+ elif action == SignalAction.BUY.value:
170
248
  op_type = 23
171
249
  elif action == SignalAction.SELL.value:
172
250
  op_type = 24
@@ -329,7 +329,12 @@ class OrderRequest:
329
329
  price_type,
330
330
  strategy_name,
331
331
  remark="",
332
+ order_type=None,
332
333
  ):
334
+ # MiniQMT-style order_type (xtconstant). Only set for operations a
335
+ # BUY/SELL action cannot express -- credit financing, repayment and the
336
+ # special-margin family. None means an ordinary stock order.
337
+ self.order_type = order_type
333
338
  self.signal_id = signal_id
334
339
  self.account_id = account_id
335
340
  self.action = action
@@ -0,0 +1,104 @@
1
+ # coding: utf-8
2
+ """An order id that is an int for MiniQMT and a string for the broker.
3
+
4
+ MiniQMT's ``order_stock`` returns an int: a positive 委托编号, or ``-1`` when the
5
+ submit failed. Big QMT has no such number -- ``get_trade_detail_data`` hands
6
+ back ``m_strOrderSysID``, the broker's 合同编号, which is a string. This bridge
7
+ was returning that string straight through (issue #113), so every caller
8
+ written against MiniQMT broke, and broke quietly:
9
+
10
+ order_id = trader.order_stock(...)
11
+ if order_id == -1: # never true: "-1" != -1, so a rejected order
12
+ ... # reads as success
13
+ if order_id > 0: # TypeError: '>' not supported between str and int
14
+
15
+ ``OrderId`` is both. It *is* an int, so comparisons, ``isinstance`` and
16
+ arithmetic behave the way MiniQMT callers expect; and it carries the exact
17
+ broker string on ``.order_sys_id``, so a cancel sends back what QMT issued
18
+ rather than a number we invented -- including ids that are not numeric at all.
19
+
20
+ Where the 合同编号 is a plain positive integer (the usual case) the int value is
21
+ that same number and the two representations agree exactly. Where it is not,
22
+ the int is a stable surrogate derived from the string: still positive, so
23
+ ``> 0`` keeps separating success from failure, while ``str()`` and the cancel
24
+ path use the real id.
25
+ """
26
+
27
+ import zlib
28
+
29
+
30
+ # Positive and comfortably inside 32 bits, so the value survives a trip through
31
+ # JSON, a database column, or anything else that is unhappy with big integers.
32
+ _SURROGATE_MASK = 0x3FFFFFFF
33
+
34
+ _DIGITS = frozenset("0123456789")
35
+
36
+
37
+ def _is_ascii_digits(text):
38
+ # str.isdigit() is True for '123' and other unicode digit forms, which
39
+ # int() then happily parses into something the broker never issued.
40
+ return bool(text) and all(char in _DIGITS for char in text)
41
+
42
+
43
+ def int_value_of(order_sys_id):
44
+ """The int an ``OrderId`` for this 合同编号 should carry.
45
+
46
+ Returns 0 for an empty id -- there is no order, so there is no number.
47
+ """
48
+ text = ("" if order_sys_id is None else str(order_sys_id)).strip()
49
+ if not text:
50
+ return 0
51
+ if _is_ascii_digits(text):
52
+ value = int(text)
53
+ if value > 0:
54
+ return value
55
+ # Alphanumeric 合同编号, or a leading-zero form whose int would not round
56
+ # trip. Derive a stable surrogate instead of guessing a number: same string
57
+ # gives the same value in any process, so a caller comparing ids across a
58
+ # restart still matches. `or 1` keeps it strictly positive.
59
+ return (zlib.crc32(text.encode("utf-8")) & _SURROGATE_MASK) or 1
60
+
61
+
62
+ class OrderId(int):
63
+ """``int(order_id)`` for MiniQMT, ``str(order_id)`` for the broker."""
64
+
65
+ # No __slots__: CPython rejects a nonempty __slots__ on an int subclass
66
+ # (variable-length builtin), so the instance carries an ordinary __dict__.
67
+
68
+ def __new__(cls, order_sys_id, value=None):
69
+ text = "" if order_sys_id is None else str(order_sys_id)
70
+ if value is None:
71
+ value = int_value_of(text)
72
+ obj = int.__new__(cls, value)
73
+ obj.order_sys_id = text
74
+ return obj
75
+
76
+ def __str__(self):
77
+ # int.__str__ is object.__str__ on py3, which routes back through the
78
+ # __repr__ below; int.__repr__ is the one that prints the number.
79
+ return self.order_sys_id or int.__repr__(self)
80
+
81
+ def __format__(self, spec):
82
+ # "{}".format(oid) and f"{oid}" go through __format__, not __str__, and
83
+ # int.__format__ with an empty spec would print the surrogate. Anything
84
+ # with an actual spec ("%05d"-style) is asking for the number.
85
+ if not spec:
86
+ return self.__str__()
87
+ return int.__format__(self, spec)
88
+
89
+ def __repr__(self):
90
+ return "OrderId(%r, %d)" % (self.order_sys_id, int(self))
91
+
92
+
93
+ def order_sys_id_of(value):
94
+ """Best available broker string for something a caller handed back.
95
+
96
+ Accepts an ``OrderId``, a bare string, or the int a caller round-tripped
97
+ through JSON and lost the string half of.
98
+ """
99
+ carried = getattr(value, "order_sys_id", None)
100
+ if carried:
101
+ return str(carried)
102
+ if value is None:
103
+ return ""
104
+ return str(value)
@@ -159,6 +159,26 @@ def is_qmt_running(install_dir):
159
159
  return bool(find_qmt_processes(install_dir))
160
160
 
161
161
 
162
+ def session_is_locked():
163
+ """True when the interactive session is at the Windows lock screen.
164
+
165
+ Login automation is impossible while locked (physical input lands on the
166
+ lock screen), so callers can check before closing a running terminal.
167
+ Heuristic: the foreground window is the lock screen's CoreWindow.
168
+ """
169
+ try:
170
+ import ctypes
171
+
172
+ import win32gui
173
+
174
+ hwnd = ctypes.windll.user32.GetForegroundWindow()
175
+ title = win32gui.GetWindowText(hwnd) or ""
176
+ cls = win32gui.GetClassName(hwnd)
177
+ return cls == "Windows.UI.Core.CoreWindow" and "锁屏" in title
178
+ except Exception:
179
+ return False
180
+
181
+
162
182
  # ------------------------------------------------------------------ readiness
163
183
  def port_is_listening(port=DEFAULT_READY_PORT, host="127.0.0.1", timeout=1.0):
164
184
  sock = socket.socket()
@@ -487,6 +507,13 @@ def restart_qmt(install_dir, settle_seconds=5.0, **open_kwargs):
487
507
  after the process dies, and the ZMQ transport binds its configured port
488
508
  exactly (no scanning), so restarting too eagerly fails the rebind.
489
509
  """
510
+ mode = str(open_kwargs.get("mode") or "auto")
511
+ if mode in ("login", "auto") and session_is_locked():
512
+ raise QmtLauncherError(
513
+ "interactive session is locked; the login dialog cannot be automated "
514
+ "now. Unlock the session first, or restart without closing "
515
+ "(the terminal would sit at the login dialog with trading down)."
516
+ )
490
517
  closed = close_qmt(install_dir)
491
518
  if closed:
492
519
  time.sleep(settle_seconds)
@@ -32,6 +32,7 @@ RPC_REVISION = "20260715-execution-snapshot-v1"
32
32
  READ_METHODS = {
33
33
  "ping",
34
34
  "get_deployment_info",
35
+ "probe_capabilities",
35
36
  "get_ticks",
36
37
  "get_instrument",
37
38
  "get_instrument_type",
@@ -340,6 +341,29 @@ def to_jsonable(value):
340
341
  }
341
342
  except Exception:
342
343
  return str(value)
344
+ # pandas Panel (3-D). QMT ships pandas 0.22, where get_financial_data with
345
+ # several stocks AND several dates still returns one. A Panel has no
346
+ # .columns and no .index, so it falls past the DataFrame and Series
347
+ # branches all the way to the __dict__ fallback -- and vars(panel) is
348
+ # {'_data': ..., 'is_copy': None}, which the underscore filter reduces to
349
+ # {'is_copy': None}. That is exactly what callers got back (issue #115):
350
+ # not an error, just the one public attribute the object happened to have.
351
+ #
352
+ # pandas removed Panel in 1.0, so the client cannot rebuild one even if we
353
+ # sent the axes. Send a DataFrame per item instead; it arrives as a dict.
354
+ if (hasattr(value, "major_axis") and hasattr(value, "minor_axis")
355
+ and hasattr(value, "items") and not isinstance(value, dict)):
356
+ try:
357
+ labels = [str(item) for item in value.items]
358
+ return {
359
+ "__bigqmt_type__": "Panel",
360
+ "items": labels,
361
+ "major_axis": [str(item) for item in value.major_axis],
362
+ "minor_axis": [str(item) for item in value.minor_axis],
363
+ "data": {str(item): to_jsonable(value[item]) for item in value.items},
364
+ }
365
+ except Exception:
366
+ return str(value)
343
367
  if hasattr(value, "tolist") and not isinstance(value, (str, bytes, bytearray)):
344
368
  try:
345
369
  return to_jsonable(value.tolist())
@@ -527,6 +551,67 @@ class BigQmtRpcHandlers:
527
551
  pass
528
552
  return info
529
553
 
554
+ # probe 时检查的关键 QMT 运行时全局函数(存在与否决定对应能力可用性)。
555
+ _PROBE_QMT_GLOBALS = (
556
+ "passorder", "cancel", "get_trade_detail_data",
557
+ "download_history_data", "download_history_data2", "down_history_data",
558
+ "get_history_trade_detail_data", "get_value_by_order_id", "get_last_order_id",
559
+ "get_ipo_data", "get_new_purchase_limit",
560
+ "get_assure_contract", "get_enable_short_contract",
561
+ "get_unclosed_compacts", "get_closed_compacts", "get_debt_contract",
562
+ "get_option_subject_position", "get_comb_option", "get_hkt_exchange_rate",
563
+ )
564
+
565
+ # probe 时抽查的 ContextInfo 方法。
566
+ _PROBE_CONTEXT_METHODS = (
567
+ "get_full_tick", "get_market_data_ex", "get_market_data", "get_local_data",
568
+ "subscribe_quote", "subscribe_whole_quote", "unsubscribe_quote",
569
+ "get_trading_dates", "get_financial_data", "get_stock_list_in_sector",
570
+ "do_back_test", "get_trade_detail_data",
571
+ )
572
+
573
+ def _handle_probe_capabilities(self, params):
574
+ """只读能力探测:当前部署暴露了哪些 QMT callable。
575
+
576
+ 部署后跑一次就能回答「这台券商 QMT 缺什么」——只读调用,不触发任何
577
+ 委托。返回三部分:运行时全局函数绑定情况、ContextInfo 方法存在性、
578
+ 信用接口只读探测(调用一次看是否报错/返回行数)。
579
+ """
580
+ info = {
581
+ "account_id": self.account_id,
582
+ "allow_order_methods": self.allow_order_methods,
583
+ "settle_orders_inline": self.settle_orders_inline,
584
+ "order_settle_timeout_seconds": self.order_settle_timeout_seconds,
585
+ "qmt_globals": {},
586
+ "contextinfo_methods": {},
587
+ "credit_probe": {},
588
+ "server_time": _dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
589
+ }
590
+ for name in self._PROBE_QMT_GLOBALS:
591
+ info["qmt_globals"][name] = callable(self.qmt_api.get(name))
592
+ context_info = getattr(self.market_data, "context_info", None)
593
+ for name in self._PROBE_CONTEXT_METHODS:
594
+ info["contextinfo_methods"][name] = callable(getattr(context_info, name, None))
595
+ # 信用接口只读探测:不存在的全局直接标 unavailable;存在的真调一次,
596
+ # 记录是否报错和返回行数(担保品/融券标的可能很多行,只计数)。
597
+ for name in ("get_assure_contract", "get_enable_short_contract",
598
+ "get_unclosed_compacts", "get_debt_contract"):
599
+ func = self.qmt_api.get(name)
600
+ if not callable(func):
601
+ info["credit_probe"][name] = {"available": False}
602
+ continue
603
+ try:
604
+ rows = func(self.account_id) or []
605
+ info["credit_probe"][name] = {
606
+ "available": True, "ok": True, "rows": len(rows),
607
+ }
608
+ except Exception as exc:
609
+ info["credit_probe"][name] = {
610
+ "available": True, "ok": False,
611
+ "error": "%s: %s" % (exc.__class__.__name__, exc),
612
+ }
613
+ return info
614
+
530
615
  # ------------------------------------------------------------------
531
616
  # 全推行情订阅控制(引用计数共享 ContextInfo.subscribe_whole_quote)。
532
617
  # 数据本身走推送通道;这里只负责订阅生命周期 + 心跳。
@@ -996,13 +1081,29 @@ class BigQmtRpcHandlers:
996
1081
  action = str(params.get("action") or "").upper()
997
1082
  if action:
998
1083
  return action
999
- order_type = str(params.get("order_type") or "").upper()
1084
+ raw = params.get("order_type")
1085
+ order_type = str(raw or "").upper()
1000
1086
  if order_type in BUY_ORDER_TYPES:
1001
1087
  return "BUY"
1002
1088
  if order_type in SELL_ORDER_TYPES:
1003
1089
  return "SELL"
1090
+ # Credit operations carry their side in the type itself (issue #103).
1091
+ # 直接还款 moves cash rather than securities and has no side, so it
1092
+ # still needs an explicit action rather than being guessed at.
1093
+ credit = _credit_action_of(raw)
1094
+ if credit:
1095
+ return credit
1096
+ if _credit_optype_of(raw) is not None:
1097
+ raise ValueError(
1098
+ "order_type %s has no implicit buy/sell side; pass action "
1099
+ "explicitly" % raw)
1004
1100
  raise ValueError("action or order_type is required")
1005
1101
 
1102
+ def _credit_order_type_from_params(self, params):
1103
+ """The MiniQMT order_type to forward, when it is a credit operation."""
1104
+ raw = params.get("order_type")
1105
+ return raw if _credit_optype_of(raw) is not None else None
1106
+
1006
1107
  def _handle_submit_order(self, params):
1007
1108
  if self.order_gateway is None:
1008
1109
  raise RuntimeError("order_gateway is not configured")
@@ -1021,6 +1122,7 @@ class BigQmtRpcHandlers:
1021
1122
  price_type=params.get("price_type") or "LIMIT",
1022
1123
  strategy_name=str(params.get("strategy_name") or "bigqmt_rpc"),
1023
1124
  remark=order_tag,
1125
+ order_type=self._credit_order_type_from_params(params),
1024
1126
  )
1025
1127
  if request.action not in ("BUY", "SELL"):
1026
1128
  raise ValueError("action must be BUY or SELL")
@@ -1262,6 +1364,24 @@ def _bool_value(value, default=False):
1262
1364
  return str(value).strip().lower() in ("1", "true", "yes", "y", "on")
1263
1365
 
1264
1366
 
1367
+ def _credit_action_of(order_type):
1368
+ try:
1369
+ from bigqmt_signal_trader.adapters.order_bigqmt import credit_action_of
1370
+
1371
+ return credit_action_of(order_type)
1372
+ except Exception:
1373
+ return None
1374
+
1375
+
1376
+ def _credit_optype_of(order_type):
1377
+ try:
1378
+ from bigqmt_signal_trader.adapters.order_bigqmt import credit_optype_of
1379
+
1380
+ return credit_optype_of(order_type)
1381
+ except Exception:
1382
+ return None
1383
+
1384
+
1265
1385
  def _deployed_version():
1266
1386
  """Version of the bridge actually running here, or "" if unknown."""
1267
1387
  try:
@@ -21,7 +21,7 @@ copy never happened" and "the copy landed but was not picked up" look identical
21
21
  from the outside.
22
22
  """
23
23
 
24
- __version__ = "0.2.16"
24
+ __version__ = "0.3.2"
25
25
 
26
26
 
27
27
  def deployment_report(package_dir=None):