Metadata-Version: 2.1
Name: qstrade
Version: 0.0.2
Summary: Chinese market data, CTP trading and event-driven strategy framework
Keywords: trading,futures,ctp,insight,tqsdk
Classifier: Development Status :: 3 - Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Topic :: Office/Business :: Financial :: Investment
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: test

# qstrade

面向中国股票与期货的 Python 行情、交易及事件驱动策略库。当前版本 **0.0.2**，提供 Insight、天勤行情适配、CTP 期货交易及 MA5/MA10 策略示例。

公共接口统一使用 Wind 证券代码；Tick、Bar 以统一字典返回。导入库不会登录账户或加载原生 SDK，由调用方管理连接与退出。

## 环境与依赖

项目支持 Python 3.9 及以上版本，原生 SDK 还需匹配操作系统、Python 版本和系统位数；已验证环境为 Windows、Python 3.9。

默认依赖包括 `tqsdk==3.8.3`、`gdp-insight-py-win==1.0.0` 和 `insight-py-win==6.1.1`。其中 `gdp-insight-py-win` 提供 `gdp_python`，当前 Insight 适配器使用的 `insight_python` 由 `insight-py-win` 提供。CTP Python 绑定仍需由调用方提供，并通过配置指定 SDK 路径。

## 配置与初始化

`create_market()` 根据默认配置创建未登录实例，`login()` 成功后返回该实例自身，支持链式调用；登录失败会抛出异常并尝试清理会话。

```python
from qstrade.market import create_market

market = create_market().login()
try:
    market.subscribe_tick(["C2701.DCE"])  # 默认打印标准 Tick 数据
    input("按回车结束订阅\n")
finally:
    market.logout()
```

系统只从 JSON 配置文件读取配置，不读取环境变量，也不会被环境变量覆盖。默认读取当前工作目录的 `config.local.json`；显式调用 `load_config(path)` 时只读取指定文件。指定文件不存在会报错，默认文件不存在时使用内置默认值及空凭据，连接前仍需填写实际账户配置。相对 SDK 和运行目录路径以当前工作目录为基准。

需要指定配置文件或行情源时：

```python
from qstrade.config import load_config
from qstrade.market import create_market

settings = load_config("config.local.json")
market = create_market(provider="tqsdk", settings=settings).login()
try:
    print("行情已登录:", market.connected)
finally:
    market.logout()
```

`provider` 可选 `insight` 或 `tqsdk`，省略时使用 `market.default_provider`。策略引擎按交易类型选择 `market.futures_provider` 或 `market.stock_provider`；天勤适配目前只支持期货。凭据统一放在配置文件的 `insight`、`tqsdk`、`ctp` 段中，CTP SDK 路径使用 `ctp.sdk_path`。

独立使用天勤行情的最小配置文件示例：

```json
{
  "market": {
    "default_provider": "tqsdk",
    "futures_provider": "tqsdk",
    "stock_provider": "insight"
  },
  "tqsdk": {
    "username": "填写天勤账号",
    "password": "填写天勤密码"
  }
}
```

使用策略和 CTP 交易时，需在同一文件中补齐 `trader`、`ctp` 配置；完整模板包含各行情源和交易通道的配置字段。

需要创建空白配置文件时，可在 Python 中读取包内模板，已有文件不会被覆盖：

```python
from importlib.resources import files
from pathlib import Path

content = files("qstrade").joinpath("config.example.json").read_text(encoding="utf-8")
with Path("config.local.json").open("x", encoding="utf-8") as target:
    target.write(content)
```

配置字段及生命周期详见 [配置说明](docs/configured-channels.md)。

## Wind 统一证券代码

策略、行情订阅、历史查询、交易报单和合约查询均使用同一套 Wind 格式，回调与查询结果中的 `symbol` 也遵循该格式。调用方无需自行转换供应商代码，转换集中在 `qstrade.utils.code_utils`，由行情和交易适配器内部调用。

### 股票

格式为 **六位数字 + 点 + 两位大写交易所代码**，代码中的前导零必须保留。

| 交易所 | 后缀 | 示例 |
| --- | --- | --- |
| 上海证券交易所 | `SH` | `600397.SH` |
| 深圳证券交易所 | `SZ` | `300750.SZ`、`000001.SZ` |
| 北京证券交易所 | `BJ` | `920590.BJ` |

