mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 18:04:20 +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
@@ -4,7 +4,8 @@
|
||||
# V2.1 2021-6-6 新增 BARSLAST函数 SLOPE,FORCAST线性回归预测函数
|
||||
# V2.3 2021-6-13 新增 TRIX,DPO,BRAR,DMA,MTM,MASS,ROC,VR,ASI等指标
|
||||
# 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.9 2021-11-29 新增 HHVBARS,LLVBARS,CONST, VALUEWHEN功能函数
|
||||
# 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 (此为核心函数,非指标)
|
||||
if isinstance(A, (int, float)):
|
||||
if isinstance(A, int | float):
|
||||
return pd.Series(S).ewm(alpha=A, adjust=False).mean().values
|
||||
A = np.array(A)
|
||||
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的天数
|
||||
return SUM(S, N)
|
||||
|
||||
|
||||
@@ -79,16 +79,15 @@ def backtest(
|
||||
|
||||
# 1. 加载策略(单策略 or 多因子组合)
|
||||
is_combo = combo_strategies is not None
|
||||
strategy = None
|
||||
|
||||
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:
|
||||
strategy = _load_strategy(strategy_str, strategy_file)
|
||||
|
||||
if strategy is None:
|
||||
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
|
||||
raise SystemExit(1)
|
||||
strategy_cls = _load_strategy(strategy_str, strategy_file)
|
||||
if strategy_cls is None:
|
||||
click.echo("错误: 必须指定 --strategy-file / --combo-strategies / --strategy", err=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
# 2. 获取数据
|
||||
mkt = parse_market(market)
|
||||
@@ -111,21 +110,21 @@ def backtest(
|
||||
if is_combo:
|
||||
from ..backtest.combo import CombinationRunner
|
||||
|
||||
assert strategy is not None # for type checker
|
||||
runner = CombinationRunner(
|
||||
strategy_classes=strategy,
|
||||
strategy_classes=combo_classes,
|
||||
df=df,
|
||||
cash=cash,
|
||||
commission=commission,
|
||||
execution=execution,
|
||||
)
|
||||
result = runner.run_combination(
|
||||
indices=list(range(len(strategy))),
|
||||
indices=list(range(len(combo_classes))),
|
||||
mode=combo_mode.upper(),
|
||||
)
|
||||
else:
|
||||
assert strategy_cls is not None # guarded above by SystemExit
|
||||
engine = BacktestEngine(
|
||||
strategy=strategy,
|
||||
strategy=strategy_cls,
|
||||
cash=cash,
|
||||
commission=commission,
|
||||
execution=execution,
|
||||
|
||||
@@ -39,6 +39,11 @@ NDArray = np.ndarray
|
||||
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)
|
||||
|
||||
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":
|
||||
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":
|
||||
n_factors = len(signals_list)
|
||||
threshold = n_factors / 2
|
||||
return (
|
||||
np.sum(buy_stack, axis=0) > threshold,
|
||||
np.sum(sell_stack, axis=0) > threshold,
|
||||
bool_array(np.sum(buy_stack, axis=0) > threshold),
|
||||
bool_array(np.sum(sell_stack, axis=0) > threshold),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"不支持的合并模式: {mode!r}(可选: AND, OR, MAJORITY)")
|
||||
|
||||
@@ -8,6 +8,7 @@ K线合并 → 分型识别 → 笔计算 → 中枢计算 → 线段 → 买卖
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
|
||||
@@ -38,10 +39,10 @@ def _df_to_klines(df: pd.DataFrame) -> list[Kline]:
|
||||
Kline(
|
||||
index=i,
|
||||
date=dt,
|
||||
open=float(row.open),
|
||||
close=float(row.close),
|
||||
high=float(row.high),
|
||||
low=float(row.low),
|
||||
open=float(row.open), # type: ignore[arg-type]
|
||||
close=float(row.close), # type: ignore[arg-type]
|
||||
high=float(row.high), # type: ignore[arg-type]
|
||||
low=float(row.low), # type: ignore[arg-type]
|
||||
amount=float(vol),
|
||||
)
|
||||
)
|
||||
@@ -64,7 +65,7 @@ class ChanlunResult:
|
||||
bcs: list[BC] = field(default_factory=list)
|
||||
macd: dict[str, list[float]] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""将结果转为可序列化的字典(用于 JSON 输出)。"""
|
||||
return {
|
||||
"code": self.code,
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
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(
|
||||
@@ -165,7 +165,7 @@ def _check_qs_beichi(bis: list[BI], zss: list[ZS]) -> list[BC]:
|
||||
return bcs
|
||||
|
||||
|
||||
def _calc_bi_force(bi: BI) -> float:
|
||||
def _calc_bi_force(bi: BI | XD) -> float:
|
||||
"""计算笔的力度(简化:用幅度表示)。
|
||||
|
||||
真正的力度应用 MACD 面积,这里用幅度作为简化替代。
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import click
|
||||
|
||||
@@ -65,7 +66,7 @@ def chanlun(
|
||||
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"原始K线: {result['kline_count']} 缠论K线: {result['ckline_count']}")
|
||||
|
||||
@@ -57,7 +57,8 @@ def kline(
|
||||
mkt = parse_ex_market(market)
|
||||
with get_mac_ex_client() as client:
|
||||
df = client.goods_kline(
|
||||
mkt, code,
|
||||
mkt,
|
||||
code,
|
||||
period=parse_period(period),
|
||||
start=start,
|
||||
count=count,
|
||||
|
||||
@@ -493,8 +493,9 @@ def _fetch_all_daily_bars(
|
||||
Returns:
|
||||
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
|
||||
|
||||
all_bars: list[SecurityBar] = []
|
||||
@@ -503,7 +504,7 @@ def _fetch_all_daily_bars(
|
||||
max_pages = 50 if need_full else 1 # 50 页 = 40000 条,足够覆盖 A 股全部历史
|
||||
|
||||
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:
|
||||
break
|
||||
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.transaction import GetHistoryTransactionDataCmd, GetTransactionDataCmd
|
||||
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 .models.bar import SecurityBar
|
||||
from .models.enums import KlineCategory, Market
|
||||
|
||||
+12
-10
@@ -21,7 +21,7 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
|
||||
return []
|
||||
|
||||
pos = 384
|
||||
(count,) = struct.unpack("<H", data[pos:pos+2])
|
||||
(count,) = struct.unpack("<H", data[pos : pos + 2])
|
||||
pos += 2
|
||||
|
||||
results: list[TdxBlock] = []
|
||||
@@ -40,8 +40,8 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
|
||||
break
|
||||
|
||||
# 板块元数据 (9 字节名称 + 2 字节股票数 + 2 字节类型)
|
||||
name_b = data[pos:pos+9]
|
||||
stock_count, _type = struct.unpack("<HH", data[pos+9:pos+13])
|
||||
name_b = data[pos : pos + 9]
|
||||
stock_count, _type = struct.unpack("<HH", data[pos + 9 : pos + 13])
|
||||
name = name_b.decode("gbk", errors="replace").strip("\x00")
|
||||
|
||||
# 股票代码区 (2800 字节,每只股票 7 字节)
|
||||
@@ -51,17 +51,19 @@ def parse_block_dat(data: bytes, filename: str = "") -> list["TdxBlock"]:
|
||||
actual_count = min(stock_count, 400)
|
||||
for i in range(actual_count):
|
||||
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")
|
||||
if code:
|
||||
codes.append(code)
|
||||
|
||||
results.append(TdxBlock(
|
||||
name=name,
|
||||
category=category,
|
||||
count=stock_count,
|
||||
codes=codes,
|
||||
))
|
||||
results.append(
|
||||
TdxBlock(
|
||||
name=name,
|
||||
category=category,
|
||||
count=stock_count,
|
||||
codes=codes,
|
||||
)
|
||||
)
|
||||
|
||||
# 跳过整个 2813 字节的记录块
|
||||
pos += 2813
|
||||
|
||||
@@ -7,13 +7,10 @@
|
||||
日线及以上(其余 category):4 字节 YYYYMMDD 整数
|
||||
"""
|
||||
|
||||
|
||||
from .._binary import unpack_from
|
||||
|
||||
|
||||
def get_datetime_minute(
|
||||
data: bytes | bytearray, pos: int
|
||||
) -> tuple[int, int, int, int, int, int]:
|
||||
def get_datetime_minute(data: bytes | bytearray, pos: int) -> tuple[int, int, int, int, int, int]:
|
||||
"""解析分钟级时间戳(4 字节)。
|
||||
|
||||
Returns:
|
||||
@@ -28,9 +25,7 @@ def get_datetime_minute(
|
||||
return year, month, day, hour, minute, pos + 4
|
||||
|
||||
|
||||
def get_datetime_day(
|
||||
data: bytes | bytearray, pos: int
|
||||
) -> tuple[int, int, int, int]:
|
||||
def get_datetime_day(data: bytes | bytearray, pos: int) -> tuple[int, int, int, int]:
|
||||
"""解析日期(4 字节 YYYYMMDD)。
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -25,9 +25,9 @@ _HEADER_FMT = "<IIIHH"
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrameHeader:
|
||||
magic: int # 协议魔数,恒为 7654321
|
||||
seq_id: int # ZipFlag(1B) + 请求 bytes 1-4 回显(3B)
|
||||
method: int # 请求回显(1B) + 保留(1B) + Method(2B)
|
||||
magic: int # 协议魔数,恒为 7654321
|
||||
seq_id: int # ZipFlag(1B) + 请求 bytes 1-4 回显(3B)
|
||||
method: int # 请求回显(1B) + 保留(1B) + Method(2B)
|
||||
zipsize: int
|
||||
unzipsize: int
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""通达信行业配置文件 (tdxhy.cfg) 解析器。"""
|
||||
|
||||
|
||||
def parse_tdxhy_cfg(content: bytes) -> dict[str, tuple[str, str]]:
|
||||
"""解析 tdxhy.cfg 字节内容。
|
||||
|
||||
|
||||
返回字典: { "code": (tdx_industry, sw_industry), ... }
|
||||
"""
|
||||
results = {}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
警告:此函数专为成交量设计,不可用于价格字段(pytdx Bug #3)。
|
||||
"""
|
||||
|
||||
|
||||
from .._binary import unpack_from
|
||||
|
||||
|
||||
@@ -53,5 +52,5 @@ def _decode_volume(ivol: int) -> float:
|
||||
|
||||
def _pow2(exp: int) -> float:
|
||||
if exp >= 0:
|
||||
return float(1 << exp) if exp < 63 else 2.0 ** exp
|
||||
return 1.0 / (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
|
||||
|
||||
@@ -39,12 +39,14 @@ class GetCompanyInfoCategoryCmd(BaseCommand[list[CompanyInfoCategory]]):
|
||||
raw = b[:nul] if nul != -1 else b
|
||||
return raw.decode("gbk", errors="replace")
|
||||
|
||||
results.append(CompanyInfoCategory(
|
||||
name=_decode(name_b),
|
||||
filename=_decode(filename_b),
|
||||
start=start,
|
||||
length=length,
|
||||
))
|
||||
results.append(
|
||||
CompanyInfoCategory(
|
||||
name=_decode(name_b),
|
||||
filename=_decode(filename_b),
|
||||
start=start,
|
||||
length=length,
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
@@ -52,9 +54,7 @@ class GetCompanyInfoCategoryCmd(BaseCommand[list[CompanyInfoCategory]]):
|
||||
class GetCompanyInfoContentCmd(BaseCommand[str]):
|
||||
"""按文件名、偏移、长度读取公司信息文本(GBK 编码)。"""
|
||||
|
||||
def __init__(
|
||||
self, market: Market, code: str, filename: str, offset: int, length: int
|
||||
) -> None:
|
||||
def __init__(self, market: Market, code: str, filename: str, offset: int, length: int) -> None:
|
||||
self.market = market
|
||||
self.code = code.encode("utf-8")
|
||||
self.filename = filename.encode("gbk")
|
||||
@@ -66,7 +66,13 @@ class GetCompanyInfoContentCmd(BaseCommand[str]):
|
||||
header = bytes.fromhex("0c07109c0001680068 00d002".replace(" ", ""))
|
||||
return header + struct.pack(
|
||||
"<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:
|
||||
|
||||
@@ -31,15 +31,41 @@ class GetFinanceInfoCmd(BaseCommand[FinanceInfo]):
|
||||
|
||||
fields = struct.unpack(_FIN_FMT, slice_bytes(body, pos, _FIN_SIZE, "finance_info body"))
|
||||
(
|
||||
liutong_guben, province, 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,
|
||||
liutong_guben,
|
||||
province,
|
||||
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,
|
||||
liudong_fuzhai, changqi_fuzhai, ziben_gongjijin, jing_zichan,
|
||||
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,
|
||||
liudong_fuzhai,
|
||||
changqi_fuzhai,
|
||||
ziben_gongjijin,
|
||||
jing_zichan,
|
||||
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
|
||||
|
||||
_SCALE = 10000.0 # 财务数据单位:万元/万股
|
||||
|
||||
@@ -41,35 +41,39 @@ class GetHistoryFundFlowCmd(BaseCommand[list[HistoricalFundFlow]]):
|
||||
# 响应格式:9字节头 + 2字节数量 + 每条记录 36 字节
|
||||
if len(body) < 11:
|
||||
return []
|
||||
|
||||
|
||||
(num,) = struct.unpack("<H", body[9:11])
|
||||
pos = 11
|
||||
results = []
|
||||
|
||||
|
||||
for _ in range(num):
|
||||
if len(body) < pos + 36:
|
||||
break
|
||||
|
||||
|
||||
# 记录格式:4字节日期 + 8个4字节自定义浮点金额
|
||||
# [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]
|
||||
year = raw_date // 10000
|
||||
month = (raw_date // 100) % 100
|
||||
day = raw_date % 100
|
||||
|
||||
results.append(HistoricalFundFlow(
|
||||
year=year, month=month, day=day,
|
||||
super_in=_decode_volume(raw_data[1]),
|
||||
large_in=_decode_volume(raw_data[2]),
|
||||
medium_in=_decode_volume(raw_data[3]),
|
||||
small_in=_decode_volume(raw_data[4]),
|
||||
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]),
|
||||
))
|
||||
|
||||
results.append(
|
||||
HistoricalFundFlow(
|
||||
year=year,
|
||||
month=month,
|
||||
day=day,
|
||||
super_in=_decode_volume(raw_data[1]),
|
||||
large_in=_decode_volume(raw_data[2]),
|
||||
medium_in=_decode_volume(raw_data[3]),
|
||||
small_in=_decode_volume(raw_data[4]),
|
||||
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
|
||||
|
||||
|
||||
return results
|
||||
|
||||
@@ -39,10 +39,10 @@ class GetSecurityListCmd(BaseCommand[list[SecurityInfo]]):
|
||||
code_bytes,
|
||||
volunit,
|
||||
name_bytes,
|
||||
_unknown1, # 4字节,排序/分组字段(非用户可见数据)
|
||||
_unknown1, # 4字节,排序/分组字段(非用户可见数据)
|
||||
decimal_point,
|
||||
pre_close_raw,
|
||||
_unknown2, # 4字节,私有时间戳(非用户可见数据)
|
||||
_unknown2, # 4字节,私有时间戳(非用户可见数据)
|
||||
) = struct.unpack("<6sH8s4sBI4s", raw)
|
||||
|
||||
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_CMD2: Final[bytes] = bytes.fromhex("0c0218940001030003000d0002")
|
||||
SETUP_CMD3: Final[bytes] = bytes.fromhex(
|
||||
"0c031899000120002000db0f"
|
||||
"d5d0c9ccd6a4a8af0000008f"
|
||||
"c22540130000d500c9ccbdf0"
|
||||
"d7ea00000002"
|
||||
"0c031899000120002000db0fd5d0c9ccd6a4a8af0000008fc22540130000d500c9ccbdf0d7ea00000002"
|
||||
)
|
||||
|
||||
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(" ", ""))
|
||||
return header + struct.pack("<H6sHH", int(self.market), self.code, self.start, self.count)
|
||||
|
||||
|
||||
|
||||
def parse_response(self, body: bytes) -> list[TransactionRecord]:
|
||||
return _parse_transaction_body(body)
|
||||
|
||||
@@ -35,9 +33,7 @@ class GetTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
|
||||
class GetHistoryTransactionDataCmd(BaseCommand[list[TransactionRecord]]):
|
||||
"""获取历史某日逐笔成交(date 格式 YYYYMMDD,分页)。"""
|
||||
|
||||
def __init__(
|
||||
self, market: Market, code: str, date: int, start: int, count: int = 800
|
||||
) -> None:
|
||||
def __init__(self, market: Market, code: str, date: int, start: int, count: int = 800) -> None:
|
||||
self.market = market
|
||||
self.code = code.encode("utf-8")
|
||||
self.date = date
|
||||
@@ -72,11 +68,17 @@ def _parse_transaction_body(body: bytes) -> list[TransactionRecord]:
|
||||
buyorsell, pos = get_price(body, pos)
|
||||
unknown_last, pos = get_price(body, pos) # Bug #4 修复:不再丢弃
|
||||
last_price += price_diff
|
||||
records.append(TransactionRecord(
|
||||
hour=hour, minute=minute,
|
||||
price=last_price / 100.0, vol=vol, buyorsell=buyorsell,
|
||||
unknown_last=unknown_last, _raw=body[record_start:pos],
|
||||
))
|
||||
records.append(
|
||||
TransactionRecord(
|
||||
hour=hour,
|
||||
minute=minute,
|
||||
price=last_price / 100.0,
|
||||
vol=vol,
|
||||
buyorsell=buyorsell,
|
||||
unknown_last=unknown_last,
|
||||
_raw=body[record_start:pos],
|
||||
)
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
@@ -93,13 +95,19 @@ def _parse_history_transaction_body(body: bytes) -> list[TransactionRecord]:
|
||||
hour, minute, pos = get_time(body, pos)
|
||||
price_diff, 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)
|
||||
last_price += price_diff
|
||||
records.append(TransactionRecord(
|
||||
hour=hour, minute=minute,
|
||||
price=last_price / 100.0, vol=vol, buyorsell=buyorsell,
|
||||
unknown_last=unknown_last, _raw=body[record_start:pos],
|
||||
))
|
||||
records.append(
|
||||
TransactionRecord(
|
||||
hour=hour,
|
||||
minute=minute,
|
||||
price=last_price / 100.0,
|
||||
vol=vol,
|
||||
buyorsell=buyorsell,
|
||||
unknown_last=unknown_last,
|
||||
_raw=body[record_start:pos],
|
||||
)
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
+12
-12
@@ -27,7 +27,7 @@ import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
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_FILE = _CONFIG_DIR / "config.json"
|
||||
@@ -137,7 +137,7 @@ _FALLBACK_TIMEOUT = 15.0
|
||||
def _load() -> dict[str, Any]:
|
||||
try:
|
||||
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:
|
||||
pass
|
||||
return {}
|
||||
@@ -161,7 +161,7 @@ def get_best_host() -> str:
|
||||
if env:
|
||||
return env
|
||||
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]:
|
||||
@@ -170,25 +170,25 @@ def get_known_hosts() -> list[str]:
|
||||
if env:
|
||||
return [h.strip() for h in env.split(",") if h.strip()]
|
||||
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]:
|
||||
"""返回计算服务器列表。"""
|
||||
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]:
|
||||
"""返回 MAC 行情服务器列表。"""
|
||||
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]:
|
||||
"""返回扩展行情服务器列表。"""
|
||||
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:
|
||||
@@ -197,13 +197,13 @@ def get_best_ex_host() -> str:
|
||||
if env:
|
||||
return env
|
||||
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]:
|
||||
"""返回 MAC 协议扩展行情服务器列表。"""
|
||||
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:
|
||||
@@ -212,7 +212,7 @@ def get_best_mac_ex_host() -> str:
|
||||
if env:
|
||||
return env
|
||||
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:
|
||||
@@ -221,7 +221,7 @@ def get_port() -> int:
|
||||
if env:
|
||||
return int(env)
|
||||
cfg = _load()
|
||||
return cfg.get("port", _FALLBACK_PORT)
|
||||
return cast(int, cfg.get("port", _FALLBACK_PORT))
|
||||
|
||||
|
||||
def get_timeout() -> float:
|
||||
@@ -230,7 +230,7 @@ def get_timeout() -> float:
|
||||
if env:
|
||||
return float(env)
|
||||
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
|
||||
|
||||
# 80 字节 Login body,来自 opentdx 参考实现,已通过实际测试验证。
|
||||
_LOGIN_BODY = bytes(bytearray.fromhex(
|
||||
"e5bb1c2fafe52594"
|
||||
"1f32c6e5d53dfb41"
|
||||
"5b734cc9cdbf0ac9"
|
||||
"2021bfdd1eb06d22"
|
||||
"d008884c1611cb13"
|
||||
"78f6abd824d899d2"
|
||||
"1f32c6e5d53dfb41"
|
||||
"1f32c6e5d53dfb41"
|
||||
"a9325ac935dc0837"
|
||||
"335a16e4ce17c1bb"
|
||||
))
|
||||
_LOGIN_BODY = bytes(
|
||||
bytearray.fromhex(
|
||||
"e5bb1c2fafe52594"
|
||||
"1f32c6e5d53dfb41"
|
||||
"5b734cc9cdbf0ac9"
|
||||
"2021bfdd1eb06d22"
|
||||
"d008884c1611cb13"
|
||||
"78f6abd824d899d2"
|
||||
"1f32c6e5d53dfb41"
|
||||
"1f32c6e5d53dfb41"
|
||||
"a9325ac935dc0837"
|
||||
"335a16e4ce17c1bb"
|
||||
)
|
||||
)
|
||||
|
||||
# EX 协议帧头格式: head_flag(1B) + customize(4B) + version(1B) + zipsize(2B) + unzipsize(2B)
|
||||
_EX_HEADER_FMT = "<BIBHH"
|
||||
|
||||
@@ -13,18 +13,18 @@ import pandas as pd
|
||||
|
||||
from .._df import _to_df
|
||||
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 .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.symbol_bar import SymbolBarCmd
|
||||
from ..mac.commands.symbol_quotes import SymbolQuotesCmd
|
||||
from ..mac.commands.symbol_tick_chart import SymbolTickChartCmd
|
||||
from ..mac.commands.symbol_transaction import SymbolTransactionCmd
|
||||
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 .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.sync import ExTdxConnection, ping_ex_all
|
||||
|
||||
@@ -194,7 +194,7 @@ class MacExClient:
|
||||
return pd.DataFrame()
|
||||
total = self._execute(GetExInstrumentCountCmd())
|
||||
page_size = 1000
|
||||
collected: list = []
|
||||
collected: list[Any] = []
|
||||
skipped = 0
|
||||
pos = offset
|
||||
while pos < total and len(collected) < count:
|
||||
@@ -532,7 +532,9 @@ class AsyncMacExClient:
|
||||
if not self._auto_reconnect:
|
||||
raise
|
||||
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._login()
|
||||
return await self._conn.execute(cmd)
|
||||
@@ -571,7 +573,7 @@ class AsyncMacExClient:
|
||||
return pd.DataFrame()
|
||||
total = await self._execute(GetExInstrumentCountCmd())
|
||||
page_size = 1000
|
||||
collected: list = []
|
||||
collected: list[Any] = []
|
||||
skipped = 0
|
||||
pos = offset
|
||||
while pos < total and len(collected) < count:
|
||||
|
||||
@@ -5,9 +5,8 @@ from types import TracebackType
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
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 ..models import KNOWN_EX_HOSTS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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 ...exceptions import TdxConnectionError
|
||||
from ..commands.get_instrument_count import GetExInstrumentCountCmd
|
||||
from ..models import KNOWN_EX_HOSTS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...commands.base import BaseCommand
|
||||
|
||||
@@ -6,7 +6,6 @@ import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from . import MyTT
|
||||
|
||||
@@ -1189,7 +1189,11 @@ class AsyncMacClient:
|
||||
|
||||
fetch_count = max(120 + count, 200)
|
||||
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:
|
||||
return df
|
||||
|
||||
@@ -59,7 +59,10 @@ class GoodsListCmd(BaseCommand[list[GoodsItem]]):
|
||||
offset = 2 + i * _RECORD_SIZE
|
||||
require_bytes(body, offset, _RECORD_SIZE, f"GoodsListCmd record[{i}]")
|
||||
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")
|
||||
items.append(
|
||||
|
||||
@@ -131,9 +131,7 @@ class UnusualCmd(BaseCommand[list[UnusualItem]]):
|
||||
|
||||
desc, value = _describe_unusual(unusual_type, body[offset + 15 : offset + 28])
|
||||
|
||||
hour, minute_sec = unpack_from(
|
||||
"<BH", body, offset + 29, f"unusual time[{i}]"
|
||||
)
|
||||
hour, minute_sec = unpack_from("<BH", body, offset + 29, f"unusual time[{i}]")
|
||||
|
||||
results.append(
|
||||
UnusualItem(
|
||||
|
||||
@@ -11,7 +11,7 @@ class SecurityBar:
|
||||
close: float
|
||||
high: float
|
||||
low: float
|
||||
vol: float # 成交量(股)
|
||||
vol: float # 成交量(股)
|
||||
amount: float # 成交额(元)
|
||||
|
||||
year: int
|
||||
|
||||
@@ -18,27 +18,27 @@ class XdxrRecord:
|
||||
year: int
|
||||
month: int
|
||||
day: int
|
||||
category: int # 事件类型(见下方 CATEGORY_NAMES)
|
||||
name: str # 事件类型名称
|
||||
category: int # 事件类型(见下方 CATEGORY_NAMES)
|
||||
name: str # 事件类型名称
|
||||
|
||||
# category == 1(除权除息)
|
||||
fenhong: float | None = None # 每股分红(元;协议原值按每10股)
|
||||
peigujia: float | None = None # 配股价(元/股)
|
||||
fenhong: float | None = None # 每股分红(元;协议原值按每10股)
|
||||
peigujia: float | None = None # 配股价(元/股)
|
||||
songzhuangu: float | None = None # 每股送转股比例(协议原值按每10股)
|
||||
peigu: float | None = None # 每股配股比例(协议原值按每10股)
|
||||
peigu: float | None = None # 每股配股比例(协议原值按每10股)
|
||||
|
||||
# category in [11, 12](扩缩股)
|
||||
suogu: float | None = None # 缩股比例
|
||||
suogu: float | None = None # 缩股比例
|
||||
|
||||
# category in [13, 14](权证)
|
||||
xingquanjia: float | None = None # 行权价
|
||||
fenshu: float | None = None # 分数
|
||||
fenshu: float | None = None # 分数
|
||||
|
||||
# category in [2..10](股本变动类,单位:万股)
|
||||
panqian_liutong: float | None = None # 盘前流通股本(万股)
|
||||
panhou_liutong: float | None = None # 盘后流通股本(万股)
|
||||
qian_zongguben: float | None = None # 前总股本(万股)
|
||||
hou_zongguben: float | None = None # 后总股本(万股)
|
||||
panqian_liutong: float | None = None # 盘前流通股本(万股)
|
||||
panhou_liutong: float | None = None # 盘后流通股本(万股)
|
||||
qian_zongguben: float | None = None # 前总股本(万股)
|
||||
hou_zongguben: float | None = None # 后总股本(万股)
|
||||
|
||||
_raw: bytes = field(default=b"", repr=False, compare=False)
|
||||
|
||||
@@ -69,45 +69,45 @@ class FinanceInfo:
|
||||
code: str
|
||||
|
||||
# 股本(万股)
|
||||
liutong_guben: float # 流通股本
|
||||
zong_guben: float # 总股本
|
||||
guojia_gu: float # 国家股
|
||||
liutong_guben: float # 流通股本
|
||||
zong_guben: float # 总股本
|
||||
guojia_gu: float # 国家股
|
||||
faqiren_faren_gu: float # 发起人法人股
|
||||
faren_gu: float # 法人股
|
||||
b_gu: float # B股
|
||||
h_gu: float # H股
|
||||
zhigong_gu: float # 职工股
|
||||
faren_gu: float # 法人股
|
||||
b_gu: float # B股
|
||||
h_gu: float # H股
|
||||
zhigong_gu: float # 职工股
|
||||
|
||||
# 基本信息
|
||||
province: int # 所属省份代码
|
||||
industry: int # 所属行业代码
|
||||
updated_date: int # 财务更新日期 YYYYMMDD
|
||||
ipo_date: int # 上市日期 YYYYMMDD
|
||||
gudong_renshu: float # 股东人数
|
||||
province: int # 所属省份代码
|
||||
industry: int # 所属行业代码
|
||||
updated_date: int # 财务更新日期 YYYYMMDD
|
||||
ipo_date: int # 上市日期 YYYYMMDD
|
||||
gudong_renshu: float # 股东人数
|
||||
|
||||
# 资产负债(元)
|
||||
zong_zichan: float # 总资产
|
||||
liudong_zichan: float # 流动资产
|
||||
guding_zichan: float # 固定资产
|
||||
wuxing_zichan: float # 无形资产
|
||||
liudong_fuzhai: float # 流动负债
|
||||
changqi_fuzhai: float # 长期负债
|
||||
ziben_gongjijin: float # 资本公积金
|
||||
jing_zichan: float # 净资产
|
||||
zong_zichan: float # 总资产
|
||||
liudong_zichan: float # 流动资产
|
||||
guding_zichan: float # 固定资产
|
||||
wuxing_zichan: float # 无形资产
|
||||
liudong_fuzhai: float # 流动负债
|
||||
changqi_fuzhai: float # 长期负债
|
||||
ziben_gongjijin: float # 资本公积金
|
||||
jing_zichan: float # 净资产
|
||||
|
||||
# 利润(元)
|
||||
zhuying_shouru: float # 主营收入
|
||||
zhuying_lirun: float # 主营利润
|
||||
zhuying_shouru: float # 主营收入
|
||||
zhuying_lirun: float # 主营利润
|
||||
yingshou_zhangkuan: float # 应收账款
|
||||
yingye_lirun: float # 营业利润
|
||||
touzi_shouyu: float # 投资收益
|
||||
yingye_lirun: float # 营业利润
|
||||
touzi_shouyu: float # 投资收益
|
||||
jingying_xianjinliu: float # 经营现金流
|
||||
zong_xianjinliu: float # 总现金流
|
||||
cunhuo: float # 存货
|
||||
lirun_zonghe: float # 利润总额
|
||||
shuihou_lirun: float # 税后利润
|
||||
jing_lirun: float # 净利润
|
||||
weifen_lirun: float # 未分配利润
|
||||
zong_xianjinliu: float # 总现金流
|
||||
cunhuo: float # 存货
|
||||
lirun_zonghe: float # 利润总额
|
||||
shuihou_lirun: float # 税后利润
|
||||
jing_lirun: float # 净利润
|
||||
weifen_lirun: float # 未分配利润
|
||||
|
||||
# 每股指标
|
||||
meigujing_zichan: float # 每股净资产(原 baoliu1)
|
||||
@@ -122,36 +122,36 @@ class FinanceInfo:
|
||||
class CompanyInfoCategory:
|
||||
"""公司信息文件目录条目"""
|
||||
|
||||
name: str = "" # 目录名(如“最新提示”)
|
||||
name: str = "" # 目录名(如“最新提示”)
|
||||
filename: str = "" # 文件名(如 '600000.txt')
|
||||
start: int = 0 # 内容起始偏移
|
||||
length: int = 0 # 内容长度(字节)
|
||||
start: int = 0 # 内容起始偏移
|
||||
length: int = 0 # 内容长度(字节)
|
||||
|
||||
|
||||
@dataclass
|
||||
class FinancialFileInfo:
|
||||
"""财报 zip 文件索引条目(来自 tdxfin/gpcw.txt)。"""
|
||||
|
||||
filename: str # "gpcw20260331.zip"
|
||||
hash: str # MD5 hex digest
|
||||
filesize: int # 字节
|
||||
filename: str # "gpcw20260331.zip"
|
||||
hash: str # MD5 hex digest
|
||||
filesize: int # 字节
|
||||
|
||||
|
||||
@dataclass
|
||||
class FinancialRecord:
|
||||
"""单只股票的一期历史专业财报记录。"""
|
||||
|
||||
code: str # 6 位股票代码
|
||||
market: Market # 市场
|
||||
report_date: int # 报告期 YYYYMMDD
|
||||
fields: list[float] # N 个浮点字段(N = report_size / 4)
|
||||
code: str # 6 位股票代码
|
||||
market: Market # 市场
|
||||
report_date: int # 报告期 YYYYMMDD
|
||||
fields: list[float] # N 个浮点字段(N = report_size / 4)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TdxBlock:
|
||||
"""通达信板块信息(行业、概念、风格等)"""
|
||||
|
||||
name: str # 板块名称(如“房地产”)
|
||||
category: int # 板块分类(0=行业, 1=地域, 2=概念, 3=风格, 等)
|
||||
count: int # 板块包含股票数量
|
||||
codes: list[str] # 股票代码列表(6位数字代码)
|
||||
name: str # 板块名称(如“房地产”)
|
||||
category: int # 板块分类(0=行业, 1=地域, 2=概念, 3=风格, 等)
|
||||
count: int # 板块包含股票数量
|
||||
codes: list[str] # 股票代码列表(6位数字代码)
|
||||
|
||||
@@ -11,13 +11,13 @@ class SecurityInfo:
|
||||
|
||||
market: Market
|
||||
code: str
|
||||
name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代)
|
||||
volunit: int # 成交量单位(手 = volunit 股)
|
||||
name: str # 股票名称(GBK 解码,截断字节用 replacement char 替代)
|
||||
volunit: int # 成交量单位(手 = volunit 股)
|
||||
decimal_point: int # 价格小数位数
|
||||
pre_close: float # 昨收价(通达信自定义浮点解码)
|
||||
pre_close: float # 昨收价(通达信自定义浮点解码)
|
||||
|
||||
# 扩展字段(通过 get_security_list_all 关联 tdxhy.cfg 获得)
|
||||
industry_tdx: str = "" # 通达信行业代码 (如 T1001)
|
||||
industry_sw: str = "" # 申万行业代码 (如 X500102)
|
||||
industry_sw: str = "" # 申万行业代码 (如 X500102)
|
||||
|
||||
_raw: bytes = field(default=b"", repr=False, compare=False)
|
||||
|
||||
@@ -1,58 +1,61 @@
|
||||
"""验证市场概况模型。"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class MarketStat:
|
||||
"""全市场涨跌统计概况。"""
|
||||
up_count: int # 上涨家数
|
||||
down_count: int # 下跌家数
|
||||
neutral_count: int # 平盘家数
|
||||
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
|
||||
total_count: int # 总计(包含停牌)
|
||||
total_amount: float # 总成交额
|
||||
total_volume: float # 总成交量
|
||||
total_market_cap: float # 总市值(亿元),来自 880001 收盘价,÷100 得万亿
|
||||
limit_up_count: int # 涨停家数,来自 880006 close
|
||||
limit_down_count: int # 跌停家数,来自 880006 open
|
||||
|
||||
up_count: int # 上涨家数
|
||||
down_count: int # 下跌家数
|
||||
neutral_count: int # 平盘家数
|
||||
suspended_count: int # 由 total-(up+down+neutral) 得到的残差项,近似表示停牌/未参与统计家数
|
||||
total_count: int # 总计(包含停牌)
|
||||
total_amount: float # 总成交额
|
||||
total_volume: float # 总成交量
|
||||
total_market_cap: float # 总市值(亿元),来自 880001 收盘价,÷100 得万亿
|
||||
limit_up_count: int # 涨停家数,来自 880006 close
|
||||
limit_down_count: int # 跌停家数,来自 880006 open
|
||||
|
||||
|
||||
@dataclass
|
||||
class FundFlow:
|
||||
"""个股资金流向统计(基于 Tick 数据加权计算)。"""
|
||||
|
||||
|
||||
# 流入项 (Buy)
|
||||
super_in: float # 超大单流入 (>100万)
|
||||
large_in: float # 大单流入 (>20万 且 <=100万)
|
||||
super_in: float # 超大单流入 (>100万)
|
||||
large_in: float # 大单流入 (>20万 且 <=100万)
|
||||
medium_in: float # 中单流入 (>4万 且 <=20万)
|
||||
small_in: float # 小单流入 (<=4万)
|
||||
|
||||
small_in: float # 小单流入 (<=4万)
|
||||
|
||||
# 流出项 (Sell)
|
||||
super_out: float
|
||||
large_out: float
|
||||
medium_out: float
|
||||
small_out: float
|
||||
|
||||
|
||||
@property
|
||||
def main_net_inflow(self) -> float:
|
||||
"""主力净流入 (超大单 + 大单)。"""
|
||||
return (self.super_in + self.large_in) - (self.super_out + self.large_out)
|
||||
|
||||
|
||||
@property
|
||||
def total_net_inflow(self) -> float:
|
||||
"""全单净流入。"""
|
||||
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)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoricalFundFlow:
|
||||
"""历史日线资金流向条目。"""
|
||||
|
||||
|
||||
year: int
|
||||
month: int
|
||||
day: int
|
||||
|
||||
|
||||
# 金额项 (单位:元)
|
||||
super_in: float
|
||||
super_out: float
|
||||
@@ -62,7 +65,7 @@ class HistoricalFundFlow:
|
||||
medium_out: float
|
||||
small_in: float
|
||||
small_out: float
|
||||
|
||||
|
||||
@property
|
||||
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)
|
||||
last_record = f.read(_DAILY_FMT.size)
|
||||
(date_int, *_) = _DAILY_FMT.unpack(last_record)
|
||||
return date_int
|
||||
return int(date_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)
|
||||
last_record = f.read(_EX_DAILY_FMT.size)
|
||||
(date_int, *_) = _EX_DAILY_FMT.unpack(last_record)
|
||||
return date_int
|
||||
return int(date_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 ..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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
+20
-8
@@ -95,7 +95,7 @@ class UnifiedTdxClient:
|
||||
def get_stock_quotes(
|
||||
self,
|
||||
stocks: list[tuple[int, str]],
|
||||
fields: object = None,
|
||||
fields: Any = None,
|
||||
) -> pd.DataFrame:
|
||||
return self._ensure_mac().get_stock_quotes(stocks, fields)
|
||||
|
||||
@@ -107,7 +107,7 @@ class UnifiedTdxClient:
|
||||
sort_type: SortType = SortType.CHANGE_PCT,
|
||||
sort_order: SortOrder = SortOrder.DESC,
|
||||
exclude_flags: list[FilterType] | None = None,
|
||||
fields: object = None,
|
||||
fields: Any = None,
|
||||
) -> pd.DataFrame:
|
||||
return self._ensure_mac().get_stock_quotes_list(
|
||||
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,
|
||||
) -> pd.DataFrame:
|
||||
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(
|
||||
@@ -185,7 +191,7 @@ class UnifiedTdxClient:
|
||||
count: int = 100000,
|
||||
sort_type: SortType = SortType.CHANGE_PCT,
|
||||
sort_order: SortOrder = SortOrder.DESC,
|
||||
fields: object = None,
|
||||
fields: Any = None,
|
||||
exclude_flags: list[FilterType] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
return self._ensure_mac().get_board_members(
|
||||
@@ -378,7 +384,7 @@ class AsyncUnifiedTdxClient:
|
||||
async def get_stock_quotes(
|
||||
self,
|
||||
stocks: list[tuple[int, str]],
|
||||
fields: object = None,
|
||||
fields: Any = None,
|
||||
) -> pd.DataFrame:
|
||||
mac = await self._ensure_mac()
|
||||
return await mac.get_stock_quotes(stocks, fields)
|
||||
@@ -391,7 +397,7 @@ class AsyncUnifiedTdxClient:
|
||||
sort_type: SortType = SortType.CHANGE_PCT,
|
||||
sort_order: SortOrder = SortOrder.DESC,
|
||||
exclude_flags: list[FilterType] | None = None,
|
||||
fields: object = None,
|
||||
fields: Any = None,
|
||||
) -> pd.DataFrame:
|
||||
mac = await self._ensure_mac()
|
||||
return await mac.get_stock_quotes_list(
|
||||
@@ -423,7 +429,13 @@ class AsyncUnifiedTdxClient:
|
||||
) -> pd.DataFrame:
|
||||
mac = await self._ensure_mac()
|
||||
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(
|
||||
@@ -478,7 +490,7 @@ class AsyncUnifiedTdxClient:
|
||||
count: int = 100000,
|
||||
sort_type: SortType = SortType.CHANGE_PCT,
|
||||
sort_order: SortOrder = SortOrder.DESC,
|
||||
fields: object = None,
|
||||
fields: Any = None,
|
||||
exclude_flags: list[FilterType] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
mac = await self._ensure_mac()
|
||||
|
||||
Reference in New Issue
Block a user