Tonghuashun A-share gateway for VeighNa
Overview
vnpy_ths connects VeighNa/vn.py to a real China A-share account through the
macOS Tonghuashun client. It uses macOS Accessibility APIs and OCR to read
account, position, order, and trade data from the Tonghuashun window, and to
submit buy, sell, and cancel actions.
Before every fund, position, order, or trade read—and before entering order or
cancel controls—the gateway reacquires the current AX tree and locates the
top-level refresh button from the named A股, 模拟, 添加, and 股票
anchors plus their relative layout. It performs AXPress without caching the
button or using fixed coordinates. Broker-specific trading endpoint connection
failures and trading-session invalidation dialogs are recognized by their
semantics and dismissed only when they are one-way informational prompts. If
the account is no longer logged in, the operation fails clearly instead of
reading stale UI data; quote/network prompts and two-way decisions are not
auto-confirmed.
Before a normal close or reconnect, the gateway drains Tonghuashun child dialogs instead of issuing a single quit against a blocked app. Known trading decisions are canceled, known one-way notices are acknowledged, and other single-button informational notices are dismissed only when they are not order-like. The bounded drain repeats until no dialog remains, then sends the normal application quit; it never confirms an unknown order prompt.
Cold startup waits for the real primary application shell before entering the trading workspace. Transient Accessibility failures while macOS is still building that shell are retried within the same bounded wait, and a final failure reports the last concrete AX error. Shallow controls and account tables are discovered incrementally instead of materializing the market page's full AX tree. Empty order and trade tables are accepted only when their exact semantic headers are present. A broker refresh is complete only after its refresh control becomes available again; AX timeout, node-limit, and workspace-recognition failures remain explicit errors.
Reading already visible AX text does not require Tonghuashun to be frontmost. Refresh, account-tab navigation, order entry, and cancellation mutate the UI and must activate Tonghuashun to work reliably across macOS fullscreen Spaces. A background broker snapshot records the user's frontmost app, activates Tonghuashun once for the complete UI transaction, and restores the original app when the batch finishes; it does not bounce focus after every button press.
THSGateway is the real-account gateway. It does not fall back to simulation
and should surface configuration, market-data, and order-entry errors clearly.
For local development and CI, the package also exports THSSimGateway, a
separate deterministic gateway that implements the same vn.py event shape
without starting Tonghuashun, the shared market-data service, or network access.
Dev Status
- macOS only. Development and verification currently use Tonghuashun macOS 5.2.2.
- Tesseract OCR is required:
brew install tesseract. - The Python process needs macOS Accessibility permission.
- The selected Tonghuashun
.appis the sole source of process identity. The gateway readsCFBundleIdentifierandCFBundleExecutablefrom itsContents/Info.plist, so Standard/Pro builds and renamed executables are not identified through a hard-coded product list. With an emptyapp_path, the gateway searches the system and user Applications directories and requires exactly one installed Standard/Pro bundle; multiple matches require an explicit path. Missing or inconsistent app metadata aborts connection. - Window geometry is interpreted relative to the selected app window, so the trading sidebar works on secondary displays and on displays left of the main screen instead of assuming global coordinates start at zero.
- Enable "remember account/password" and "auto login" in Tonghuashun to reduce manual login steps.
- Realtime quotes exclusively use the local
tdx-apimanaged bypytdxfeed. The host is fixed at127.0.0.1; the port defaults to8080. Before touching the Tonghuashun UI,pytdxfeedensures the shared macOS user LaunchAgent is healthy. There is no secondary market-data source.
Install
git clone https://github.com/hermanzhaozzzz/vnpy_ths.git
cd vnpy_ths
uv sync --locked --all-extras --all-groups
Run commands through uv:
uv run python -c "import vnpy_ths; print(vnpy_ths.__version__)"
Configuration
The recommended setup is a single default config file:
~/.vnpy_ths/config.toml.
mkdir -p ~/.vnpy_ths
# ~/.vnpy_ths/config.toml
[ths_broker]
broker_name = "银河证券" # The Galaxy Securities config is enabled.
account_type = "客户号"
trade_account = "1234567890"
trade_password = "123456"
# Synthetic MARKET settings.
# Tonghuashun macOS only accepts limit orders. MARKET is implemented by
# submitting a limit order at the opposite quote plus/minus allowed slippage.
market_order_slippage_ticks = 1 # Allowed pricetick offset when synthesizing MARKET as a limit order; buy=ask1+N ticks, sell=bid1-N ticks. Larger values improve fill chance but increase slippage.
market_order_max_tick_age_seconds = 3.0 # Maximum accepted staleness, in seconds, of the latest tick before MARKET submission; stale ticks are rejected to avoid quoting from an old book.
# Random split settings. Disabled by default.
# Lots are board lots: 1 lot = 100 shares. Only child size and delay are
# randomized; price is not randomized.
enable_random_split_order = false # Enable random splitting; false sends one Tonghuashun order per vn.py order, true splits it into multiple child orders.
random_split_min_lots = 1 # Minimum child order size in lots; each child order is randomly sized no smaller than this value and it must be greater than 0.
random_split_max_lots = 20 # Maximum child order size in lots; each child order is randomly sized no larger than this value and it must be >= min_lots.
random_split_min_delay_seconds = 0.3 # Minimum random delay between child orders, in seconds; set to 0 to send consecutive child orders as quickly as possible.
random_split_max_delay_seconds = 2.0 # Maximum random delay between child orders, in seconds; must be >= min_delay. Larger values slow down the split order schedule.
# Security name cache. Loaded at gateway startup; stale cache is refreshed
# from AKShare's static full-market A-share code-name list.
security_name_cache_path = "" # Security name CSV cache path; empty uses ~/.vnpy_ths/security_names.csv.
security_name_cache_ttl_days = 1 # Cache TTL in days; 1 means at most one successful refresh per day.
# Realtime market data. The host is fixed at 127.0.0.1; only the port is configurable.
tdx_api_port = 8080 # Local tdx-api TCP port, integer from 1 through 65535.
tdx_api_timeout = 3.0 # Timeout in seconds for health and quote requests; must be greater than 0.
On the first real-gateway connection, pytdxfeed installs and starts the shared
service automatically on macOS. Its runtime root is ~/.pytdxfeed and its
LaunchAgent label is com.hermanzhaozzzz.pytdxfeed.tdx-api. Other consumers,
including VQT, reuse the same service. Deployment or first-quote failure aborts
the gateway connection.
An explicit tdx_api_port passed to MainEngine.connect() overrides the broker
file value. This lets VQT keep one port setting for sync and live operation.
The runtime never scans for another free port.
Here connect() means vn.py's MainEngine.connect(setting, gateway_name).
Configuration is resolved in this order:
-
If
~/.vnpy_ths/config.tomlexists, it always wins:main_engine.connect({}, "THS")
-
If the default file does not exist, an explicit TOML path must be used:
main_engine.connect( {"config_path": "somewhere/your_config.toml"}, "THS", )
Account fields are only read from TOML. In the VeighNa connection dialog, leave
config_path empty for the default file or set it to the desired TOML path.
Multiple accounts can live in one TOML file and be selected with broker_name
or the THS_BROKER environment variable:
[ths_broker]
broker_name = "银河证券" # In this multi-account config, only Galaxy Securities is selected.
[ths_broker.accounts."银河证券"]
account_type = "客户号"
trade_account = "111111"
trade_password = "pw1"
[ths_broker.accounts."国金证券"]
account_type = "资金账户"
trade_account = "222222"
trade_password = "pw2"
THS_BROKER=国金证券 python run.py
Trading Behavior
Order Types
Among vn.py order types, this gateway currently supports limit orders (LIMIT)
and market orders (MARKET). Other types are rejected explicitly and are never
auto-converted.
Tonghuashun macOS order entry only submits limit orders. LIMIT orders are sent
to Tonghuashun with the price supplied by vn.py.
The gateway focuses and commits every order-entry field before validating the code, price, and size. Broker warnings carrying an error code are immediately reported as rejected orders rather than as submitted orders awaiting confirmation. Startup and shutdown may dismiss only these terminal rejection dialogs; an order confirmation dialog with a cancel action is never confirmed automatically.
MARKET is a gateway-synthesized marketable limit order, not a native
Tonghuashun market order:
- Buy:
ask_price_1 + market_order_slippage_ticks * pricetick - Sell:
bid_price_1 - market_order_slippage_ticks * pricetick - The target symbol must already be subscribed and must have a fresh tick.
- A temporarily unavailable precise last trade does not discard a fresh
order book. Such a tick keeps
last_price = 0, carries the exact bid/ask, and is not passed into minute-bar generation. MARKET execution still uses only the fresh opposite quote; it never substitutes the coarse K-line close or a broker position snapshot. - A successful trade response that reports
Count=0describes only that endpoint's coverage. If the same live quote proves current-session volume and turnover and has a valid book, MARKET may use the fresh opposite quote; otherwise the residual book remains observation-only. - An explicit
full_suspensionorintraday_haltstate rejects MARKET even if the upstream payload still contains residual bid/ask values. Missing trades or an empty book alone are never guessed to be a halt. - The submitted order is still a limit order. Large order size, thin liquidity, or fast price movement can still leave it partially filled or unfilled.
- The gateway does not auto-reprice. Strategies should cancel and resend explicitly when they want to chase.
Example: for a stock with 0.01 pricetick, slippage set to 2, and
ask_price_1 = 4.00, the gateway submits a buy limit price of 4.02. The
order may fill at 4.00, 4.01, 4.02, or any better price not above 4.02.
If the ask moves to 4.03 or higher, the order will not fill. For ETFs with a
typical 0.001 pricetick, ask_price_1 = 4.000 becomes 4.002.
Market and Order Facts
query_market_status(vt_symbols) returns one THSMarketStatus per requested
symbol. The state is one of tradable, full_suspension, intraday_halt, or
unknown. A missing tick or empty order book is never treated as suspension;
when the upstream quote does not provide an explicit state, the result remains
unknown.
Reliable source limits are exposed through the standard TickData.limit_up
and TickData.limit_down fields. Order events retain the broker message and
also expose ths_reject_code and ths_queue_state in OrderData.extra, so an
upper layer can distinguish an active order, a queued order, insufficient
sellable quantity, price-limit rejection, and an unknown failure without
parsing UI text itself.
After Tonghuashun confirms an order submission, the gateway returns control to the strategy thread without synchronously traversing the order table. The existing background broker snapshot reconciles the temporary local order with the real contract or trade record. A temporary order without a contract number is removed only after that authoritative snapshot identifies the matching real order or fill; account cash alone is never used to guess an order rejection.
Random Split Orders
With enable_random_split_order = true, one vn.py order is split into several
real Tonghuashun limit orders with randomized child sizes and randomized delays
between children. This reduces the visibility of a single large order on the
book, but it does not guarantee that a strategy cannot be tracked.
Strategies still call MainEngine.send_order() once. The returned vt_orderid
is a synthetic parent order; real child orders continue to be emitted as normal
order events:
- Parent:
extra["random_split_parent"] = True - Child:
extra["random_split_child"] = True - Child events include
extra["split_parent_vt_orderid"]
Canceling the parent order makes the gateway try to cancel every still cancelable child order. Parent aggregation is in-process only; after a process restart, Tonghuashun only has the real child orders.
Usage
GUI
from vnpy.event import EventEngine
from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import MainWindow, create_qapp
from vnpy_ths import THSGateway
qapp = create_qapp()
event_engine = EventEngine()
main_engine = MainEngine(event_engine)
main_engine.add_gateway(THSGateway, "THS")
main_window = MainWindow(main_engine, event_engine)
main_window.showMaximized()
# Empty setting makes the gateway read ~/.vnpy_ths/config.toml.
main_engine.connect({}, "THS")
qapp.exec()
On startup this reads ~/.vnpy_ths/config.toml and connects the THS gateway
automatically.
Programmatic
from vnpy.event import EventEngine
from vnpy.trader.engine import MainEngine
from vnpy_ths import THSGateway
event_engine = EventEngine()
main_engine = MainEngine(event_engine)
main_engine.add_gateway(THSGateway)
main_engine.connect({}, "THS")
Simulated Gateway
THSSimGateway is a 24-hour local simulation gateway for strategy development,
debugging, and regression tests. It does not open Tonghuashun, does not start
the shared market-data service, and does not touch the network.
The runnable teaching steps live in example/README.md.
uv run python example/step01_sim_and_real_account_funds.py
Examples
The example/ directory contains minimal runnable examples:
uv run python example/step01_sim_and_real_account_funds.py
uv run python example/step02_sim_and_real_positions.py
uv run python example/step03_sim_and_real_send_order_latency.py
uv run python example/step04_sim_and_real_orders_trades.py
uv run python example/step05_sim_buy_immediate_fill.py
uv run python example/step06_sim_buy_cancel_reprice.py
uv run python example/step07_sim_sell_partial_cancel_clear.py
uv run python example/step08_cli_market_monitor.py
uv run python example/step09_probe_ths_order_methods.py
uv run python example/step10_ui_gateway.py
step01 uses vn.py MainEngine to run both the local simulation gateway and
the real gateway, then compares vn.py AccountData fund fields. It triggers
account refreshes 10 times with gateway.query_account(), reads the latest
account from the OMS cache with main_engine.get_all_accounts(), measures the
connect/query/close timings, and explicitly uses the default
connect_snapshot="account" so the connect phase only emits account funds.
Each query first presses the trading workspace refresh control and then
re-locates the visible fund table. THS-specific fund values remain in
AccountData.extra on the OMS account object.
step02 compares simulated and real OMS reads for positions only. It triggers
query_position() and reads results from main_engine.get_all_positions();
PositionData.yd_volume is the current sellable quantity, while THS-specific
display fields stay in PositionData.extra.
step03 compares simulated and real order submission latency by looping
main_engine.send_order() 10 times and reading OMS order caches after
EVENT_ORDER; the real timing table separates main-page submit_click,
order-confirmation click, and the delisted rejection-popup loop. step04
compares simulated and real OMS reads for orders, active orders, and trades;
the real side buys 512000.SSE at the limit-down price, reads OMS caches,
then cancels the active order. step05 compares
simulated and real order submission, step06 and step07 are simulation-only
trading flows, and step08 through step10 use the real gateway for
market/event monitoring, UI probing, and GUI startup. Script settings are edited as
constants at the top of each file; they do not use argparse.
Testing
pytest
pytest -m "needs_ths"
pytest -m ""
| Marker | Coverage | Runs |
|---|---|---|
| (none) | config, models, validation, market data, gateway unit logic | default |
needs_ths |
login, refresh, positions, orders, gateway connection | requires THS |
Development Notes
- Realtime market data exclusively uses the shared
tdx-apibatch-quote, trade, and index endpoints. Per-symbol trade facts within one quote batch are fetched with at most 16 concurrent workers while preserving subscription order; ticks are still emitted only after the complete poll batch is ready. - Batch quote and precise last-trade availability are tracked separately. A quote-only tick remains usable as an execution-book fact and emits a visible degraded/recovered log transition without creating a false trade bar.
TickData.datetimeremains the local arrival time.TickData.extraexposes the latest canonical-session trade timestamp/status and poll batch boundary; index ticks additionally expose the latest native completed-minute OHLCV. Status-5 after-hours trades are excluded from these minute facts.pytdxfeedowns~/.pytdxfeedand the user LaunchAgent. A strictly healthy external service on port 8080 is reused; an unhealthy unknown owner fails explicitly and is never terminated.- A first-quote failure aborts gateway connect. Runtime polling retries only the same tdx-api service and never switches sources.
- Order, trade, and cancel reports with fast order reconciliation and Chinese logs.
- Random split order configuration, disabled by default.
- Public deterministic
THSSimGatewayfor local debug and CI. - Minimal examples: simulation gateway demo, market monitor, one-shot order sender, GUI gateway, and THS UI probe.
Project Layout
src/vnpy_ths/
├── gateway.py # vn.py THSGateway
├── sim_gateway.py # deterministic THSSimGateway for local debug/tests
├── td.py # trading adapter
├── broker.py # Tonghuashun UI trading facade
├── config.py # TOML config
├── md.py # exclusive tdx-api feed backed by pytdxfeed
├── security_names.py # AKShare security name cache
├── models.py # domain models
├── validation.py # A-share code, size, and price validation
└── _ui/ # macOS UI automation
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vnpy_ths-0.5.9.tar.gz.
File metadata
- Download URL: vnpy_ths-0.5.9.tar.gz
- Upload date:
- Size: 323.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
feb646b77d2b12f0c8c8ea084f80a8644945ebf6f6b54ce4d6c987737874f36e
|
|
| MD5 |
ae3661c11839f1e9b151092b21736376
|
|
| BLAKE2b-256 |
5dacf306e3e2087de0623f364b324688faa2567374195e928779153bfa4f1a58
|
File details
Details for the file vnpy_ths-0.5.9-py3-none-any.whl.
File metadata
- Download URL: vnpy_ths-0.5.9-py3-none-any.whl
- Upload date:
- Size: 112.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d710918a250855251e872cfbed9fbc369a5179e71058a0fdc6927cc7315b151a
|
|
| MD5 |
a791be0235b15e627d344e9b248cf87b
|
|
| BLAKE2b-256 |
14147454c9dd6e8b18f24bbf28423b69449b65b5994b8e37342f5f35c93ed0d8
|