### 期货

格式为 **大写品种代码 + 四位年月 YYMM + 点 + 三位大写交易所代码**。月份取 `01` 至 `12`；郑商所公共代码同样使用四位年月。月均等合约的特殊大写字母保留在年月之后，例如 `PP2701F.DCE`。

| 交易所 | 后缀 | 示例 |
| --- | --- | --- |
| 上海期货交易所 | `SHF` | `SP2704.SHF` |
| 上海国际能源交易中心 | `INE` | `SC2709.INE` |
| 广州期货交易所 | `GFE` | `SI2703.GFE` |
| 中国金融期货交易所 | `CFE` | `IH2612.CFE` |
| 大连商品交易所 | `DCE` | `JM2706.DCE`、`PP2701F.DCE` |
| 郑州商品交易所 | `CZC` | `SA2708.CZC` |

例如 `SA2708.CZC` 表示纯碱 2027 年 8 月合约；调用方始终使用这个代码，CTP 所需的 `SA708`、天勤所需的 `CZCE.SA708` 由适配器转换。

以下是连接已建立后的调用方式，报单示例仅定义函数，不会自动执行：

```python
# market 已登录，trader 已连接。
market.subscribe_tick(["SA2708.CZC"])
market.subscribe_kline(["SA2708.CZC"], "1min")
bars = market.get_kline(
    ["SA2708.CZC"], "2026-10-21", "2026-10-24", frequency="1min"
)
contracts = trader.query_instruments("SA2708.CZC")


def buy_one_lot(trader, limit_price):
    return trader.place_order("SA2708.CZC", "BUY", "OPEN", limit_price, 1)
```

公共接口不接受 `rb2701`、`SA708` 或 `DCE.c2701` 等供应商写法。格式正确不代表合约已挂牌或账户具备权限，示例代码需替换为实际可用合约。更多转换规则见 [Wind 代码说明](docs/wind-symbols.md)。

## 行情模块

Insight 和天勤的 Tick 订阅、K 线订阅及历史 K 线查询使用统一结构。`TickData`、`BarData`、`QuoteLevel` 定义在 `qstrade.market.data`，使用 `TypedDict` 描述，运行时均为普通 Python 字典。

### 订阅与历史查询

订阅时通过 `callback` 传入处理函数，省略或传入 `None` 时默认打印数据。`get_kline()` 返回 `list[BarData]`，与 K 线回调中的单条记录结构一致。

```python
import time
from datetime import datetime, timedelta

from qstrade.market import create_market
from qstrade.market.data import TickData, BarData


def on_tick(tick: TickData):
    print("Tick:", tick["symbol"], tick["last_price"], tick["volume"])


def on_bar(bar: BarData):
    print("Bar:", bar["symbol"], bar["timestamp"], bar["close"], bar["is_closed"])


market = create_market(provider="tqsdk").login()
try:
    symbol = "C2701.DCE"
    market.subscribe_tick([symbol], callback=on_tick)
    market.subscribe_kline([symbol], "1min", callback=on_bar)

    # 查询参数采用上海本地、无时区的 datetime。
    end = datetime(2026, 10, 23, 15, 0)
    bars = market.get_kline(
        [symbol], end - timedelta(days=1), end, frequency="1min", fq="none"
    )
    for bar in bars[-3:]:
        on_bar(bar)

    time.sleep(30)  # 保持进程存活，接收订阅回调
finally:
    market.logout()
```

查询日期及合约按实际需求调整；历史范围受行情源和账户权限限制。回调应快速返回，不要在 SDK 回调内同步查询、订阅、登录或释放会话。各行情源支持的频率不同，类型中存在某个频率不代表所有行情源都支持。

### 共用字段与数据约定

Tick 和 Bar 都包含以下身份字段。下表及后续表格中的 `?` 表示字段值可为 `None`，所有字段键仍会出现。缺失值不填成零，真实零成交量、零盘口量保留为 `0`。

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `symbol` | `str` | Wind 证券代码 |
| `exchange` | `str` | Wind 交易所后缀，与 `symbol` 一致 |
| `asset_type` | `str` | `stock`（股票）或 `futures`（期货） |
| `source` | `str` | 数据来源，如 `insight`、`tqsdk`、`replay` |
| `trading_day` | `str?` | 交易所交易日，格式 `YYYY-MM-DD`；未知时为空 |

