mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 13:24:15 +08:00
fix: resolve all CI mypy (265→0) and ruff (26→0) errors
- pyproject.toml: add mypy overrides for pandas/tabulate/matplotlib stubs, disable strict checking for vendored MyTT library - config.py: use cast() for dict[str, Any] .get() returns - beichi.py: widen _calc_bi_force param to BI | XD, import XD - backtest/cli.py: split combo/single strategy into separate typed variables - backtest/combo.py: add bool_array() helper for numpy return types - chanlun/analyser.py: type ignore for pandas row access, fix dict type arg - unified.py: change fields param from object to Any - ex/mac_client.py: add type args to list literals - cli/cmd_offline.py: wrap int market as Market enum before API call - cli/cmd_chanlun.py: fix dict type arg - offline/write_*.py: explicit int() cast for struct.unpack returns - MyTT.py: fix line-too-long comments, UP038 isinstance syntax - tests: fix E712 (==False → ~mask), E741 (noqa), F841, import sorting - ruff format applied across codebase Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
5aac7d3a39
commit
4dfd18050e
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Skill(update-config)",
|
||||||
|
"Bash(echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"D:\\\\\\\\python\\\\\\\\xmtdx\\\\\\\\src\\\\\\\\xmtdx\\\\\\\\__init__.py\"}}')",
|
||||||
|
"Bash(python -c \"import sys,json,subprocess;d=json.load\\(sys.stdin\\);f=d.get\\('tool_input',{}\\).get\\('file_path',''\\);exec\\('' if not f.endswith\\(\\\\\\\\'.py\\\\\\\\'\\) else 'subprocess.run\\([\\\\\\\\'ruff\\\\\\\\',\\\\\\\\'check\\\\\\\\',\\\\\\\\'--fix\\\\\\\\',f],capture_output=True\\);subprocess.run\\([\\\\\\\\'ruff\\\\\\\\',\\\\\\\\'format\\\\\\\\',f],capture_output=True\\)'\\);print\\(f'done: {f}'\\)\")",
|
||||||
|
"Bash(python *)",
|
||||||
|
"Bash(ruff check *)",
|
||||||
|
"Bash(git add *)",
|
||||||
|
"Bash(git commit *)",
|
||||||
|
"Bash(git push *)",
|
||||||
|
"mcp__zread__get_repo_structure",
|
||||||
|
"Bash(pip install *)",
|
||||||
|
"mcp__web-reader__webReader",
|
||||||
|
"Bash(python3 *)",
|
||||||
|
"Bash(where mypy *)",
|
||||||
|
"Bash(pip list *)",
|
||||||
|
"Bash(uv run *)",
|
||||||
|
"Bash(git stash *)",
|
||||||
|
"Bash(dir /s /b src\\\\xmtdx)",
|
||||||
|
"Bash(git remote *)",
|
||||||
|
"Bash(uv --version)",
|
||||||
|
"Bash(git rm *)",
|
||||||
|
"mcp__plugin_context7_context7__resolve-library-id",
|
||||||
|
"mcp__plugin_context7_context7__query-docs",
|
||||||
|
"Bash(grep -E \"\\\\.py$\")",
|
||||||
|
"Bash(awk '{added+=$1; deleted+=$2} END {print \"+\" added \" -\" deleted}')",
|
||||||
|
"Bash(find D:/python/easty_tdx/examples/1[1-7]* -name \"*.py\")",
|
||||||
|
"Bash(easy-tdx ping *)",
|
||||||
|
"Bash(easy-tdx version *)",
|
||||||
|
"Bash(easy-tdx kline *)",
|
||||||
|
"Bash(easy-tdx quote *)",
|
||||||
|
"Bash(easy-tdx tick *)",
|
||||||
|
"Bash(easy-tdx transaction *)",
|
||||||
|
"Bash(easy-tdx auction *)",
|
||||||
|
"Bash(easy-tdx quote-list *)",
|
||||||
|
"Bash(easy-tdx board-list *)",
|
||||||
|
"Bash(easy-tdx board-members *)",
|
||||||
|
"Bash(easy-tdx belong-board *)",
|
||||||
|
"Bash(easy-tdx capital-flow *)",
|
||||||
|
"Bash(echo \"EXIT CODE: $?\")",
|
||||||
|
"Bash(easy-tdx unusual *)",
|
||||||
|
"Bash(easy-tdx market-stat *)",
|
||||||
|
"Bash(easy-tdx server-info *)",
|
||||||
|
"Bash(easy-tdx symbol-info *)",
|
||||||
|
"Bash(easy-tdx ex *)",
|
||||||
|
"WebSearch",
|
||||||
|
"mcp__zread__search_doc",
|
||||||
|
"mcp__zread__read_file",
|
||||||
|
"Bash(git status *)",
|
||||||
|
"Bash(pip show *)",
|
||||||
|
"Bash(git tag *)",
|
||||||
|
"Bash(twine upload *)",
|
||||||
|
"Bash(pip index *)",
|
||||||
|
"Bash(curl -s https://pypi.org/pypi/easy-tdx/json)",
|
||||||
|
"Bash(curl -sI \"https://files.pythonhosted.org/packages/py3/e/easy-tdx/easy_tdx-1.2.0-py3-none-any.whl\")",
|
||||||
|
"Bash(gh run *)",
|
||||||
|
"Bash(easy-tdx *)",
|
||||||
|
"mcp__web-search-prime__web_search_prime",
|
||||||
|
"mcp__plugin_compound-engineering_context7__resolve-library-id",
|
||||||
|
"mcp__plugin_compound-engineering_context7__query-docs",
|
||||||
|
"Bash(pip uninstall *)",
|
||||||
|
"Bash(xargs grep -l \"Command\\\\|Request\\\\|Response\")",
|
||||||
|
"Bash(quotes/stock_xdxr_info.go)",
|
||||||
|
"Bash(mypy src/)",
|
||||||
|
"Bash(ruff format *)",
|
||||||
|
"mcp__plugin_episodic-memory_episodic-memory__search",
|
||||||
|
"Bash(echo \"FK/\")",
|
||||||
|
"Bash(git pull *)",
|
||||||
|
"mcp__playwright__browser_navigate",
|
||||||
|
"mcp__playwright__browser_snapshot",
|
||||||
|
"Bash(gh auth *)",
|
||||||
|
"Bash(rm -rf easy_tdx.wiki)",
|
||||||
|
"Bash(gh repo *)",
|
||||||
|
"Bash(gh api *)",
|
||||||
|
"Bash(mypy src/easy_tdx/backtest/types.py)",
|
||||||
|
"Bash(mypy tests/unit/test_backtest_types.py)",
|
||||||
|
"Bash(mypy src/easy_tdx/backtest/types.py tests/unit/test_backtest_types.py)",
|
||||||
|
"Bash(mypy *)",
|
||||||
|
"Bash(git diff *)",
|
||||||
|
"Bash(echo \"exit: $?\")",
|
||||||
|
"Bash(where easy-tdx *)",
|
||||||
|
"mcp__zai-mcp-server__analyze_image",
|
||||||
|
"Bash(git ls-remote *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,15 @@ packages = ["src/easy_tdx"]
|
|||||||
strict = true
|
strict = true
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = ["pandas", "tabulate", "matplotlib", "matplotlib.*"]
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "easy_tdx.MyTT"
|
||||||
|
disallow_untyped_defs = false
|
||||||
|
disallow_untyped_calls = false
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
# V2.1 2021-6-6 新增 BARSLAST函数 SLOPE,FORCAST线性回归预测函数
|
# V2.1 2021-6-6 新增 BARSLAST函数 SLOPE,FORCAST线性回归预测函数
|
||||||
# V2.3 2021-6-13 新增 TRIX,DPO,BRAR,DMA,MTM,MASS,ROC,VR,ASI等指标
|
# V2.3 2021-6-13 新增 TRIX,DPO,BRAR,DMA,MTM,MASS,ROC,VR,ASI等指标
|
||||||
# V2.4 2021-6-27 新增 EXPMA,OBV,MFI指标, 改进SMA核心函数(核心函数彻底无循环)
|
# V2.4 2021-6-27 新增 EXPMA,OBV,MFI指标, 改进SMA核心函数(核心函数彻底无循环)
|
||||||
# V2.7 2021-11-21 修正 SLOPE,BARSLAST,函数,新加FILTER,LONGCROSS, 感谢qzhjiang对SLOPE,SMA等函数的指正
|
# V2.7 2021-11-21 修正 SLOPE,BARSLAST,函数,新加FILTER,LONGCROSS,
|
||||||
|
# 感谢qzhjiang对SLOPE,SMA等函数的指正
|
||||||
# V2.8 2021-11-23 修正 FORCAST,WMA函数,欢迎qzhjiang,stanene,bcq加入社群,一起来完善myTT库
|
# V2.8 2021-11-23 修正 FORCAST,WMA函数,欢迎qzhjiang,stanene,bcq加入社群,一起来完善myTT库
|
||||||
# V2.9 2021-11-29 新增 HHVBARS,LLVBARS,CONST, VALUEWHEN功能函数
|
# V2.9 2021-11-29 新增 HHVBARS,LLVBARS,CONST, VALUEWHEN功能函数
|
||||||
# V2.92 2021-11-30 新增 BARSSINCEN函数,现在可以 pip install MyTT 完成安装
|
# V2.92 2021-11-30 新增 BARSSINCEN函数,现在可以 pip install MyTT 完成安装
|
||||||
@@ -128,7 +129,7 @@ def WMA(S, N): # 通达信S序列的N日加权移动平均 Yn = (1*X1+2*X2+3*X3
|
|||||||
|
|
||||||
|
|
||||||
def DMA(S, A): # 求S的动态移动平均,A作平滑因子,必须 0<A<1 (此为核心函数,非指标)
|
def DMA(S, A): # 求S的动态移动平均,A作平滑因子,必须 0<A<1 (此为核心函数,非指标)
|
||||||
if isinstance(A, (int, float)):
|
if isinstance(A, int | float):
|
||||||
return pd.Series(S).ewm(alpha=A, adjust=False).mean().values
|
return pd.Series(S).ewm(alpha=A, adjust=False).mean().values
|
||||||
A = np.array(A)
|
A = np.array(A)
|
||||||
A[np.isnan(A)] = 1.0
|
A[np.isnan(A)] = 1.0
|
||||||
@@ -164,7 +165,7 @@ def LAST(S, A, B): # 从前A日到前B日一直满足S_BOOL条件, 要求A>B &
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ------------------ 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信--------------------------------
|
# -- 1级:应用层函数(通过0级核心函数实现)使用方法请参考通达信 --------------------
|
||||||
def COUNT(S, N): # COUNT(CLOSE>O, N): 最近N天满足S_BOO的天数 True的天数
|
def COUNT(S, N): # COUNT(CLOSE>O, N): 最近N天满足S_BOO的天数 True的天数
|
||||||
return SUM(S, N)
|
return SUM(S, N)
|
||||||
|
|
||||||
|
|||||||
@@ -79,16 +79,15 @@ def backtest(
|
|||||||
|
|
||||||
# 1. 加载策略(单策略 or 多因子组合)
|
# 1. 加载策略(单策略 or 多因子组合)
|
||||||
is_combo = combo_strategies is not None
|
is_combo = combo_strategies is not None
|
||||||
strategy = None
|
|
||||||
|
|
||||||
if is_combo:
|
if is_combo:
|
||||||
strategy = _load_combo_strategies(combo_strategies)
|
assert combo_strategies is not None # narrowed by is_combo
|
||||||
|
combo_classes = _load_combo_strategies(combo_strategies)
|
||||||
else:
|
else:
|
||||||
strategy = _load_strategy(strategy_str, strategy_file)
|
strategy_cls = _load_strategy(strategy_str, strategy_file)
|
||||||
|
if strategy_cls is None:
|
||||||
if strategy is None:
|
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
|
||||||
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
|
raise SystemExit(1)
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
# 2. 获取数据
|
# 2. 获取数据
|
||||||
mkt = parse_market(market)
|
mkt = parse_market(market)
|
||||||
@@ -111,21 +110,21 @@ def backtest(
|
|||||||
if is_combo:
|
if is_combo:
|
||||||
from ..backtest.combo import CombinationRunner
|
from ..backtest.combo import CombinationRunner
|
||||||
|
|
||||||
assert strategy is not None # for type checker
|
|
||||||
runner = CombinationRunner(
|
runner = CombinationRunner(
|
||||||
strategy_classes=strategy,
|
strategy_classes=combo_classes,
|
||||||
df=df,
|
df=df,
|
||||||
cash=cash,
|
cash=cash,
|
||||||
commission=commission,
|
commission=commission,
|
||||||
execution=execution,
|
execution=execution,
|
||||||
)
|
)
|
||||||
result = runner.run_combination(
|
result = runner.run_combination(
|
||||||
indices=list(range(len(strategy))),
|
indices=list(range(len(combo_classes))),
|
||||||
mode=combo_mode.upper(),
|
mode=combo_mode.upper(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
assert strategy_cls is not None # guarded above by SystemExit
|
||||||
engine = BacktestEngine(
|
engine = BacktestEngine(
|
||||||
strategy=strategy,
|
strategy=strategy_cls,
|
||||||
cash=cash,
|
cash=cash,
|
||||||
commission=commission,
|
commission=commission,
|
||||||
execution=execution,
|
execution=execution,
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ NDArray = np.ndarray
|
|||||||
BoolArray = npt.NDArray[np.bool_]
|
BoolArray = npt.NDArray[np.bool_]
|
||||||
|
|
||||||
|
|
||||||
|
def bool_array(x: Any) -> BoolArray:
|
||||||
|
"""Ensure the result is a BoolArray (not a scalar bool_)."""
|
||||||
|
return np.asarray(x, dtype=np.bool_)
|
||||||
|
|
||||||
|
|
||||||
# ── 数据结构 ────────────────────────────────────────────────────────────────
|
# ── 数据结构 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -200,15 +205,15 @@ def combine_masks(
|
|||||||
sell_stack = np.stack(sell_arrays)
|
sell_stack = np.stack(sell_arrays)
|
||||||
|
|
||||||
if mode == "AND":
|
if mode == "AND":
|
||||||
return np.all(buy_stack, axis=0), np.all(sell_stack, axis=0)
|
return bool_array(np.all(buy_stack, axis=0)), bool_array(np.all(sell_stack, axis=0))
|
||||||
elif mode == "OR":
|
elif mode == "OR":
|
||||||
return np.any(buy_stack, axis=0), np.any(sell_stack, axis=0)
|
return bool_array(np.any(buy_stack, axis=0)), bool_array(np.any(sell_stack, axis=0))
|
||||||
elif mode == "MAJORITY":
|
elif mode == "MAJORITY":
|
||||||
n_factors = len(signals_list)
|
n_factors = len(signals_list)
|
||||||
threshold = n_factors / 2
|
threshold = n_factors / 2
|
||||||
return (
|
return (
|
||||||
np.sum(buy_stack, axis=0) > threshold,
|
bool_array(np.sum(buy_stack, axis=0) > threshold),
|
||||||
np.sum(sell_stack, axis=0) > threshold,
|
bool_array(np.sum(sell_stack, axis=0) > threshold),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"不支持的合并模式: {mode!r}(可选: AND, OR, MAJORITY)")
|
raise ValueError(f"不支持的合并模式: {mode!r}(可选: AND, OR, MAJORITY)")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ K线合并 → 分型识别 → 笔计算 → 中枢计算 → 线段 → 买卖
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@@ -38,10 +39,10 @@ def _df_to_klines(df: pd.DataFrame) -> list[Kline]:
|
|||||||
Kline(
|
Kline(
|
||||||
index=i,
|
index=i,
|
||||||
date=dt,
|
date=dt,
|
||||||
open=float(row.open),
|
open=float(row.open), # type: ignore[arg-type]
|
||||||
close=float(row.close),
|
close=float(row.close), # type: ignore[arg-type]
|
||||||
high=float(row.high),
|
high=float(row.high), # type: ignore[arg-type]
|
||||||
low=float(row.low),
|
low=float(row.low), # type: ignore[arg-type]
|
||||||
amount=float(vol),
|
amount=float(vol),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -64,7 +65,7 @@ class ChanlunResult:
|
|||||||
bcs: list[BC] = field(default_factory=list)
|
bcs: list[BC] = field(default_factory=list)
|
||||||
macd: dict[str, list[float]] = field(default_factory=dict)
|
macd: dict[str, list[float]] = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
"""将结果转为可序列化的字典(用于 JSON 输出)。"""
|
"""将结果转为可序列化的字典(用于 JSON 输出)。"""
|
||||||
return {
|
return {
|
||||||
"code": self.code,
|
"code": self.code,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from easy_tdx.chanlun.config import ChanlunConfig
|
from easy_tdx.chanlun.config import ChanlunConfig
|
||||||
from easy_tdx.chanlun.types import BC, BI, ZS, BCType
|
from easy_tdx.chanlun.types import BC, BI, XD, ZS, BCType
|
||||||
|
|
||||||
|
|
||||||
def check_bi_beichi(
|
def check_bi_beichi(
|
||||||
@@ -165,7 +165,7 @@ def _check_qs_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
|
|||||||
return bcs
|
return bcs
|
||||||
|
|
||||||
|
|
||||||
def _calc_bi_force(bi: BI) -> float:
|
def _calc_bi_force(bi: BI | XD) -> float:
|
||||||
"""计算笔的力度(简化:用幅度表示)。
|
"""计算笔的力度(简化:用幅度表示)。
|
||||||
|
|
||||||
真正的力度应用 MACD 面积,这里用幅度作为简化替代。
|
真正的力度应用 MACD 面积,这里用幅度作为简化替代。
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ def chanlun(
|
|||||||
click.echo(json.dumps(result_dict, ensure_ascii=False))
|
click.echo(json.dumps(result_dict, ensure_ascii=False))
|
||||||
|
|
||||||
|
|
||||||
def _print_table(result: dict) -> None:
|
def _print_table(result: dict[str, Any]) -> None:
|
||||||
"""以表格形式输出缠论分析结果。"""
|
"""以表格形式输出缠论分析结果。"""
|
||||||
click.echo(f"标的: {result['code']} 周期: {result['frequency']}")
|
click.echo(f"标的: {result['code']} 周期: {result['frequency']}")
|
||||||
click.echo(f"原始K线: {result['kline_count']} 缠论K线: {result['ckline_count']}")
|
click.echo(f"原始K线: {result['kline_count']} 缠论K线: {result['ckline_count']}")
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ def kline(
|
|||||||
mkt = parse_ex_market(market)
|
mkt = parse_ex_market(market)
|
||||||
with get_mac_ex_client() as client:
|
with get_mac_ex_client() as client:
|
||||||
df = client.goods_kline(
|
df = client.goods_kline(
|
||||||
mkt, code,
|
mkt,
|
||||||
|
code,
|
||||||
period=parse_period(period),
|
period=parse_period(period),
|
||||||
start=start,
|
start=start,
|
||||||
count=count,
|
count=count,
|
||||||
|
|||||||
@@ -493,8 +493,9 @@ def _fetch_all_daily_bars(
|
|||||||
Returns:
|
Returns:
|
||||||
SecurityBar 列表(按日期升序)。
|
SecurityBar 列表(按日期升序)。
|
||||||
"""
|
"""
|
||||||
from ..models.enums import KlineCategory
|
from ..models.enums import KlineCategory, Market
|
||||||
|
|
||||||
|
mkt = Market(market)
|
||||||
fetch_fn = client.get_index_bars if is_index else client.get_security_bars
|
fetch_fn = client.get_index_bars if is_index else client.get_security_bars
|
||||||
|
|
||||||
all_bars: list[SecurityBar] = []
|
all_bars: list[SecurityBar] = []
|
||||||
@@ -503,7 +504,7 @@ def _fetch_all_daily_bars(
|
|||||||
max_pages = 50 if need_full else 1 # 50 页 = 40000 条,足够覆盖 A 股全部历史
|
max_pages = 50 if need_full else 1 # 50 页 = 40000 条,足够覆盖 A 股全部历史
|
||||||
|
|
||||||
for _ in range(max_pages):
|
for _ in range(max_pages):
|
||||||
df = fetch_fn(market, code, KlineCategory.DAY, start, page_size)
|
df = fetch_fn(mkt, code, KlineCategory.DAY, start, page_size)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
break
|
break
|
||||||
all_bars.extend(_df_to_bars(df))
|
all_bars.extend(_df_to_bars(df))
|
||||||
|
|||||||
@@ -32,7 +32,14 @@ from .commands.security_list import GetSecurityListCmd
|
|||||||
from .commands.security_quotes import GetSecurityQuotesCmd
|
from .commands.security_quotes import GetSecurityQuotesCmd
|
||||||
from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
|
from .commands.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
|
||||||
from .commands.xdxr_info import GetXdxrInfoCmd
|
from .commands.xdxr_info import GetXdxrInfoCmd
|
||||||
from .config import get_best_host, get_calc_hosts, get_known_hosts, get_port, get_timeout, save_best_host
|
from .config import (
|
||||||
|
get_best_host,
|
||||||
|
get_calc_hosts,
|
||||||
|
get_known_hosts,
|
||||||
|
get_port,
|
||||||
|
get_timeout,
|
||||||
|
save_best_host,
|
||||||
|
)
|
||||||
from .exceptions import TdxConnectionError
|
from .exceptions import TdxConnectionError
|
||||||
from .models.bar import SecurityBar
|
from .models.bar import SecurityBar
|
||||||
from .models.enums import KlineCategory, Market
|
from .models.enums import KlineCategory, Market
|
||||||
|
|||||||
+12
-10
@@ -21,7 +21,7 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
pos = 384
|
pos = 384
|
||||||
(count,) = struct.unpack("<H", data[pos:pos+2])
|
(count,) = struct.unpack("<H", data[pos : pos + 2])
|
||||||
pos += 2
|
pos += 2
|
||||||
|
|
||||||
results: list[TdxBlock] = []
|
results: list[TdxBlock] = []
|
||||||
@@ -40,8 +40,8 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
|
|||||||
break
|
break
|
||||||
|
|
||||||
# 板块元数据 (9 字节名称 + 2 字节股票数 + 2 字节类型)
|
# 板块元数据 (9 字节名称 + 2 字节股票数 + 2 字节类型)
|
||||||
name_b = data[pos:pos+9]
|
name_b = data[pos : pos + 9]
|
||||||
stock_count, _type = struct.unpack("<HH", data[pos+9:pos+13])
|
stock_count, _type = struct.unpack("<HH", data[pos + 9 : pos + 13])
|
||||||
name = name_b.decode("gbk", errors="replace").strip("\x00")
|
name = name_b.decode("gbk", errors="replace").strip("\x00")
|
||||||
|
|
||||||
# 股票代码区 (2800 字节,每只股票 7 字节)
|
# 股票代码区 (2800 字节,每只股票 7 字节)
|
||||||
@@ -51,17 +51,19 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
|
|||||||
actual_count = min(stock_count, 400)
|
actual_count = min(stock_count, 400)
|
||||||
for i in range(actual_count):
|
for i in range(actual_count):
|
||||||
c_start = codes_start + i * 7
|
c_start = codes_start + i * 7
|
||||||
c_raw = data[c_start:c_start+7]
|
c_raw = data[c_start : c_start + 7]
|
||||||
code = c_raw.decode("ascii", errors="replace").strip("\x00")
|
code = c_raw.decode("ascii", errors="replace").strip("\x00")
|
||||||
if code:
|
if code:
|
||||||
codes.append(code)
|
codes.append(code)
|
||||||
|
|
||||||
results.append(TdxBlock(
|
results.append(
|
||||||
name=name,
|
TdxBlock(
|
||||||
category=category,
|
name=name,
|
||||||
count=stock_count,
|
category=category,
|
||||||
codes=codes,
|
count=stock_count,
|
||||||
))
|
codes=codes,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# 跳过整个 2813 字节的记录块
|
# 跳过整个 2813 字节的记录块
|
||||||
pos += 2813
|
pos += 2813
|
||||||
|
|||||||
@@ -7,13 +7,10 @@
|
|||||||
日线及以上(其余 category):4 字节 YYYYMMDD 整数
|
日线及以上(其余 category):4 字节 YYYYMMDD 整数
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from .._binary import unpack_from
|
from .._binary import unpack_from
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_minute(
|
def get_datetime_minute(data: bytes | bytearray, pos: int) -> tuple[int, int, int, int, int, int]:
|
||||||
data: bytes | bytearray, pos: int
|
|
||||||
) -> tuple[int, int, int, int, int, int]:
|
|
||||||
"""解析分钟级时间戳(4 字节)。
|
"""解析分钟级时间戳(4 字节)。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -28,9 +25,7 @@ def get_datetime_minute(
|
|||||||
return year, month, day, hour, minute, pos + 4
|
return year, month, day, hour, minute, pos + 4
|
||||||
|
|
||||||
|
|
||||||
def get_datetime_day(
|
def get_datetime_day(data: bytes | bytearray, pos: int) -> tuple[int, int, int, int]:
|
||||||
data: bytes | bytearray, pos: int
|
|
||||||
) -> tuple[int, int, int, int]:
|
|
||||||
"""解析日期(4 字节 YYYYMMDD)。
|
"""解析日期(4 字节 YYYYMMDD)。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ _HEADER_FMT = "<IIIHH"
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class FrameHeader:
|
class FrameHeader:
|
||||||
magic: int # 协议魔数,恒为 7654321
|
magic: int # 协议魔数,恒为 7654321
|
||||||
seq_id: int # ZipFlag(1B) + 请求 bytes 1-4 回显(3B)
|
seq_id: int # ZipFlag(1B) + 请求 bytes 1-4 回显(3B)
|
||||||
method: int # 请求回显(1B) + 保留(1B) + Method(2B)
|
method: int # 请求回显(1B) + 保留(1B) + Method(2B)
|
||||||
zipsize: int
|
zipsize: int
|
||||||
unzipsize: int
|
unzipsize: int
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""通达信行业配置文件 (tdxhy.cfg) 解析器。"""
|
"""通达信行业配置文件 (tdxhy.cfg) 解析器。"""
|
||||||
|
|
||||||
|
|
||||||
def parse_tdxhy_cfg(content: bytes) -> dict[str, tuple[str, str]]:
|
def parse_tdxhy_cfg(content: bytes) -> dict[str, tuple[str, str]]:
|
||||||
"""解析 tdxhy.cfg 字节内容。
|
"""解析 tdxhy.cfg 字节内容。
|
||||||
|
|
||||||
返回字典: { "code": (tdx_industry, sw_industry), ... }
|
返回字典: { "code": (tdx_industry, sw_industry), ... }
|
||||||
"""
|
"""
|
||||||
results = {}
|
results = {}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
警告:此函数专为成交量设计,不可用于价格字段(pytdx Bug #3)。
|
警告:此函数专为成交量设计,不可用于价格字段(pytdx Bug #3)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
from .._binary import unpack_from
|
from .._binary import unpack_from
|
||||||
|
|
||||||
|
|
||||||
@@ -53,5 +52,5 @@ def _decode_volume(ivol: int) -> float:
|
|||||||
|
|
||||||
def _pow2(exp: int) -> float:
|
def _pow2(exp: int) -> float:
|
||||||
if exp >= 0:
|
if exp >= 0:
|
||||||
return float(1 << exp) if exp < 63 else 2.0 ** exp
|
return float(1 << exp) if exp < 63 else 2.0**exp
|
||||||
return 1.0 / (1 << (-exp)) if -exp < 63 else 2.0 ** exp
|
return 1.0 / (1 << (-exp)) if -exp < 63 else 2.0**exp
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ class GetCompanyInfoCategoryCmd(BaseCommand[list[CompanyInfoCategory]]):
|
|||||||
raw = b[:nul] if nul != -1 else b
|
raw = b[:nul] if nul != -1 else b
|
||||||
return raw.decode("gbk", errors="replace")
|
return raw.decode("gbk", errors="replace")
|
||||||
|
|
||||||
results.append(CompanyInfoCategory(
|
results.append(
|
||||||
name=_decode(name_b),
|
CompanyInfoCategory(
|
||||||
filename=_decode(filename_b),
|
name=_decode(name_b),
|
||||||
start=start,
|
filename=_decode(filename_b),
|
||||||
length=length,
|
start=start,
|
||||||
))
|
length=length,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -52,9 +54,7 @@ class GetCompanyInfoCategoryCmd(BaseCommand[list[CompanyInfoCategory]]):
|
|||||||
class GetCompanyInfoContentCmd(BaseCommand[str]):
|
class GetCompanyInfoContentCmd(BaseCommand[str]):
|
||||||
"""按文件名、偏移、长度读取公司信息文本(GBK 编码)。"""
|
"""按文件名、偏移、长度读取公司信息文本(GBK 编码)。"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, market: Market, code: str, filename: str, offset: int, length: int) -> None:
|
||||||
self, market: Market, code: str, filename: str, offset: int, length: int
|
|
||||||
) -> None:
|
|
||||||
self.market = market
|
self.market = market
|
||||||
self.code = code.encode("utf-8")
|
self.code = code.encode("utf-8")
|
||||||
self.filename = filename.encode("gbk")
|
self.filename = filename.encode("gbk")
|
||||||
@@ -66,7 +66,13 @@ class GetCompanyInfoContentCmd(BaseCommand[str]):
|
|||||||
header = bytes.fromhex("0c07109c0001680068 00d002".replace(" ", ""))
|
header = bytes.fromhex("0c07109c0001680068 00d002".replace(" ", ""))
|
||||||
return header + struct.pack(
|
return header + struct.pack(
|
||||||
"<H6sH80sIII",
|
"<H6sH80sIII",
|
||||||
int(self.market), self.code, 0, fname_padded, self.offset, self.length, 0,
|
int(self.market),
|
||||||
|
self.code,
|
||||||
|
0,
|
||||||
|
fname_padded,
|
||||||
|
self.offset,
|
||||||
|
self.length,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse_response(self, body: bytes) -> str:
|
def parse_response(self, body: bytes) -> str:
|
||||||
|
|||||||
@@ -31,15 +31,41 @@ class GetFinanceInfoCmd(BaseCommand[FinanceInfo]):
|
|||||||
|
|
||||||
fields = struct.unpack(_FIN_FMT, slice_bytes(body, pos, _FIN_SIZE, "finance_info body"))
|
fields = struct.unpack(_FIN_FMT, slice_bytes(body, pos, _FIN_SIZE, "finance_info body"))
|
||||||
(
|
(
|
||||||
liutong_guben, province, industry, updated_date, ipo_date,
|
liutong_guben,
|
||||||
zong_guben, guojia_gu, faqiren_faren_gu, faren_gu, b_gu, h_gu, zhigong_gu,
|
province,
|
||||||
zong_zichan, liudong_zichan, guding_zichan, wuxing_zichan,
|
industry,
|
||||||
|
updated_date,
|
||||||
|
ipo_date,
|
||||||
|
zong_guben,
|
||||||
|
guojia_gu,
|
||||||
|
faqiren_faren_gu,
|
||||||
|
faren_gu,
|
||||||
|
b_gu,
|
||||||
|
h_gu,
|
||||||
|
zhigong_gu,
|
||||||
|
zong_zichan,
|
||||||
|
liudong_zichan,
|
||||||
|
guding_zichan,
|
||||||
|
wuxing_zichan,
|
||||||
gudong_renshu,
|
gudong_renshu,
|
||||||
liudong_fuzhai, changqi_fuzhai, ziben_gongjijin, jing_zichan,
|
liudong_fuzhai,
|
||||||
zhuying_shouru, zhuying_lirun, yingshou_zhangkuan, yingye_lirun,
|
changqi_fuzhai,
|
||||||
touzi_shouyu, jingying_xianjinliu, zong_xianjinliu,
|
ziben_gongjijin,
|
||||||
cunhuo, lirun_zonghe, shuihou_lirun, jing_lirun, weifen_lirun,
|
jing_zichan,
|
||||||
meigujing_zichan, reserve2,
|
zhuying_shouru,
|
||||||
|
zhuying_lirun,
|
||||||
|
yingshou_zhangkuan,
|
||||||
|
yingye_lirun,
|
||||||
|
touzi_shouyu,
|
||||||
|
jingying_xianjinliu,
|
||||||
|
zong_xianjinliu,
|
||||||
|
cunhuo,
|
||||||
|
lirun_zonghe,
|
||||||
|
shuihou_lirun,
|
||||||
|
jing_lirun,
|
||||||
|
weifen_lirun,
|
||||||
|
meigujing_zichan,
|
||||||
|
reserve2,
|
||||||
) = fields
|
) = fields
|
||||||
|
|
||||||
_SCALE = 10000.0 # 财务数据单位:万元/万股
|
_SCALE = 10000.0 # 财务数据单位:万元/万股
|
||||||
|
|||||||
@@ -41,35 +41,39 @@ class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]):
|
|||||||
# 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节
|
# 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节
|
||||||
if len(body) < 11:
|
if len(body) < 11:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
(num,) = struct.unpack("<H", body[9:11])
|
(num,) = struct.unpack("<H", body[9:11])
|
||||||
pos = 11
|
pos = 11
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
for _ in range(num):
|
for _ in range(num):
|
||||||
if len(body) < pos + 36:
|
if len(body) < pos + 36:
|
||||||
break
|
break
|
||||||
|
|
||||||
# 记录格式:4字节日期 + 8个4字节自定义浮点金额
|
# 记录格式:4字节日期 + 8个4字节自定义浮点金额
|
||||||
# [0]日期, [1..4]流入(超/大/中/小), [5..8]流出(超/大/中/小)
|
# [0]日期, [1..4]流入(超/大/中/小), [5..8]流出(超/大/中/小)
|
||||||
raw_data = struct.unpack("<IIIIIIIII", body[pos:pos+36])
|
raw_data = struct.unpack("<IIIIIIIII", body[pos : pos + 36])
|
||||||
|
|
||||||
raw_date = raw_data[0]
|
raw_date = raw_data[0]
|
||||||
year = raw_date // 10000
|
year = raw_date // 10000
|
||||||
month = (raw_date // 100) % 100
|
month = (raw_date // 100) % 100
|
||||||
day = raw_date % 100
|
day = raw_date % 100
|
||||||
|
|
||||||
results.append(HistoricalFundFlow(
|
results.append(
|
||||||
year=year, month=month, day=day,
|
HistoricalFundFlow(
|
||||||
super_in=_decode_volume(raw_data[1]),
|
year=year,
|
||||||
large_in=_decode_volume(raw_data[2]),
|
month=month,
|
||||||
medium_in=_decode_volume(raw_data[3]),
|
day=day,
|
||||||
small_in=_decode_volume(raw_data[4]),
|
super_in=_decode_volume(raw_data[1]),
|
||||||
super_out=_decode_volume(raw_data[5]),
|
large_in=_decode_volume(raw_data[2]),
|
||||||
large_out=_decode_volume(raw_data[6]),
|
medium_in=_decode_volume(raw_data[3]),
|
||||||
medium_out=_decode_volume(raw_data[7]),
|
small_in=_decode_volume(raw_data[4]),
|
||||||
small_out=_decode_volume(raw_data[8]),
|
super_out=_decode_volume(raw_data[5]),
|
||||||
))
|
large_out=_decode_volume(raw_data[6]),
|
||||||
|
medium_out=_decode_volume(raw_data[7]),
|
||||||
|
small_out=_decode_volume(raw_data[8]),
|
||||||
|
)
|
||||||
|
)
|
||||||
pos += 36
|
pos += 36
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ class GetSecurityListCmd(BaseCommand[list[SecurityInfo]]):
|
|||||||
code_bytes,
|
code_bytes,
|
||||||
volunit,
|
volunit,
|
||||||
name_bytes,
|
name_bytes,
|
||||||
_unknown1, # 4字节,排序/分组字段(非用户可见数据)
|
_unknown1, # 4字节,排序/分组字段(非用户可见数据)
|
||||||
decimal_point,
|
decimal_point,
|
||||||
pre_close_raw,
|
pre_close_raw,
|
||||||
_unknown2, # 4字节,私有时间戳(非用户可见数据)
|
_unknown2, # 4字节,私有时间戳(非用户可见数据)
|
||||||
) = struct.unpack("<6sH8s4sBI4s", raw)
|
) = struct.unpack("<6sH8s4sBI4s", raw)
|
||||||
|
|
||||||
code = code_bytes.decode("utf-8", errors="replace").rstrip("\x00")
|
code = code_bytes.decode("utf-8", errors="replace").rstrip("\x00")
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ from typing import Final
|
|||||||
SETUP_CMD1: Final[bytes] = bytes.fromhex("0c0218930001030003000d0001")
|
SETUP_CMD1: Final[bytes] = bytes.fromhex("0c0218930001030003000d0001")
|
||||||
SETUP_CMD2: Final[bytes] = bytes.fromhex("0c0218940001030003000d0002")
|
SETUP_CMD2: Final[bytes] = bytes.fromhex("0c0218940001030003000d0002")
|
||||||
SETUP_CMD3: Final[bytes] = bytes.fromhex(
|
SETUP_CMD3: Final[bytes] = bytes.fromhex(
|
||||||
"0c031899000120002000db0f"
|
"0c031899000120002000db0fd5d0c9ccd6a4a8af0000008fc22540130000d500c9ccbdf0d7ea00000002"
|
||||||
"d5d0c9ccd6a4a8af0000008f"
|
|
||||||
"c22540130000d500c9ccbdf0"
|
|
||||||
"d7ea00000002"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
SETUP_COMMANDS: Final[tuple[bytes, ...]] = (SETUP_CMD1, SETUP_CMD2, SETUP_CMD3)
|
SETUP_COMMANDS: Final[tuple[bytes, ...]] = (SETUP_CMD1, SETUP_CMD2, SETUP_CMD3)
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ class GetTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
|
|||||||
header = bytes.fromhex("0c170801010 10e000e00c50f".replace(" ", ""))
|
header = bytes.fromhex("0c170801010 10e000e00c50f".replace(" ", ""))
|
||||||
return header + struct.pack("<H6sHH", int(self.market), self.code, self.start, self.count)
|
return header + struct.pack("<H6sHH", int(self.market), self.code, self.start, self.count)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def parse_response(self, body: bytes) -> list[TransactionRecord]:
|
def parse_response(self, body: bytes) -> list[TransactionRecord]:
|
||||||
return _parse_transaction_body(body)
|
return _parse_transaction_body(body)
|
||||||
|
|
||||||
@@ -35,9 +33,7 @@ class GetTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
|
|||||||
class GetHistoryTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
|
class GetHistoryTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
|
||||||
"""获取历史某日逐笔成交(date 格式 YYYYMMDD,分页)。"""
|
"""获取历史某日逐笔成交(date 格式 YYYYMMDD,分页)。"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, market: Market, code: str, date: int, start: int, count: int = 800) -> None:
|
||||||
self, market: Market, code: str, date: int, start: int, count: int = 800
|
|
||||||
) -> None:
|
|
||||||
self.market = market
|
self.market = market
|
||||||
self.code = code.encode("utf-8")
|
self.code = code.encode("utf-8")
|
||||||
self.date = date
|
self.date = date
|
||||||
@@ -72,11 +68,17 @@ def _parse_transaction_body(body: bytes) -> list[TransactionRecord]:
|
|||||||
buyorsell, pos = get_price(body, pos)
|
buyorsell, pos = get_price(body, pos)
|
||||||
unknown_last, pos = get_price(body, pos) # Bug #4 修复:不再丢弃
|
unknown_last, pos = get_price(body, pos) # Bug #4 修复:不再丢弃
|
||||||
last_price += price_diff
|
last_price += price_diff
|
||||||
records.append(TransactionRecord(
|
records.append(
|
||||||
hour=hour, minute=minute,
|
TransactionRecord(
|
||||||
price=last_price / 100.0, vol=vol, buyorsell=buyorsell,
|
hour=hour,
|
||||||
unknown_last=unknown_last, _raw=body[record_start:pos],
|
minute=minute,
|
||||||
))
|
price=last_price / 100.0,
|
||||||
|
vol=vol,
|
||||||
|
buyorsell=buyorsell,
|
||||||
|
unknown_last=unknown_last,
|
||||||
|
_raw=body[record_start:pos],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return records
|
return records
|
||||||
|
|
||||||
@@ -93,13 +95,19 @@ def _parse_history_transaction_body(body: bytes) -> list[TransactionRecord]:
|
|||||||
hour, minute, pos = get_time(body, pos)
|
hour, minute, pos = get_time(body, pos)
|
||||||
price_diff, pos = get_price(body, pos)
|
price_diff, pos = get_price(body, pos)
|
||||||
vol, pos = get_price(body, pos)
|
vol, pos = get_price(body, pos)
|
||||||
buyorsell, pos = get_price(body, pos) # 历史无 num_orders
|
buyorsell, pos = get_price(body, pos) # 历史无 num_orders
|
||||||
unknown_last, pos = get_price(body, pos)
|
unknown_last, pos = get_price(body, pos)
|
||||||
last_price += price_diff
|
last_price += price_diff
|
||||||
records.append(TransactionRecord(
|
records.append(
|
||||||
hour=hour, minute=minute,
|
TransactionRecord(
|
||||||
price=last_price / 100.0, vol=vol, buyorsell=buyorsell,
|
hour=hour,
|
||||||
unknown_last=unknown_last, _raw=body[record_start:pos],
|
minute=minute,
|
||||||
))
|
price=last_price / 100.0,
|
||||||
|
vol=vol,
|
||||||
|
buyorsell=buyorsell,
|
||||||
|
unknown_last=unknown_last,
|
||||||
|
_raw=body[record_start:pos],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return records
|
return records
|
||||||
|
|||||||
+12
-12
@@ -27,7 +27,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
_CONFIG_DIR = Path(os.environ.get("EASY_TDX_CONFIG_DIR", str(Path.home() / ".easy_tdx")))
|
_CONFIG_DIR = Path(os.environ.get("EASY_TDX_CONFIG_DIR", str(Path.home() / ".easy_tdx")))
|
||||||
_CONFIG_FILE = _CONFIG_DIR / "config.json"
|
_CONFIG_FILE = _CONFIG_DIR / "config.json"
|
||||||
@@ -137,7 +137,7 @@ _FALLBACK_TIMEOUT = 15.0
|
|||||||
def _load() -> dict[str, Any]:
|
def _load() -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
if _CONFIG_FILE.exists():
|
if _CONFIG_FILE.exists():
|
||||||
return json.loads(_CONFIG_FILE.read_text("utf-8"))
|
return cast(dict[str, Any], json.loads(_CONFIG_FILE.read_text("utf-8")))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return {}
|
return {}
|
||||||
@@ -161,7 +161,7 @@ def get_best_host() -> str:
|
|||||||
if env:
|
if env:
|
||||||
return env
|
return env
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("best_host", _FALLBACK_HOSTS[0])
|
return cast(str, cfg.get("best_host", _FALLBACK_HOSTS[0]))
|
||||||
|
|
||||||
|
|
||||||
def get_known_hosts() -> list[str]:
|
def get_known_hosts() -> list[str]:
|
||||||
@@ -170,25 +170,25 @@ def get_known_hosts() -> list[str]:
|
|||||||
if env:
|
if env:
|
||||||
return [h.strip() for h in env.split(",") if h.strip()]
|
return [h.strip() for h in env.split(",") if h.strip()]
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("known_hosts", list(_FALLBACK_HOSTS))
|
return cast(list[str], cfg.get("known_hosts", list(_FALLBACK_HOSTS)))
|
||||||
|
|
||||||
|
|
||||||
def get_calc_hosts() -> list[str]:
|
def get_calc_hosts() -> list[str]:
|
||||||
"""返回计算服务器列表。"""
|
"""返回计算服务器列表。"""
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("calc_hosts", list(_FALLBACK_CALC_HOSTS))
|
return cast(list[str], cfg.get("calc_hosts", list(_FALLBACK_CALC_HOSTS)))
|
||||||
|
|
||||||
|
|
||||||
def get_mac_hosts() -> list[str]:
|
def get_mac_hosts() -> list[str]:
|
||||||
"""返回 MAC 行情服务器列表。"""
|
"""返回 MAC 行情服务器列表。"""
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("mac_hosts", list(_FALLBACK_MAC_HOSTS))
|
return cast(list[str], cfg.get("mac_hosts", list(_FALLBACK_MAC_HOSTS)))
|
||||||
|
|
||||||
|
|
||||||
def get_ex_hosts() -> list[str]:
|
def get_ex_hosts() -> list[str]:
|
||||||
"""返回扩展行情服务器列表。"""
|
"""返回扩展行情服务器列表。"""
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("ex_hosts", list(_FALLBACK_EX_HOSTS))
|
return cast(list[str], cfg.get("ex_hosts", list(_FALLBACK_EX_HOSTS)))
|
||||||
|
|
||||||
|
|
||||||
def get_best_ex_host() -> str:
|
def get_best_ex_host() -> str:
|
||||||
@@ -197,13 +197,13 @@ def get_best_ex_host() -> str:
|
|||||||
if env:
|
if env:
|
||||||
return env
|
return env
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("best_ex_host", _FALLBACK_EX_HOSTS[0])
|
return cast(str, cfg.get("best_ex_host", _FALLBACK_EX_HOSTS[0]))
|
||||||
|
|
||||||
|
|
||||||
def get_mac_ex_hosts() -> list[str]:
|
def get_mac_ex_hosts() -> list[str]:
|
||||||
"""返回 MAC 协议扩展行情服务器列表。"""
|
"""返回 MAC 协议扩展行情服务器列表。"""
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("mac_ex_hosts", list(_FALLBACK_MAC_EX_HOSTS))
|
return cast(list[str], cfg.get("mac_ex_hosts", list(_FALLBACK_MAC_EX_HOSTS)))
|
||||||
|
|
||||||
|
|
||||||
def get_best_mac_ex_host() -> str:
|
def get_best_mac_ex_host() -> str:
|
||||||
@@ -212,7 +212,7 @@ def get_best_mac_ex_host() -> str:
|
|||||||
if env:
|
if env:
|
||||||
return env
|
return env
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("best_mac_ex_host", _FALLBACK_MAC_EX_HOSTS[0])
|
return cast(str, cfg.get("best_mac_ex_host", _FALLBACK_MAC_EX_HOSTS[0]))
|
||||||
|
|
||||||
|
|
||||||
def get_port() -> int:
|
def get_port() -> int:
|
||||||
@@ -221,7 +221,7 @@ def get_port() -> int:
|
|||||||
if env:
|
if env:
|
||||||
return int(env)
|
return int(env)
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("port", _FALLBACK_PORT)
|
return cast(int, cfg.get("port", _FALLBACK_PORT))
|
||||||
|
|
||||||
|
|
||||||
def get_timeout() -> float:
|
def get_timeout() -> float:
|
||||||
@@ -230,7 +230,7 @@ def get_timeout() -> float:
|
|||||||
if env:
|
if env:
|
||||||
return float(env)
|
return float(env)
|
||||||
cfg = _load()
|
cfg = _load()
|
||||||
return cfg.get("timeout", _FALLBACK_TIMEOUT)
|
return cast(float, cfg.get("timeout", _FALLBACK_TIMEOUT))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,18 +12,20 @@ _MSG_ID = 0x2454
|
|||||||
_HEAD_FLAG = 0x01
|
_HEAD_FLAG = 0x01
|
||||||
|
|
||||||
# 80 字节 Login body,来自 opentdx 参考实现,已通过实际测试验证。
|
# 80 字节 Login body,来自 opentdx 参考实现,已通过实际测试验证。
|
||||||
_LOGIN_BODY = bytes(bytearray.fromhex(
|
_LOGIN_BODY = bytes(
|
||||||
"e5bb1c2fafe52594"
|
bytearray.fromhex(
|
||||||
"1f32c6e5d53dfb41"
|
"e5bb1c2fafe52594"
|
||||||
"5b734cc9cdbf0ac9"
|
"1f32c6e5d53dfb41"
|
||||||
"2021bfdd1eb06d22"
|
"5b734cc9cdbf0ac9"
|
||||||
"d008884c1611cb13"
|
"2021bfdd1eb06d22"
|
||||||
"78f6abd824d899d2"
|
"d008884c1611cb13"
|
||||||
"1f32c6e5d53dfb41"
|
"78f6abd824d899d2"
|
||||||
"1f32c6e5d53dfb41"
|
"1f32c6e5d53dfb41"
|
||||||
"a9325ac935dc0837"
|
"1f32c6e5d53dfb41"
|
||||||
"335a16e4ce17c1bb"
|
"a9325ac935dc0837"
|
||||||
))
|
"335a16e4ce17c1bb"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# EX 协议帧头格式: head_flag(1B) + customize(4B) + version(1B) + zipsize(2B) + unzipsize(2B)
|
# EX 协议帧头格式: head_flag(1B) + customize(4B) + version(1B) + zipsize(2B) + unzipsize(2B)
|
||||||
_EX_HEADER_FMT = "<BIBHH"
|
_EX_HEADER_FMT = "<BIBHH"
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ import pandas as pd
|
|||||||
|
|
||||||
from .._df import _to_df
|
from .._df import _to_df
|
||||||
from ..commands.base import BaseCommand
|
from ..commands.base import BaseCommand
|
||||||
|
from ..config import get_best_mac_ex_host, get_mac_ex_hosts, save_best_mac_ex_host
|
||||||
from ..exceptions import TdxConnectionError
|
from ..exceptions import TdxConnectionError
|
||||||
from .commands.login import MacExLoginCmd
|
|
||||||
from .commands.get_instrument_count import GetExInstrumentCountCmd
|
|
||||||
from .commands.get_instrument_info import GetExInstrumentInfoCmd
|
|
||||||
from ..mac.commands.chart_sampling import ChartSamplingCmd
|
from ..mac.commands.chart_sampling import ChartSamplingCmd
|
||||||
from ..mac.commands.symbol_bar import SymbolBarCmd
|
from ..mac.commands.symbol_bar import SymbolBarCmd
|
||||||
from ..mac.commands.symbol_quotes import SymbolQuotesCmd
|
from ..mac.commands.symbol_quotes import SymbolQuotesCmd
|
||||||
from ..mac.commands.symbol_tick_chart import SymbolTickChartCmd
|
from ..mac.commands.symbol_tick_chart import SymbolTickChartCmd
|
||||||
from ..mac.commands.symbol_transaction import SymbolTransactionCmd
|
from ..mac.commands.symbol_transaction import SymbolTransactionCmd
|
||||||
from ..mac.enums import Adjust, Period, SortOrder, SortType
|
from ..mac.enums import Adjust, Period, SortOrder, SortType
|
||||||
from ..config import get_best_mac_ex_host, get_mac_ex_hosts, save_best_mac_ex_host
|
|
||||||
from ..mac.models import MacQuoteField
|
from ..mac.models import MacQuoteField
|
||||||
|
from .commands.get_instrument_count import GetExInstrumentCountCmd
|
||||||
|
from .commands.get_instrument_info import GetExInstrumentInfoCmd
|
||||||
|
from .commands.login import MacExLoginCmd
|
||||||
from .transport.async_ import AsyncExTdxConnection
|
from .transport.async_ import AsyncExTdxConnection
|
||||||
from .transport.sync import ExTdxConnection, ping_ex_all
|
from .transport.sync import ExTdxConnection, ping_ex_all
|
||||||
|
|
||||||
@@ -194,7 +194,7 @@ class MacExClient:
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
total = self._execute(GetExInstrumentCountCmd())
|
total = self._execute(GetExInstrumentCountCmd())
|
||||||
page_size = 1000
|
page_size = 1000
|
||||||
collected: list = []
|
collected: list[Any] = []
|
||||||
skipped = 0
|
skipped = 0
|
||||||
pos = offset
|
pos = offset
|
||||||
while pos < total and len(collected) < count:
|
while pos < total and len(collected) < count:
|
||||||
@@ -532,7 +532,9 @@ class AsyncMacExClient:
|
|||||||
if not self._auto_reconnect:
|
if not self._auto_reconnect:
|
||||||
raise
|
raise
|
||||||
await self._conn.close()
|
await self._conn.close()
|
||||||
self._conn = AsyncExTdxConnection(self._host, self._port, self._timeout, mac_ex_mode=True)
|
self._conn = AsyncExTdxConnection(
|
||||||
|
self._host, self._port, self._timeout, mac_ex_mode=True
|
||||||
|
)
|
||||||
await self._conn.connect()
|
await self._conn.connect()
|
||||||
await self._login()
|
await self._login()
|
||||||
return await self._conn.execute(cmd)
|
return await self._conn.execute(cmd)
|
||||||
@@ -571,7 +573,7 @@ class AsyncMacExClient:
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
total = await self._execute(GetExInstrumentCountCmd())
|
total = await self._execute(GetExInstrumentCountCmd())
|
||||||
page_size = 1000
|
page_size = 1000
|
||||||
collected: list = []
|
collected: list[Any] = []
|
||||||
skipped = 0
|
skipped = 0
|
||||||
pos = offset
|
pos = offset
|
||||||
while pos < total and len(collected) < count:
|
while pos < total and len(collected) < count:
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ from types import TracebackType
|
|||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
|
from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
|
||||||
from ...config import get_best_ex_host, get_ex_hosts
|
from ...config import get_best_ex_host
|
||||||
from ...exceptions import TdxConnectionError
|
from ...exceptions import TdxConnectionError
|
||||||
from ..models import KNOWN_EX_HOSTS
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ...commands.base import BaseCommand
|
from ...commands.base import BaseCommand
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from ...codec.frame import HEADER_SIZE, decompress_body, parse_header
|
|||||||
from ...config import get_best_ex_host, get_ex_hosts
|
from ...config import get_best_ex_host, get_ex_hosts
|
||||||
from ...exceptions import TdxConnectionError
|
from ...exceptions import TdxConnectionError
|
||||||
from ..commands.get_instrument_count import GetExInstrumentCountCmd
|
from ..commands.get_instrument_count import GetExInstrumentCountCmd
|
||||||
from ..models import KNOWN_EX_HOSTS
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ...commands.base import BaseCommand
|
from ...commands.base import BaseCommand
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import warnings
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from . import MyTT
|
from . import MyTT
|
||||||
|
|||||||
@@ -1189,7 +1189,11 @@ class AsyncMacClient:
|
|||||||
|
|
||||||
fetch_count = max(120 + count, 200)
|
fetch_count = max(120 + count, 200)
|
||||||
df = await self.get_stock_kline(
|
df = await self.get_stock_kline(
|
||||||
market, code, period=period, count=fetch_count, adjust=adjust,
|
market,
|
||||||
|
code,
|
||||||
|
period=period,
|
||||||
|
count=fetch_count,
|
||||||
|
adjust=adjust,
|
||||||
)
|
)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
return df
|
return df
|
||||||
|
|||||||
@@ -59,7 +59,10 @@ class GoodsListCmd(BaseCommand[list[GoodsItem]]):
|
|||||||
offset = 2 + i * _RECORD_SIZE
|
offset = 2 + i * _RECORD_SIZE
|
||||||
require_bytes(body, offset, _RECORD_SIZE, f"GoodsListCmd record[{i}]")
|
require_bytes(body, offset, _RECORD_SIZE, f"GoodsListCmd record[{i}]")
|
||||||
category, raw_name, u, index, switch, v1, v2, v3, c1, c2 = unpack_from(
|
category, raw_name, u, index, switch, v1, v2, v3, c1, c2 = unpack_from(
|
||||||
_RECORD_FMT, body, offset, f"GoodsListCmd record[{i}]",
|
_RECORD_FMT,
|
||||||
|
body,
|
||||||
|
offset,
|
||||||
|
f"GoodsListCmd record[{i}]",
|
||||||
)
|
)
|
||||||
name = raw_name.decode("gbk", errors="replace").rstrip("\x00")
|
name = raw_name.decode("gbk", errors="replace").rstrip("\x00")
|
||||||
items.append(
|
items.append(
|
||||||
|
|||||||
@@ -131,9 +131,7 @@ class UnusualCmd(BaseCommand[list[UnusualItem]]):
|
|||||||
|
|
||||||
desc, value = _describe_unusual(unusual_type, body[offset + 15 : offset + 28])
|
desc, value = _describe_unusual(unusual_type, body[offset + 15 : offset + 28])
|
||||||
|
|
||||||
hour, minute_sec = unpack_from(
|
hour, minute_sec = unpack_from("<BH", body, offset + 29, f"unusual time[{i}]")
|
||||||
"<BH", body, offset + 29, f"unusual time[{i}]"
|
|
||||||
)
|
|
||||||
|
|
||||||
results.append(
|
results.append(
|
||||||
UnusualItem(
|
UnusualItem(
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class SecurityBar:
|
|||||||
close: float
|
close: float
|
||||||
high: float
|
high: float
|
||||||
low: float
|
low: float
|
||||||
vol: float # 成交量(股)
|
vol: float # 成交量(股)
|
||||||
amount: float # 成交额(元)
|
amount: float # 成交额(元)
|
||||||
|
|
||||||
year: int
|
year: int
|
||||||
|
|||||||
@@ -18,27 +18,27 @@ class XdxrRecord:
|
|||||||
year: int
|
year: int
|
||||||
month: int
|
month: int
|
||||||
day: int
|
day: int
|
||||||
category: int # 事件类型(见下方 CATEGORY_NAMES)
|
category: int # 事件类型(见下方 CATEGORY_NAMES)
|
||||||
name: str # 事件类型名称
|
name: str # 事件类型名称
|
||||||
|
|
||||||
# category == 1(除权除息)
|
# category == 1(除权除息)
|
||||||
fenhong: float | None = None # 每股分红(元;协议原值按每10股)
|
fenhong: float | None = None # 每股分红(元;协议原值按每10股)
|
||||||
peigujia: float | None = None # 配股价(元/股)
|
peigujia: float | None = None # 配股价(元/股)
|
||||||
songzhuangu: float | None = None # 每股送转股比例(协议原值按每10股)
|
songzhuangu: float | None = None # 每股送转股比例(协议原值按每10股)
|
||||||
peigu: float | None = None # 每股配股比例(协议原值按每10股)
|
peigu: float | None = None # 每股配股比例(协议原值按每10股)
|
||||||
|
|
||||||
# category in [11, 12](扩缩股)
|
# category in [11, 12](扩缩股)
|
||||||
suogu: float | None = None # 缩股比例
|
suogu: float | None = None # 缩股比例
|
||||||
|
|
||||||
# category in [13, 14](权证)
|
# category in [13, 14](权证)
|
||||||
xingquanjia: float | None = None # 行权价
|
xingquanjia: float | None = None # 行权价
|
||||||
fenshu: float | None = None # 分数
|
fenshu: float | None = None # 分数
|
||||||
|
|
||||||
# category in [2..10](股本变动类,单位:万股)
|
# category in [2..10](股本变动类,单位:万股)
|
||||||
panqian_liutong: float | None = None # 盘前流通股本(万股)
|
panqian_liutong: float | None = None # 盘前流通股本(万股)
|
||||||
panhou_liutong: float | None = None # 盘后流通股本(万股)
|
panhou_liutong: float | None = None # 盘后流通股本(万股)
|
||||||
qian_zongguben: float | None = None # 前总股本(万股)
|
qian_zongguben: float | None = None # 前总股本(万股)
|
||||||
hou_zongguben: float | None = None # 后总股本(万股)
|
hou_zongguben: float | None = None # 后总股本(万股)
|
||||||
|
|
||||||
_raw: bytes = field(default=b"", repr=False, compare=False)
|
_raw: bytes = field(default=b"", repr=False, compare=False)
|
||||||
|
|
||||||
@@ -69,45 +69,45 @@ class FinanceInfo:
|
|||||||
code: str
|
code: str
|
||||||
|
|
||||||
# 股本(万股)
|
# 股本(万股)
|
||||||
liutong_guben: float # 流通股本
|
liutong_guben: float # 流通股本
|
||||||
zong_guben: float # 总股本
|
zong_guben: float # 总股本
|
||||||
guojia_gu: float # 国家股
|
guojia_gu: float # 国家股
|
||||||
faqiren_faren_gu: float # 发起人法人股
|
faqiren_faren_gu: float # 发起人法人股
|
||||||
faren_gu: float # 法人股
|
faren_gu: float # 法人股
|
||||||
b_gu: float # B股
|
b_gu: float # B股
|
||||||
h_gu: float # H股
|
h_gu: float # H股
|
||||||
zhigong_gu: float # 职工股
|
zhigong_gu: float # 职工股
|
||||||
|
|
||||||
# 基本信息
|
# 基本信息
|
||||||
province: int # 所属省份代码
|
province: int # 所属省份代码
|
||||||
industry: int # 所属行业代码
|
industry: int # 所属行业代码
|
||||||
updated_date: int # 财务更新日期 YYYYMMDD
|
updated_date: int # 财务更新日期 YYYYMMDD
|
||||||
ipo_date: int # 上市日期 YYYYMMDD
|
ipo_date: int # 上市日期 YYYYMMDD
|
||||||
gudong_renshu: float # 股东人数
|
gudong_renshu: float # 股东人数
|
||||||
|
|
||||||
# 资产负债(元)
|
# 资产负债(元)
|
||||||
zong_zichan: float # 总资产
|
zong_zichan: float # 总资产
|
||||||
liudong_zichan: float # 流动资产
|
liudong_zichan: float # 流动资产
|
||||||
guding_zichan: float # 固定资产
|
guding_zichan: float # 固定资产
|
||||||
wuxing_zichan: float # 无形资产
|
wuxing_zichan: float # 无形资产
|
||||||
liudong_fuzhai: float # 流动负债
|
liudong_fuzhai: float # 流动负债
|
||||||
changqi_fuzhai: float # 长期负债
|
changqi_fuzhai: float # 长期负债
|
||||||
ziben_gongjijin: float # 资本公积金
|
ziben_gongjijin: float # 资本公积金
|
||||||
jing_zichan: float # 净资产
|
jing_zichan: float # 净资产
|
||||||
|
|
||||||
# 利润(元)
|
# 利润(元)
|
||||||
zhuying_shouru: float # 主营收入
|
zhuying_shouru: float # 主营收入
|
||||||
zhuying_lirun: float # 主营利润
|
zhuying_lirun: float # 主营利润
|
||||||
yingshou_zhangkuan: float # 应收账款
|
yingshou_zhangkuan: float # 应收账款
|
||||||
yingye_lirun: float # 营业利润
|
yingye_lirun: float # 营业利润
|
||||||
touzi_shouyu: float # 投资收益
|
touzi_shouyu: float # 投资收益
|
||||||
jingying_xianjinliu: float # 经营现金流
|
jingying_xianjinliu: float # 经营现金流
|
||||||
zong_xianjinliu: float # 总现金流
|
zong_xianjinliu: float # 总现金流
|
||||||
cunhuo: float # 存货
|
cunhuo: float # 存货
|
||||||
lirun_zonghe: float # 利润总额
|
lirun_zonghe: float # 利润总额
|
||||||
shuihou_lirun: float # 税后利润
|
shuihou_lirun: float # 税后利润
|
||||||
jing_lirun: float # 净利润
|
jing_lirun: float # 净利润
|
||||||
weifen_lirun: float # 未分配利润
|
weifen_lirun: float # 未分配利润
|
||||||
|
|
||||||
# 每股指标
|
# 每股指标
|
||||||
meigujing_zichan: float # 每股净资产(原 baoliu1)
|
meigujing_zichan: float # 每股净资产(原 baoliu1)
|
||||||
@@ -122,36 +122,36 @@ class FinanceInfo:
|
|||||||
class CompanyInfoCategory:
|
class CompanyInfoCategory:
|
||||||
"""公司信息文件目录条目"""
|
"""公司信息文件目录条目"""
|
||||||
|
|
||||||
name: str = "" # 目录名(如“最新提示”)
|
name: str = "" # 目录名(如“最新提示”)
|
||||||
filename: str = "" # 文件名(如 '600000.txt')
|
filename: str = "" # 文件名(如 '600000.txt')
|
||||||
start: int = 0 # 内容起始偏移
|
start: int = 0 # 内容起始偏移
|
||||||
length: int = 0 # 内容长度(字节)
|
length: int = 0 # 内容长度(字节)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FinancialFileInfo:
|
class FinancialFileInfo:
|
||||||
"""财报 zip 文件索引条目(来自 tdxfin/gpcw.txt)。"""
|
"""财报 zip 文件索引条目(来自 tdxfin/gpcw.txt)。"""
|
||||||
|
|
||||||
filename: str # "gpcw20260331.zip"
|
filename: str # "gpcw20260331.zip"
|
||||||
hash: str # MD5 hex digest
|
hash: str # MD5 hex digest
|
||||||
filesize: int # 字节
|
filesize: int # 字节
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FinancialRecord:
|
class FinancialRecord:
|
||||||
"""单只股票的一期历史专业财报记录。"""
|
"""单只股票的一期历史专业财报记录。"""
|
||||||
|
|
||||||
code: str # 6 位股票代码
|
code: str # 6 位股票代码
|
||||||
market: Market # 市场
|
market: Market # 市场
|
||||||
report_date: int # 报告期 YYYYMMDD
|
report_date: int # 报告期 YYYYMMDD
|
||||||
fields: list[float] # N 个浮点字段(N = report_size / 4)
|
fields: list[float] # N 个浮点字段(N = report_size / 4)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TdxBlock:
|
class TdxBlock:
|
||||||
"""通达信板块信息(行业、概念、风格等)"""
|
"""通达信板块信息(行业、概念、风格等)"""
|
||||||
|
|
||||||
name: str # 板块名称(如“房地产”)
|
name: str # 板块名称(如“房地产”)
|
||||||
category: int # 板块分类(0=行业, 1=地域, 2=概念, 3=风格, 等)
|
category: int # 板块分类(0=行业, 1=地域, 2=概念, 3=风格, 等)
|
||||||
count: int # 板块包含股票数量
|
count: int # 板块包含股票数量
|
||||||
codes: list[str] # 股票代码列表(6位数字代码)
|
codes: list[str] # 股票代码列表(6位数字代码)
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ class SecurityInfo:
|
|||||||
|
|
||||||
market: Market
|
market: Market
|
||||||
code: str
|
code: str
|
||||||
name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代)
|
name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代)
|
||||||
volunit: int # 成交量单位(手 = volunit 股)
|
volunit: int # 成交量单位(手 = volunit 股)
|
||||||
decimal_point: int # 价格小数位数
|
decimal_point: int # 价格小数位数
|
||||||
pre_close: float # 昨收价(通达信自定义浮点解码)
|
pre_close: float # 昨收价(通达信自定义浮点解码)
|
||||||
|
|
||||||
# 扩展字段(通过 get_security_list_all 关联 tdxhy.cfg 获得)
|
# 扩展字段(通过 get_security_list_all 关联 tdxhy.cfg 获得)
|
||||||
industry_tdx: str = "" # 通达信行业代码 (如 T1001)
|
industry_tdx: str = "" # 通达信行业代码 (如 T1001)
|
||||||
industry_sw: str = "" # 申万行业代码 (如 X500102)
|
industry_sw: str = "" # 申万行业代码 (如 X500102)
|
||||||
|
|
||||||
_raw: bytes = field(default=b"", repr=False, compare=False)
|
_raw: bytes = field(default=b"", repr=False, compare=False)
|
||||||
|
|||||||
@@ -1,58 +1,61 @@
|
|||||||
"""验证市场概况模型。"""
|
"""验证市场概况模型。"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MarketStat:
|
class MarketStat:
|
||||||
"""全市场涨跌统计概况。"""
|
"""全市场涨跌统计概况。"""
|
||||||
up_count: int # 上涨家数
|
|
||||||
down_count: int # 下跌家数
|
up_count: int # 上涨家数
|
||||||
neutral_count: int # 平盘家数
|
down_count: int # 下跌家数
|
||||||
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
|
neutral_count: int # 平盘家数
|
||||||
total_count: int # 总计(包含停牌)
|
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
|
||||||
total_amount: float # 总成交额
|
total_count: int # 总计(包含停牌)
|
||||||
total_volume: float # 总成交量
|
total_amount: float # 总成交额
|
||||||
total_market_cap: float # 总市值(亿元),来自 880001 收盘价,÷100 得万亿
|
total_volume: float # 总成交量
|
||||||
limit_up_count: int # 涨停家数,来自 880006 close
|
total_market_cap: float # 总市值(亿元),来自 880001 收盘价,÷100 得万亿
|
||||||
limit_down_count: int # 跌停家数,来自 880006 open
|
limit_up_count: int # 涨停家数,来自 880006 close
|
||||||
|
limit_down_count: int # 跌停家数,来自 880006 open
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FundFlow:
|
class FundFlow:
|
||||||
"""个股资金流向统计(基于 Tick 数据加权计算)。"""
|
"""个股资金流向统计(基于 Tick 数据加权计算)。"""
|
||||||
|
|
||||||
# 流入项 (Buy)
|
# 流入项 (Buy)
|
||||||
super_in: float # 超大单流入 (>100万)
|
super_in: float # 超大单流入 (>100万)
|
||||||
large_in: float # 大单流入 (>20万 且 <=100万)
|
large_in: float # 大单流入 (>20万 且 <=100万)
|
||||||
medium_in: float # 中单流入 (>4万 且 <=20万)
|
medium_in: float # 中单流入 (>4万 且 <=20万)
|
||||||
small_in: float # 小单流入 (<=4万)
|
small_in: float # 小单流入 (<=4万)
|
||||||
|
|
||||||
# 流出项 (Sell)
|
# 流出项 (Sell)
|
||||||
super_out: float
|
super_out: float
|
||||||
large_out: float
|
large_out: float
|
||||||
medium_out: float
|
medium_out: float
|
||||||
small_out: float
|
small_out: float
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def main_net_inflow(self) -> float:
|
def main_net_inflow(self) -> float:
|
||||||
"""主力净流入 (超大单 + 大单)。"""
|
"""主力净流入 (超大单 + 大单)。"""
|
||||||
return (self.super_in + self.large_in) - (self.super_out + self.large_out)
|
return (self.super_in + self.large_in) - (self.super_out + self.large_out)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total_net_inflow(self) -> float:
|
def total_net_inflow(self) -> float:
|
||||||
"""全单净流入。"""
|
"""全单净流入。"""
|
||||||
return (self.super_in + self.large_in + self.medium_in + self.small_in) - \
|
return (self.super_in + self.large_in + self.medium_in + self.small_in) - (
|
||||||
(self.super_out + self.large_out + self.medium_out + self.small_out)
|
self.super_out + self.large_out + self.medium_out + self.small_out
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HistoricalFundFlow:
|
class HistoricalFundFlow:
|
||||||
"""历史日线资金流向条目。"""
|
"""历史日线资金流向条目。"""
|
||||||
|
|
||||||
year: int
|
year: int
|
||||||
month: int
|
month: int
|
||||||
day: int
|
day: int
|
||||||
|
|
||||||
# 金额项 (单位:元)
|
# 金额项 (单位:元)
|
||||||
super_in: float
|
super_in: float
|
||||||
super_out: float
|
super_out: float
|
||||||
@@ -62,7 +65,7 @@ class HistoricalFundFlow:
|
|||||||
medium_out: float
|
medium_out: float
|
||||||
small_in: float
|
small_in: float
|
||||||
small_out: float
|
small_out: float
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def main_net_inflow(self) -> float:
|
def main_net_inflow(self) -> float:
|
||||||
"""当日主力净流入。"""
|
"""当日主力净流入。"""
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ def get_last_bar_date(filepath: str | Path) -> int | None:
|
|||||||
f.seek(size - _DAILY_FMT.size)
|
f.seek(size - _DAILY_FMT.size)
|
||||||
last_record = f.read(_DAILY_FMT.size)
|
last_record = f.read(_DAILY_FMT.size)
|
||||||
(date_int, *_) = _DAILY_FMT.unpack(last_record)
|
(date_int, *_) = _DAILY_FMT.unpack(last_record)
|
||||||
return date_int
|
return int(date_int)
|
||||||
|
|
||||||
|
|
||||||
def _bar_date_int(bar: SecurityBar) -> int:
|
def _bar_date_int(bar: SecurityBar) -> int:
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ def get_last_ex_bar_date(filepath: str | Path) -> int | None:
|
|||||||
f.seek(size - _EX_DAILY_FMT.size)
|
f.seek(size - _EX_DAILY_FMT.size)
|
||||||
last_record = f.read(_EX_DAILY_FMT.size)
|
last_record = f.read(_EX_DAILY_FMT.size)
|
||||||
(date_int, *_) = _EX_DAILY_FMT.unpack(last_record)
|
(date_int, *_) = _EX_DAILY_FMT.unpack(last_record)
|
||||||
return date_int
|
return int(date_int)
|
||||||
|
|
||||||
|
|
||||||
def _bar_date_int(bar: ExDailyBar) -> int:
|
def _bar_date_int(bar: ExDailyBar) -> int:
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ from typing import TYPE_CHECKING, TypeVar
|
|||||||
|
|
||||||
from ..codec.frame import HEADER_SIZE, decompress_body, parse_header
|
from ..codec.frame import HEADER_SIZE, decompress_body, parse_header
|
||||||
from ..commands.setup import SETUP_COMMANDS
|
from ..commands.setup import SETUP_COMMANDS
|
||||||
from ..config import get_best_host, get_calc_hosts, get_known_hosts, get_mac_hosts, get_port, get_timeout
|
from ..config import (
|
||||||
|
get_best_host,
|
||||||
|
get_calc_hosts,
|
||||||
|
get_known_hosts,
|
||||||
|
get_mac_hosts,
|
||||||
|
get_port,
|
||||||
|
get_timeout,
|
||||||
|
)
|
||||||
from ..exceptions import TdxConnectionError
|
from ..exceptions import TdxConnectionError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
+20
-8
@@ -95,7 +95,7 @@ class UnifiedTdxClient:
|
|||||||
def get_stock_quotes(
|
def get_stock_quotes(
|
||||||
self,
|
self,
|
||||||
stocks: list[tuple[int, str]],
|
stocks: list[tuple[int, str]],
|
||||||
fields: object = None,
|
fields: Any = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
return self._ensure_mac().get_stock_quotes(stocks, fields)
|
return self._ensure_mac().get_stock_quotes(stocks, fields)
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ class UnifiedTdxClient:
|
|||||||
sort_type: SortType = SortType.CHANGE_PCT,
|
sort_type: SortType = SortType.CHANGE_PCT,
|
||||||
sort_order: SortOrder = SortOrder.DESC,
|
sort_order: SortOrder = SortOrder.DESC,
|
||||||
exclude_flags: list[FilterType] | None = None,
|
exclude_flags: list[FilterType] | None = None,
|
||||||
fields: object = None,
|
fields: Any = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
return self._ensure_mac().get_stock_quotes_list(
|
return self._ensure_mac().get_stock_quotes_list(
|
||||||
category, start, count, sort_type, sort_order, exclude_flags, fields
|
category, start, count, sort_type, sort_order, exclude_flags, fields
|
||||||
@@ -136,7 +136,13 @@ class UnifiedTdxClient:
|
|||||||
params: dict[str, dict[str, int | float]] | None = None,
|
params: dict[str, dict[str, int | float]] | None = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
return self._ensure_mac().get_stock_kline_with_indicators(
|
return self._ensure_mac().get_stock_kline_with_indicators(
|
||||||
market, code, indicators, period, count, adjust, params,
|
market,
|
||||||
|
code,
|
||||||
|
indicators,
|
||||||
|
period,
|
||||||
|
count,
|
||||||
|
adjust,
|
||||||
|
params,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tick_chart(
|
def get_tick_chart(
|
||||||
@@ -185,7 +191,7 @@ class UnifiedTdxClient:
|
|||||||
count: int = 100000,
|
count: int = 100000,
|
||||||
sort_type: SortType = SortType.CHANGE_PCT,
|
sort_type: SortType = SortType.CHANGE_PCT,
|
||||||
sort_order: SortOrder = SortOrder.DESC,
|
sort_order: SortOrder = SortOrder.DESC,
|
||||||
fields: object = None,
|
fields: Any = None,
|
||||||
exclude_flags: list[FilterType] | None = None,
|
exclude_flags: list[FilterType] | None = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
return self._ensure_mac().get_board_members(
|
return self._ensure_mac().get_board_members(
|
||||||
@@ -378,7 +384,7 @@ class AsyncUnifiedTdxClient:
|
|||||||
async def get_stock_quotes(
|
async def get_stock_quotes(
|
||||||
self,
|
self,
|
||||||
stocks: list[tuple[int, str]],
|
stocks: list[tuple[int, str]],
|
||||||
fields: object = None,
|
fields: Any = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
mac = await self._ensure_mac()
|
mac = await self._ensure_mac()
|
||||||
return await mac.get_stock_quotes(stocks, fields)
|
return await mac.get_stock_quotes(stocks, fields)
|
||||||
@@ -391,7 +397,7 @@ class AsyncUnifiedTdxClient:
|
|||||||
sort_type: SortType = SortType.CHANGE_PCT,
|
sort_type: SortType = SortType.CHANGE_PCT,
|
||||||
sort_order: SortOrder = SortOrder.DESC,
|
sort_order: SortOrder = SortOrder.DESC,
|
||||||
exclude_flags: list[FilterType] | None = None,
|
exclude_flags: list[FilterType] | None = None,
|
||||||
fields: object = None,
|
fields: Any = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
mac = await self._ensure_mac()
|
mac = await self._ensure_mac()
|
||||||
return await mac.get_stock_quotes_list(
|
return await mac.get_stock_quotes_list(
|
||||||
@@ -423,7 +429,13 @@ class AsyncUnifiedTdxClient:
|
|||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
mac = await self._ensure_mac()
|
mac = await self._ensure_mac()
|
||||||
return await mac.get_stock_kline_with_indicators(
|
return await mac.get_stock_kline_with_indicators(
|
||||||
market, code, indicators, period, count, adjust, params,
|
market,
|
||||||
|
code,
|
||||||
|
indicators,
|
||||||
|
period,
|
||||||
|
count,
|
||||||
|
adjust,
|
||||||
|
params,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_tick_chart(
|
async def get_tick_chart(
|
||||||
@@ -478,7 +490,7 @@ class AsyncUnifiedTdxClient:
|
|||||||
count: int = 100000,
|
count: int = 100000,
|
||||||
sort_type: SortType = SortType.CHANGE_PCT,
|
sort_type: SortType = SortType.CHANGE_PCT,
|
||||||
sort_order: SortOrder = SortOrder.DESC,
|
sort_order: SortOrder = SortOrder.DESC,
|
||||||
fields: object = None,
|
fields: Any = None,
|
||||||
exclude_flags: list[FilterType] | None = None,
|
exclude_flags: list[FilterType] | None = None,
|
||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
mac = await self._ensure_mac()
|
mac = await self._ensure_mac()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from easy_tdx import MyTT
|
from easy_tdx import MyTT
|
||||||
from easy_tdx.backtest.strategy import Strategy
|
from easy_tdx.backtest.strategy import Strategy
|
||||||
|
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ def test_position_aware_buy_sell_alternation():
|
|||||||
engine = BacktestEngine(PositionAwareStrategy, cash=100000)
|
engine = BacktestEngine(PositionAwareStrategy, cash=100000)
|
||||||
result = engine.run(df)
|
result = engine.run(df)
|
||||||
|
|
||||||
trades = result.trades[result.trades["rejected"] == False]
|
trades = result.trades[~result.trades["rejected"]]
|
||||||
directions = trades["direction"].tolist()
|
directions = trades["direction"].tolist()
|
||||||
|
|
||||||
# Must have both BUYs and SELLs
|
# Must have both BUYs and SELLs
|
||||||
@@ -339,9 +339,7 @@ def test_position_aware_no_duplicate_buys():
|
|||||||
engine = BacktestEngine(PositionAwareStrategy, cash=100000)
|
engine = BacktestEngine(PositionAwareStrategy, cash=100000)
|
||||||
result = engine.run(df)
|
result = engine.run(df)
|
||||||
|
|
||||||
buy_trades = result.trades[
|
buy_trades = result.trades[(result.trades["direction"] == "BUY") & (~result.trades["rejected"])]
|
||||||
(result.trades["direction"] == "BUY") & (result.trades["rejected"] == False)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Each BUY's size should be reasonable (not tiny leftover from exhausted cash)
|
# Each BUY's size should be reasonable (not tiny leftover from exhausted cash)
|
||||||
if len(buy_trades) > 1:
|
if len(buy_trades) > 1:
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ def _make_equity_curve(n: int = 252, total_return: float = 0.1) -> pd.DataFrame:
|
|||||||
drawdown = peak - total
|
drawdown = peak - total
|
||||||
drawdown_pct = np.divide(drawdown, peak, out=np.zeros_like(drawdown), where=(peak != 0))
|
drawdown_pct = np.divide(drawdown, peak, out=np.zeros_like(drawdown), where=(peak != 0))
|
||||||
|
|
||||||
return pd.DataFrame({
|
return pd.DataFrame(
|
||||||
"datetime": np.arange(n),
|
{
|
||||||
"total": total,
|
"datetime": np.arange(n),
|
||||||
"drawdown": drawdown,
|
"total": total,
|
||||||
"drawdown_pct": drawdown_pct,
|
"drawdown": drawdown,
|
||||||
})
|
"drawdown_pct": drawdown_pct,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_trades() -> pd.DataFrame:
|
def _make_trades() -> pd.DataFrame:
|
||||||
@@ -48,11 +50,13 @@ def _make_trades() -> pd.DataFrame:
|
|||||||
包含 direction, pnl, rejected 的 DataFrame
|
包含 direction, pnl, rejected 的 DataFrame
|
||||||
4 条交易: BUY@100, SELL@105(pnl=500), BUY@95, SELL@90(pnl=-500)
|
4 条交易: BUY@100, SELL@105(pnl=500), BUY@95, SELL@90(pnl=-500)
|
||||||
"""
|
"""
|
||||||
return pd.DataFrame({
|
return pd.DataFrame(
|
||||||
"direction": ["BUY", "SELL", "BUY", "SELL"],
|
{
|
||||||
"pnl": [0, 500, 0, -500],
|
"direction": ["BUY", "SELL", "BUY", "SELL"],
|
||||||
"rejected": [False, False, False, False],
|
"pnl": [0, 500, 0, -500],
|
||||||
})
|
"rejected": [False, False, False, False],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_total_return() -> None:
|
def test_total_return() -> None:
|
||||||
@@ -70,20 +74,24 @@ def test_total_return() -> None:
|
|||||||
def test_max_drawdown_never_exceeds_100_pct() -> None:
|
def test_max_drawdown_never_exceeds_100_pct() -> None:
|
||||||
"""测试最大回撤永远不超过 100%(从峰值的跌幅)。"""
|
"""测试最大回撤永远不超过 100%(从峰值的跌幅)。"""
|
||||||
# 模拟先涨 5 倍再腰斩的资金曲线
|
# 模拟先涨 5 倍再腰斩的资金曲线
|
||||||
total = np.concatenate([
|
total = np.concatenate(
|
||||||
np.linspace(100000, 600000, 126), # 涨到 60 万
|
[
|
||||||
np.linspace(600000, 300000, 126), # 跌到 30 万
|
np.linspace(100000, 600000, 126), # 涨到 60 万
|
||||||
])
|
np.linspace(600000, 300000, 126), # 跌到 30 万
|
||||||
|
]
|
||||||
|
)
|
||||||
peak = np.maximum.accumulate(total)
|
peak = np.maximum.accumulate(total)
|
||||||
drawdown = peak - total
|
drawdown = peak - total
|
||||||
drawdown_pct = np.divide(drawdown, peak, out=np.zeros_like(drawdown), where=(peak != 0))
|
drawdown_pct = np.divide(drawdown, peak, out=np.zeros_like(drawdown), where=(peak != 0))
|
||||||
|
|
||||||
equity = pd.DataFrame({
|
equity = pd.DataFrame(
|
||||||
"datetime": np.arange(252),
|
{
|
||||||
"total": total,
|
"datetime": np.arange(252),
|
||||||
"drawdown": drawdown,
|
"total": total,
|
||||||
"drawdown_pct": drawdown_pct,
|
"drawdown": drawdown,
|
||||||
})
|
"drawdown_pct": drawdown_pct,
|
||||||
|
}
|
||||||
|
)
|
||||||
trades = _make_trades()
|
trades = _make_trades()
|
||||||
|
|
||||||
analyzer = PerformanceAnalyzer(equity, trades)
|
analyzer = PerformanceAnalyzer(equity, trades)
|
||||||
@@ -287,11 +295,13 @@ def test_rejected_trades() -> None:
|
|||||||
equity = _make_equity_curve(n=252, total_return=0.1)
|
equity = _make_equity_curve(n=252, total_return=0.1)
|
||||||
|
|
||||||
# 创建包含被拒绝交易的记录
|
# 创建包含被拒绝交易的记录
|
||||||
trades = pd.DataFrame({
|
trades = pd.DataFrame(
|
||||||
"direction": ["BUY", "SELL", "SELL", "SELL"],
|
{
|
||||||
"pnl": [0, 500, 0, -500],
|
"direction": ["BUY", "SELL", "SELL", "SELL"],
|
||||||
"rejected": [False, False, True, False],
|
"pnl": [0, 500, 0, -500],
|
||||||
})
|
"rejected": [False, False, True, False],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
analyzer = PerformanceAnalyzer(equity, trades)
|
analyzer = PerformanceAnalyzer(equity, trades)
|
||||||
metrics = analyzer.compute()
|
metrics = analyzer.compute()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ def _k(
|
|||||||
o: float,
|
o: float,
|
||||||
c: float,
|
c: float,
|
||||||
h: float,
|
h: float,
|
||||||
l: float,
|
l: float, # noqa: E741
|
||||||
a: float = 0.0,
|
a: float = 0.0,
|
||||||
) -> Kline:
|
) -> Kline:
|
||||||
"""快速构造 Kline。"""
|
"""快速构造 Kline。"""
|
||||||
@@ -40,7 +40,7 @@ def _ck(
|
|||||||
o: float,
|
o: float,
|
||||||
c: float,
|
c: float,
|
||||||
h: float,
|
h: float,
|
||||||
l: float,
|
l: float, # noqa: E741
|
||||||
merged_count: int = 1,
|
merged_count: int = 1,
|
||||||
direction: str = "",
|
direction: str = "",
|
||||||
) -> CLKline:
|
) -> CLKline:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def _k(
|
|||||||
o: float,
|
o: float,
|
||||||
c: float,
|
c: float,
|
||||||
h: float,
|
h: float,
|
||||||
l: float,
|
l: float, # noqa: E741
|
||||||
a: float = 0.0,
|
a: float = 0.0,
|
||||||
) -> Kline:
|
) -> Kline:
|
||||||
return Kline(
|
return Kline(
|
||||||
@@ -37,7 +37,7 @@ def _ck(
|
|||||||
o: float,
|
o: float,
|
||||||
c: float,
|
c: float,
|
||||||
h: float,
|
h: float,
|
||||||
l: float,
|
l: float, # noqa: E741
|
||||||
merged_count: int = 1,
|
merged_count: int = 1,
|
||||||
direction: str = "",
|
direction: str = "",
|
||||||
) -> CLKline:
|
) -> CLKline:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from easy_tdx.chanlun.types import CLKline, Kline
|
|||||||
# ── helpers ──────────────────────────────────────────────────────────────
|
# ── helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _k(idx: int, dt: str, o: float, c: float, h: float, l: float, a: float = 0.0) -> Kline:
|
def _k(idx: int, dt: str, o: float, c: float, h: float, l: float, a: float = 0.0) -> Kline: # noqa: E741
|
||||||
return Kline(
|
return Kline(
|
||||||
index=idx,
|
index=idx,
|
||||||
date=datetime.strptime(dt, "%Y-%m-%d"),
|
date=datetime.strptime(dt, "%Y-%m-%d"),
|
||||||
@@ -55,9 +55,8 @@ class TestMultiLevel:
|
|||||||
|
|
||||||
def test_multi_level_basic(self) -> None:
|
def test_multi_level_basic(self) -> None:
|
||||||
"""多级别分析应返回各级别结果。"""
|
"""多级别分析应返回各级别结果。"""
|
||||||
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
|
|
||||||
|
|
||||||
from easy_tdx.chanlun.analyser import ChanlunAnalyser
|
from easy_tdx.chanlun.analyser import ChanlunAnalyser
|
||||||
|
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
|
||||||
|
|
||||||
df_daily = _make_df(100)
|
df_daily = _make_df(100)
|
||||||
df_30min = _make_df(200)
|
df_30min = _make_df(200)
|
||||||
@@ -77,9 +76,8 @@ class TestMultiLevel:
|
|||||||
|
|
||||||
def test_multi_level_low_level_qs(self) -> None:
|
def test_multi_level_low_level_qs(self) -> None:
|
||||||
"""高级别笔对应的低级别趋势信息。"""
|
"""高级别笔对应的低级别趋势信息。"""
|
||||||
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
|
|
||||||
|
|
||||||
from easy_tdx.chanlun.analyser import ChanlunAnalyser
|
from easy_tdx.chanlun.analyser import ChanlunAnalyser
|
||||||
|
from easy_tdx.chanlun.multi_level import MultiLevelAnalyser
|
||||||
|
|
||||||
df_daily = _make_df(100)
|
df_daily = _make_df(100)
|
||||||
df_30min = _make_df(200)
|
df_30min = _make_df(200)
|
||||||
@@ -158,9 +156,8 @@ class TestZsd:
|
|||||||
|
|
||||||
def test_zsd_from_xds(self) -> None:
|
def test_zsd_from_xds(self) -> None:
|
||||||
"""线段应能组合为走势段。"""
|
"""线段应能组合为走势段。"""
|
||||||
from easy_tdx.chanlun.zsd import find_zsds
|
|
||||||
|
|
||||||
from easy_tdx.chanlun.xd import find_xds
|
from easy_tdx.chanlun.xd import find_xds
|
||||||
|
from easy_tdx.chanlun.zsd import find_zsds
|
||||||
|
|
||||||
cks = [
|
cks = [
|
||||||
CLKline(
|
CLKline(
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ class TestParseFinancialDat:
|
|||||||
index_entries: list[bytes] = []
|
index_entries: list[bytes] = []
|
||||||
for i, (code, market_byte, _) in enumerate(stocks):
|
for i, (code, market_byte, _) in enumerate(stocks):
|
||||||
index_entries.append(
|
index_entries.append(
|
||||||
struct.pack(
|
struct.pack(index_fmt, code.encode("ascii"), bytes([market_byte]), offsets[i])
|
||||||
index_fmt, code.encode("ascii"), bytes([market_byte]), offsets[i]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return header + b"".join(index_entries) + b"".join(data_chunks)
|
return header + b"".join(index_entries) + b"".join(data_chunks)
|
||||||
|
|||||||
@@ -13,28 +13,29 @@ def test_heartbeat_sends_periodically():
|
|||||||
mock_conn = mock_conn_cls.return_value
|
mock_conn = mock_conn_cls.return_value
|
||||||
mock_conn.connect = AsyncMock()
|
mock_conn.connect = AsyncMock()
|
||||||
mock_conn.close = AsyncMock()
|
mock_conn.close = AsyncMock()
|
||||||
|
|
||||||
# 记录调用次数
|
# 记录调用次数
|
||||||
call_count = 0
|
call_count = 0
|
||||||
|
|
||||||
async def mock_execute(cmd):
|
async def mock_execute(cmd):
|
||||||
nonlocal call_count
|
nonlocal call_count
|
||||||
call_count += 1
|
call_count += 1
|
||||||
return 5 # 模拟 get_security_count 返回值
|
return 5 # 模拟 get_security_count 返回值
|
||||||
|
|
||||||
mock_conn.execute.side_effect = mock_execute
|
mock_conn.execute.side_effect = mock_execute
|
||||||
|
|
||||||
# 设置非常短的心跳间隔以便测试
|
# 设置非常短的心跳间隔以便测试
|
||||||
client = AsyncTdxClient("127.0.0.1", heartbeat_interval=0.1)
|
client = AsyncTdxClient("127.0.0.1", heartbeat_interval=0.1)
|
||||||
await client.connect()
|
await client.connect()
|
||||||
|
|
||||||
# 等待几次心跳周期
|
# 等待几次心跳周期
|
||||||
await asyncio.sleep(0.35)
|
await asyncio.sleep(0.35)
|
||||||
|
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
# 0.35s 应该触发约 3 次心跳 (0.1, 0.2, 0.3)
|
# 0.35s 应该触发约 3 次心跳 (0.1, 0.2, 0.3)
|
||||||
assert call_count >= 3
|
assert call_count >= 3
|
||||||
|
|
||||||
asyncio.run(run_test())
|
asyncio.run(run_test())
|
||||||
|
|
||||||
|
|
||||||
@@ -45,14 +46,14 @@ def test_heartbeat_stops_on_close():
|
|||||||
mock_conn.connect = AsyncMock()
|
mock_conn.connect = AsyncMock()
|
||||||
mock_conn.close = AsyncMock()
|
mock_conn.close = AsyncMock()
|
||||||
mock_conn.execute = AsyncMock(return_value=5)
|
mock_conn.execute = AsyncMock(return_value=5)
|
||||||
|
|
||||||
client = AsyncTdxClient("127.0.0.1", heartbeat_interval=0.01)
|
client = AsyncTdxClient("127.0.0.1", heartbeat_interval=0.01)
|
||||||
await client.connect()
|
await client.connect()
|
||||||
assert client._heartbeat_task is not None
|
assert client._heartbeat_task is not None
|
||||||
|
|
||||||
task = client._heartbeat_task
|
task = client._heartbeat_task
|
||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
assert client._heartbeat_task is None
|
assert client._heartbeat_task is None
|
||||||
assert task.done() or task.cancelled()
|
assert task.done() or task.cancelled()
|
||||||
|
|
||||||
@@ -65,5 +66,5 @@ if __name__ == "__main__":
|
|||||||
await test_heartbeat_sends_periodically()
|
await test_heartbeat_sends_periodically()
|
||||||
await test_heartbeat_stops_on_close()
|
await test_heartbeat_stops_on_close()
|
||||||
print("Heartbeat tests passed!")
|
print("Heartbeat tests passed!")
|
||||||
|
|
||||||
asyncio.run(run())
|
asyncio.run(run())
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from easy_tdx.indicator import compute_indicators, list_indicators, _REGISTRY
|
from easy_tdx.indicator import _REGISTRY, compute_indicators, list_indicators
|
||||||
|
|
||||||
|
|
||||||
def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame:
|
def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame:
|
||||||
@@ -18,15 +18,17 @@ def _make_ohlcv(n: int = 200, seed: int = 42) -> pd.DataFrame:
|
|||||||
low = close - np.abs(rng.standard_normal(n))
|
low = close - np.abs(rng.standard_normal(n))
|
||||||
open_ = low + (high - low) * rng.random(n)
|
open_ = low + (high - low) * rng.random(n)
|
||||||
vol = (rng.random(n) * 1e6).astype(float)
|
vol = (rng.random(n) * 1e6).astype(float)
|
||||||
return pd.DataFrame({
|
return pd.DataFrame(
|
||||||
"datetime": pd.date_range("2024-01-01", periods=n, freq="D"),
|
{
|
||||||
"open": open_,
|
"datetime": pd.date_range("2024-01-01", periods=n, freq="D"),
|
||||||
"high": high,
|
"open": open_,
|
||||||
"low": low,
|
"high": high,
|
||||||
"close": close,
|
"low": low,
|
||||||
"vol": vol,
|
"close": close,
|
||||||
"amount": vol * close,
|
"vol": vol,
|
||||||
})
|
"amount": vol * close,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestRegistry:
|
class TestRegistry:
|
||||||
|
|||||||
@@ -19,8 +19,20 @@ def test_security_bars_exact_layout():
|
|||||||
# Payload: 0x052D, 1 (Market.SH), "600000", 4 (KlineCategory.DAY), 1, 0 (start), 10, 0, 0, 0
|
# Payload: 0x052D, 1 (Market.SH), "600000", 4 (KlineCategory.DAY), 1, 0 (start), 10, 0, 0, 0
|
||||||
expected = struct.pack(
|
expected = struct.pack(
|
||||||
"<HIHHHH6sHHHHIIH",
|
"<HIHHHH6sHHHHIIH",
|
||||||
0x010C, 0x01016408, 0x001C, 0x001C,
|
0x010C,
|
||||||
0x052D, 1, b"600000", 4, 1, 0, 10, 0, 0, 0
|
0x01016408,
|
||||||
|
0x001C,
|
||||||
|
0x001C,
|
||||||
|
0x052D,
|
||||||
|
1,
|
||||||
|
b"600000",
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
assert req == expected
|
assert req == expected
|
||||||
assert len(req) == 38
|
assert len(req) == 38
|
||||||
@@ -34,8 +46,20 @@ def test_history_fund_flow_exact_layout():
|
|||||||
# Payload: 0x052D, 1 (Market.SH), "600000", 22, 1, 0, 10, 0, 0, 0
|
# Payload: 0x052D, 1 (Market.SH), "600000", 22, 1, 0, 10, 0, 0, 0
|
||||||
expected = struct.pack(
|
expected = struct.pack(
|
||||||
"<HIHHHH6sHHHHIIH",
|
"<HIHHHH6sHHHHIIH",
|
||||||
0x010C, 0x01016408, 0x001C, 0x001C,
|
0x010C,
|
||||||
0x052D, 1, b"600000", 22, 1, 0, 10, 0, 0, 0
|
0x01016408,
|
||||||
|
0x001C,
|
||||||
|
0x001C,
|
||||||
|
0x052D,
|
||||||
|
1,
|
||||||
|
b"600000",
|
||||||
|
22,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
assert req == expected
|
assert req == expected
|
||||||
assert len(req) == 38
|
assert len(req) == 38
|
||||||
@@ -56,16 +80,16 @@ def test_security_quotes_limit_mapping():
|
|||||||
from easy_tdx.codec.price import put_price
|
from easy_tdx.codec.price import put_price
|
||||||
|
|
||||||
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
|
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
|
||||||
|
|
||||||
# 构造响应报文
|
# 构造响应报文
|
||||||
body = bytearray(b"\x00\x00")
|
body = bytearray(b"\x00\x00")
|
||||||
body.extend(struct.pack("<H", 1))
|
body.extend(struct.pack("<H", 1))
|
||||||
|
|
||||||
# Record: Market(B), Code(6s), Active1(H) + ...
|
# Record: Market(B), Code(6s), Active1(H) + ...
|
||||||
body.extend(struct.pack("<B6sH", 1, b"600000", 0))
|
body.extend(struct.pack("<B6sH", 1, b"600000", 0))
|
||||||
|
|
||||||
body.extend(put_price(1010)) # price_raw
|
body.extend(put_price(1010)) # price_raw
|
||||||
body.extend(put_price(-5)) # last_close_diff
|
body.extend(put_price(-5)) # last_close_diff
|
||||||
body.extend(put_price(0))
|
body.extend(put_price(0))
|
||||||
body.extend(put_price(0))
|
body.extend(put_price(0))
|
||||||
body.extend(put_price(0))
|
body.extend(put_price(0))
|
||||||
@@ -86,7 +110,7 @@ def test_security_quotes_limit_mapping():
|
|||||||
body.extend(put_price(0))
|
body.extend(put_price(0))
|
||||||
body.extend(put_price(0))
|
body.extend(put_price(0))
|
||||||
body.extend(struct.pack("<hH", 0, 0))
|
body.extend(struct.pack("<hH", 0, 0))
|
||||||
|
|
||||||
quotes = cmd.parse_response(bytes(body))
|
quotes = cmd.parse_response(bytes(body))
|
||||||
q = quotes[0]
|
q = quotes[0]
|
||||||
assert q.limit_up is None
|
assert q.limit_up is None
|
||||||
@@ -120,18 +144,19 @@ def test_compute_price_limits_for_indices():
|
|||||||
|
|
||||||
def test_compute_price_limits_for_newly_listed_stocks():
|
def test_compute_price_limits_for_newly_listed_stocks():
|
||||||
"""上市初期限价窗口应返回 None。"""
|
"""上市初期限价窗口应返回 None。"""
|
||||||
assert compute_price_limits(
|
assert compute_price_limits(Market.SH, "600001", "主板新股", 10.0, listed_days=5) == (
|
||||||
Market.SH, "600001", "主板新股", 10.0, listed_days=5
|
None,
|
||||||
) == (None, None)
|
None,
|
||||||
assert compute_price_limits(
|
)
|
||||||
Market.SH, "600001", "主板新股", 10.0, listed_days=6
|
assert compute_price_limits(Market.SH, "600001", "主板新股", 10.0, listed_days=6) == (11.0, 9.0)
|
||||||
) == (11.0, 9.0)
|
assert compute_price_limits(Market.BJ, "920002", "北交所新股", 84.36, listed_days=1) == (
|
||||||
assert compute_price_limits(
|
None,
|
||||||
Market.BJ, "920002", "北交所新股", 84.36, listed_days=1
|
None,
|
||||||
) == (None, None)
|
)
|
||||||
assert compute_price_limits(
|
assert compute_price_limits(Market.BJ, "920002", "北交所新股", 84.36, listed_days=2) == (
|
||||||
Market.BJ, "920002", "北交所新股", 84.36, listed_days=2
|
109.67,
|
||||||
) == (109.67, 59.05)
|
59.05,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_history_fund_flow_uses_uint32_volume_words():
|
def test_history_fund_flow_uses_uint32_volume_words():
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from unittest.mock import MagicMock, patch
|
|||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from easy_tdx.models.bar import SecurityBar
|
from easy_tdx.models.bar import SecurityBar
|
||||||
|
|
||||||
# ── 辅助:构造 SecurityBar ────────────────────────────────────────────────
|
# ── 辅助:构造 SecurityBar ────────────────────────────────────────────────
|
||||||
@@ -300,7 +301,6 @@ class TestScanOne:
|
|||||||
scanner._cash = 100000.0
|
scanner._cash = 100000.0
|
||||||
scanner._commission = 0.0003
|
scanner._commission = 0.0003
|
||||||
|
|
||||||
bars = _make_bars(100)
|
|
||||||
with patch.object(scanner, "_scan_one") as mock_scan:
|
with patch.object(scanner, "_scan_one") as mock_scan:
|
||||||
# 不产生信号时返回 None
|
# 不产生信号时返回 None
|
||||||
mock_scan.return_value = None
|
mock_scan.return_value = None
|
||||||
|
|||||||
@@ -26,10 +26,13 @@ def test_sync_connection_closes_socket_when_setup_fails() -> None:
|
|||||||
sock = _FakeSocket()
|
sock = _FakeSocket()
|
||||||
conn = TdxConnection("127.0.0.1", port=7709, timeout=0.2)
|
conn = TdxConnection("127.0.0.1", port=7709, timeout=0.2)
|
||||||
|
|
||||||
with patch("easy_tdx.transport.sync.socket.socket", return_value=sock), patch.object(
|
with (
|
||||||
TdxConnection,
|
patch("easy_tdx.transport.sync.socket.socket", return_value=sock),
|
||||||
"_send_setup",
|
patch.object(
|
||||||
side_effect=TdxConnectionError("setup failed"),
|
TdxConnection,
|
||||||
|
"_send_setup",
|
||||||
|
side_effect=TdxConnectionError("setup failed"),
|
||||||
|
),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
conn.connect()
|
conn.connect()
|
||||||
|
|||||||
Reference in New Issue
Block a user