Metadata-Version: 2.5
Name: vnpy-ths
Version: 0.5.9
Summary: Tonghuashun (同花顺) A-share trading gateway for VeighNa.
Project-URL: homepage, https://github.com/hermanzhaozzzz/vnpy_ths
Project-URL: repository, https://github.com/hermanzhaozzzz/vnpy_ths
Author-email: Huanan Herman Zhao <hermanzhaozzzz@gmail.com>
Maintainer-email: Huanan Herman Zhao <hermanzhaozzzz@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: a-share,algotrading,investment,quant,quantitative,ths,tonghuashun,trading
Classifier: Development Status :: 4 - Beta
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: <3.14,>=3.11
Requires-Dist: akshare<2.0.0,>=1.17.0
Requires-Dist: coloredlogs<16.0.0,>=15.0.1
Requires-Dist: pillow<13.0.0,>=12.0.0
Requires-Dist: prettytable<4.0.0,>=3.17.0
Requires-Dist: pydantic<3.0.0,>=2.12.4
Requires-Dist: pyobjc<13.0,>=12.1
Requires-Dist: pytdxfeed==1.0.5
Requires-Dist: pytesseract<0.4.0,>=0.3.13
Requires-Dist: requests<3.0.0,>=2.32.0
Requires-Dist: vnpy<5.0.0,>=4.4.0
Provides-Extra: dev
Requires-Dist: ruff; extra == 'dev'
Requires-Dist: toml; extra == 'dev'
Provides-Extra: test
Requires-Dist: pytest; extra == 'test'
Requires-Dist: pytest-cov; extra == 'test'
Description-Content-Type: text/markdown

# Tonghuashun A-share gateway for [VeighNa](https://github.com/vnpy/vnpy)

<p align="center">
    <img src ="https://img.shields.io/badge/version-0.5.9-blueviolet.svg"/>
    <img src ="https://img.shields.io/badge/platform-macos-yellow.svg"/>
    <img src ="https://img.shields.io/badge/python-3.11|3.12|3.13-blue.svg"/>
    <img src ="https://img.shields.io/badge/license-MIT-orange.svg"/>
</p>

<p align="center">
    <a href="README.md">English</a> | <a href="README_CN.md">中文</a>
</p>


## 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

```bash
git clone https://github.com/hermanzhaozzzz/vnpy_ths.git
cd vnpy_ths
uv sync --locked --all-extras --all-groups
```

Run commands through uv:

```bash
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`.

```bash
mkdir -p ~/.vnpy_ths
```

```toml
# ~/.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:

   ```python
   main_engine.connect({}, "THS")
   ```

2. If the default file does not exist, an explicit TOML path must be used:

   ```python
   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:

```toml
[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"
```

```bash
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

```python
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

```python
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`.

```bash
uv run python example/step01_sim_and_real_account_funds.py
```

### Examples

The `example/` directory contains minimal runnable examples:

```bash
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

```bash
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

```text
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
```