时间统一为上海本地字符串 `YYYY-MM-DD HH:MM:SS[.小数秒]`，不带时区后缀，保留来源精度。夜盘事件日期可能与交易日不同，不应从 `timestamp` 的日期直接推算 `trading_day`。

成交量单位为股票“股”、期货“手”，成交额单位为人民币元。期货价格保留合约原生报价单位，合约乘数不会乘入价格。

### TickData：行情快照

Tick 是某一时点的行情快照，盘口更新也可能触发回调，不等同于一笔成交。除共用字段外，包含以下字段：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `timestamp` | `str` | 交易所事件时间，不用本地接收时间替代 |
| `last_price` | `float?` | 最新成交价 |
| `open` / `high` / `low` | `float?` | 本交易日开盘价、截至当前最高价和最低价 |
| `close` | `float?` | 本交易日官方收盘价，尚未提供时为空 |
| `pre_close` | `float?` | 前一交易日可比收盘参考价 |
| `volume` | `int?` | **当日累计成交量** |
| `amount` | `float?` | **当日累计成交额** |
| `quotes` | `list[QuoteLevel]?` | 按买卖一档起依次排列的盘口列表 |
| `upper_limit` / `lower_limit` | `float?` | 本交易日涨停价、跌停价 |
| `open_interest` | `int?` | 当前期货持仓量，单位手 |
| `pre_open_interest` | `int?` | 上一交易日期货持仓量，单位手 |
| `settlement` | `float?` | 本交易日期货结算价，发布后才有值 |
| `pre_settlement` | `float?` | 上一交易日期货结算价 |

股票的持仓量和结算价相关字段为 `None`。Tick 仅保留 `volume`、`amount` 表示当日累计量额，不提供 `cum_*`、`delta_*` 字段；最新价字段为 `last_price`，没有 `price` 别名。

`QuoteLevel` 的每个档位包含：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `bid_price` | `float?` | 买价 |
| `bid_volume` | `int?` | 该买价上的委托量 |
| `ask_price` | `float?` | 卖价 |
| `ask_volume` | `int?` | 该卖价上的委托量 |

盘口量单位同成交量，索引 `0` 是买卖一档。档位按实际来源保留，不用零补齐至固定深度；`quotes=None` 表示未提供盘口，中间缺失档位不会导致后续档位前移。

以下为字段完整的虚构期货 Tick 示例：

```python
from qstrade.market.data import TickData

tick: TickData = {
    "symbol": "C2701.DCE",
    "exchange": "DCE",
    "asset_type": "futures",
    "source": "tqsdk",
    "trading_day": None,
    "timestamp": "2026-10-23 09:30:00.123456",
    "last_price": 2300.0,
    "open": 2290.0,
    "high": 2305.0,
    "low": 2288.0,
    "close": None,
    "pre_close": 2295.0,
    "volume": 12000,
    "amount": None,
    "quotes": [
        {"bid_price": 2299.0, "bid_volume": 12, "ask_price": 2300.0, "ask_volume": 8}
    ],
    "upper_limit": 2400.0,
    "lower_limit": 2200.0,
    "open_interest": 300000,
    "pre_open_interest": 298000,
    "settlement": None,
    "pre_settlement": 2294.0,
}
```

### BarData：K 线

实时和历史 K 线采用相同结构。除共用字段外，包含以下字段：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `frequency` | `str` | 周期，如 `15s`、`1min`、`daily`、`weekly`、`monthly` |
| `timestamp` | `str?` | **区间起点**；无法确认起点时为空 |
| `open` / `high` / `low` / `close` | `float?` | 区间开盘、最高、最低、收盘价；形成中 K 线的 close 为区间内最新价 |
| `volume` | `int?` | **该区间成交量**，不是当日累计量 |
| `amount` | `float?` | **该区间成交额** |
| `open_interest` | `int?` | 区间终点期货持仓量；形成中 K 线为当前值 |
| `open_interest_start` | `int?` | 区间起点期货持仓量 |
| `pre_close` | `float?` | 仅日线：前一交易日可比收盘参考价 |
| `settlement` / `pre_settlement` | `float?` | 仅期货日线：当日和前一交易日结算价 |
| `fq` | `str` | `none` 不复权、`pre` 前复权、`post` 后复权；期货固定为 `none` |
| `is_closed` | `bool?` | `True` 已确认完成、`False` 正在形成、`None` 无法确认 |

