Skip to main content

Tonghuashun A-share gateway for VeighNa

English | 中文

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 .app is the sole source of process identity. The gateway reads CFBundleIdentifier and CFBundleExecutable from its Contents/Info.plist, so Standard/Pro builds and renamed executables are not identified through a hard-coded product list. With an empty app_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-api managed by pytdxfeed. The host is fixed at 127.0.0.1; the port defaults to 8080. Before touching the Tonghuashun UI, pytdxfeed ensures 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:

  1. If ~/.vnpy_ths/config.toml exists, it always wins:

    main_engine.connect({}, "THS")
    
  2. 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=0 describes 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_suspension or intraday_halt state 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-api batch-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.datetime remains the local arrival time. TickData.extra exposes 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.
  • pytdxfeed owns ~/.pytdxfeed and 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 THSSimGateway for 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

vnpy_ths-0.5.9.tar.gz (323.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vnpy_ths-0.5.9-py3-none-any.whl (112.8 kB view details)

Uploaded Python 3

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

Hashes for vnpy_ths-0.5.9.tar.gz
Algorithm Hash digest
SHA256 feb646b77d2b12f0c8c8ea084f80a8644945ebf6f6b54ce4d6c987737874f36e
MD5 ae3661c11839f1e9b151092b21736376
BLAKE2b-256 5dacf306e3e2087de0623f364b324688faa2567374195e928779153bfa4f1a58

See more details on using hashes here.

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

Hashes for vnpy_ths-0.5.9-py3-none-any.whl
Algorithm Hash digest
SHA256 d710918a250855251e872cfbed9fbc369a5179e71058a0fdc6927cc7315b151a
MD5 a791be0235b15e627d344e9b248cf87b
BLAKE2b-256 14147454c9dd6e8b18f24bbf28423b69449b65b5994b8e37342f5f35c93ed0d8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page