Bar 的时间字段只有 `timestamp`，不另存 `start_time`、`end_time`；价格使用 OHLC，不提供 `price=close` 别名。股票的持仓量和结算字段为空，分钟、周、月线的日线专用参考字段为空。

以下为字段完整的虚构分钟 K 线示例：

```python
from qstrade.market.data import BarData

bar: BarData = {
    "symbol": "C2701.DCE",
    "exchange": "DCE",
    "asset_type": "futures",
    "source": "tqsdk",
    "trading_day": None,
    "frequency": "1min",
    "timestamp": "2026-10-23 09:30:00.000000000",
    "open": 2300.0,
    "high": 2302.0,
    "low": 2298.0,
    "close": 2301.0,
    "volume": 120,
    "amount": None,
    "open_interest": 300010,
    "open_interest_start": 300000,
    "pre_close": None,
    "settlement": None,
    "pre_settlement": None,
    "fq": "none",
    "is_closed": True,
}
```

目前天勤未提供的 K 线成交额及交易日保持 `None`。Insight K 线原生时间的起止语义尚未确认，默认 `timestamp=None`，均线策略会跳过；独立确认所用接口时间标签确为周期起点后，才可配置 `insight.bar_time_label="start"`。详细口径见 [行情数据结构](docs/market-data.md)。

## 交易模块

`create_trader()` 创建未连接的交易实例，由调用方执行 `connect()` 和 `disconnect()`。以下示例仅查询账户和持仓：

```python
from qstrade.trader import create_trader

trader = create_trader()
try:
    trader.connect()
    account = trader.query_account()
    positions = trader.query_positions()
    print(account)
    print(positions)
finally:
    trader.disconnect()
```

报单使用 `place_order(symbol, direction, offset, price, volume)`，撤单使用 `cancel_order(order_id)`。返回订单 ID 只表示本地已提交请求，受理和成交状态以委托、成交回报为准；查询与回报中的证券代码仍为 Wind 格式。

## 策略示例

`StrategyEngine` 负责行情和交易通道的生命周期，策略实现 `on_tick()` / `on_kline()` 等回调。下例使用默认配置启动 MA5/MA10 观察模式，不提交策略订单：

```python
import time
from qstrade.strategy import StrategyEngine
from qstrade.strategy.examples import MaCrossStrategy

engine = StrategyEngine(trading_enabled=False)
strategy = MaCrossStrategy(
    "ma5_ma10", engine, "C2701.DCE", volume=1, frequency="1min", warmup_days=0
)
engine.add_strategy(strategy)
try:
    engine.start()
    time.sleep(30)
finally:
    engine.stop()
```

显式设置 `trading_enabled=True` 后允许策略报撤单，库中该参数默认值也是 `True`，调用方应明确指定。MA 示例在 MA5 高于 MA10 且空仓时开多，低于时平多；不开空、不加仓，不要求前后两组均线发生交叉。持仓按实际成交维护，停止时尝试撤销本次引擎的在途订单，不主动平仓。完整生命周期和执行规则见 [策略说明](docs/strategy.md)。

无需账户的离线回放也可以直接通过 Python 调用：

```python
from qstrade import __version__
from qstrade.strategy.examples.replay import run_replay

print(__version__)
print(run_replay())
# {'orders': 2, 'trades': 2, 'position': 0}
```

回放仅验证事件与成交处理流程，不模拟真实交易成本或收益。

## 项目结构

| 路径 | 内容 |
| --- | --- |
| `src/qstrade/market/` | 行情接口、统一数据结构及供应商适配 |
| `src/qstrade/trader/` | 交易接口及 CTP 适配 |
| `src/qstrade/strategy/` | 策略基类、引擎、均线与回放示例 |
| `src/qstrade/utils/` | 代码转换、时间、回调及异常工具 |
| `src/qstrade/config.py` | 配置模型与加载函数 |
| `examples/` | Python 调用示例 |
| `tests/` | 离线回归测试 |
| `docs/` | 接口与策略详细说明 |

从旧项目迁移时，将 `from strategy_system...` 改为 `from qstrade...`，行情统一使用 `create_market().login()` 初始化。
