feat!: rename project from xmtdx to easy-tdx

- Package directory: src/xmtdx/ -> src/easy_tdx/
- Import path: from easy_tdx import ...
- pip install easy-tdx
- Add LICENSE (MIT) with upstream attribution (pytdx, xmtdx)
- Add NOTICE with detailed attribution
- Update all examples, tests, scripts, docs
- Bump version to 1.0.0

BREAKING CHANGE: import path changed from `xmtdx` to `easy_tdx`

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-05-21 23:21:21 +08:00
co-authored by Claude Opus 4.7
parent 9c5672b4d2
commit 50491f9aae
119 changed files with 1175 additions and 167 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ import os
import pytest
from xmtdx import AsyncTdxClient, Market, TdxClient
from easy_tdx import AsyncTdxClient, Market, TdxClient
_LIVE_ENABLED = os.getenv("XMTDX_LIVE") == "1"
_LIVE_HOST = os.getenv("XMTDX_HOST", "180.153.18.170")
+20 -20
View File
@@ -4,17 +4,17 @@ import asyncio
import struct
from unittest.mock import patch
from xmtdx import AsyncTdxClient, Market, TdxClient
from xmtdx.client import _classify_fund_flow
from xmtdx.models.bar import SecurityBar
from xmtdx.models.quote import SecurityQuote
from xmtdx.models.security import SecurityInfo
from xmtdx.models.stats import HistoricalFundFlow
from xmtdx.models.timeseries import MinuteBar
from xmtdx.models.timeseries import TransactionRecord
from easy_tdx import AsyncTdxClient, Market, TdxClient
from easy_tdx.client import _classify_fund_flow
from easy_tdx.models.bar import SecurityBar
from easy_tdx.models.quote import SecurityQuote
from easy_tdx.models.security import SecurityInfo
from easy_tdx.models.stats import HistoricalFundFlow
from easy_tdx.models.timeseries import MinuteBar
from easy_tdx.models.timeseries import TransactionRecord
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_fund_flow_logic(_mock_conn_cls):
"""测试资金流分类计算逻辑。"""
client = TdxClient("127.0.0.1")
@@ -48,7 +48,7 @@ def test_classify_fund_flow_exact_thresholds_use_lower_bucket():
assert flow.medium_in == 200000.0
assert flow.small_in == 40000.0
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_security_list_all_filtering(_mock_conn_cls):
"""测试三市 A 股过滤与行业挂载逻辑。"""
client = TdxClient("127.0.0.1")
@@ -84,7 +84,7 @@ def test_get_security_list_all_filtering(_mock_conn_cls):
s0 = next(s for s in all_stocks if s.code == "600000")
assert s0.industry_tdx == "T01"
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_market_stat_mapping(_mock_conn_cls):
"""测试市场统计字段映射。"""
client = TdxClient("127.0.0.1")
@@ -114,7 +114,7 @@ def test_get_market_stat_mapping(_mock_conn_cls):
def test_get_history_fund_flow_parsing():
"""测试历史资金流序列解析逻辑。"""
from xmtdx.commands.fund_flow import GetHistoryFundFlowCmd
from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd
# 模拟 Category 22 响应 (Header 9 + Count 2 + Body 36)
body = bytearray(9)
@@ -136,7 +136,7 @@ def test_get_history_fund_flow_parsing():
assert res[0].day == 8
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_history_fund_flow_fallback(_mock_conn_cls):
"""Category 22 空回包时,自动回退到历史逐笔重算。"""
client = TdxClient("127.0.0.1")
@@ -197,7 +197,7 @@ def test_get_history_fund_flow_fallback(_mock_conn_cls):
]
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_price_limits_uses_listing_window(_mock_conn_cls):
"""client.get_price_limits 应结合日 K 条数判断上市初期限价窗口。"""
client = TdxClient("127.0.0.1")
@@ -223,13 +223,13 @@ def test_get_price_limits_uses_listing_window(_mock_conn_cls):
)
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_minute_time_data_prefers_history_endpoint(_mock_conn_cls):
"""今日分时优先走历史分时接口,规避当前分时协议歧义。"""
client = TdxClient("127.0.0.1")
expected = [MinuteBar(price=9.7, vol=13694)]
with patch("xmtdx.client._today_in_shanghai", return_value=20260422), patch.object(
with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object(
TdxClient,
"get_history_minute_time_data",
return_value=expected,
@@ -244,13 +244,13 @@ def test_get_minute_time_data_prefers_history_endpoint(_mock_conn_cls):
assert result == expected
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_minute_time_data_falls_back_to_current_endpoint(_mock_conn_cls):
"""历史分时失败时,仍回退到原今日分时命令。"""
client = TdxClient("127.0.0.1")
fallback = [MinuteBar(price=9.61, vol=10698)]
with patch("xmtdx.client._today_in_shanghai", return_value=20260422), patch.object(
with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object(
TdxClient,
"get_history_minute_time_data",
side_effect=RuntimeError("history unavailable"),
@@ -271,9 +271,9 @@ def test_async_get_minute_time_data_prefers_history_endpoint():
expected = [MinuteBar(price=9.7, vol=13694)]
async def run_test() -> None:
with patch("xmtdx.client.AsyncTdxConnection"):
with patch("easy_tdx.client.AsyncTdxConnection"):
client = AsyncTdxClient("127.0.0.1")
with patch("xmtdx.client._today_in_shanghai", return_value=20260422), patch.object(
with patch("easy_tdx.client._today_in_shanghai", return_value=20260422), patch.object(
AsyncTdxClient,
"get_history_minute_time_data",
return_value=expected,
+4 -4
View File
@@ -6,10 +6,10 @@ import asyncio
import struct
import time
from xmtdx import AsyncTdxClient, Market
from xmtdx.commands.security_count import GetSecurityCountCmd
from xmtdx.commands.setup import SETUP_COMMANDS
from xmtdx.exceptions import TdxConnectionError
from easy_tdx import AsyncTdxClient, Market
from easy_tdx.commands.security_count import GetSecurityCountCmd
from easy_tdx.commands.setup import SETUP_COMMANDS
from easy_tdx.exceptions import TdxConnectionError
def _pack_frame(body: bytes) -> bytes:
+9 -9
View File
@@ -4,19 +4,19 @@ import asyncio
import struct
from unittest.mock import patch
from xmtdx.client import AsyncTdxClient, TdxClient
from xmtdx.codec.block import parse_block_dat
from xmtdx.models.finance import TdxBlock
from easy_tdx.client import AsyncTdxClient, TdxClient
from easy_tdx.codec.block import parse_block_dat
from easy_tdx.models.finance import TdxBlock
@patch("xmtdx.client.AsyncTdxConnection")
@patch("easy_tdx.client.AsyncTdxConnection")
def test_async_get_block_info_logic(mock_conn_cls):
"""测试 AsyncTdxClient.get_block_info 的异步拉取逻辑。"""
mock_conn = mock_conn_cls.return_value
# 模拟异步 execute
async def mock_execute(cmd):
from xmtdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
from easy_tdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
if isinstance(cmd, GetBlockInfoMetaCmd):
return 100, "hash"
if isinstance(cmd, GetBlockInfoCmd):
@@ -29,7 +29,7 @@ def test_async_get_block_info_logic(mock_conn_cls):
async def main():
client = AsyncTdxClient("127.0.0.1")
with patch("xmtdx.client.parse_block_dat") as mock_parse:
with patch("easy_tdx.client.parse_block_dat") as mock_parse:
mock_parse.return_value = []
res = await client.get_block_info("test.dat")
@@ -71,7 +71,7 @@ def test_parse_block_dat_basic():
assert b.codes == ["600000", "000001"]
@patch("xmtdx.client.TdxConnection")
@patch("easy_tdx.client.TdxConnection")
def test_get_block_info_logic(mock_conn_cls):
"""测试 TdxClient.get_block_info 的分片拉取逻辑。"""
mock_conn = mock_conn_cls.return_value
@@ -80,7 +80,7 @@ def test_get_block_info_logic(mock_conn_cls):
# 模拟 GetBlockInfoMeta 响应:size=35000 (需要2次拉取)
def mock_execute(cmd):
from xmtdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
from easy_tdx.commands.block_info import GetBlockInfoCmd, GetBlockInfoMetaCmd
if isinstance(cmd, GetBlockInfoMetaCmd):
return 35000, "dummy_hash"
if isinstance(cmd, GetBlockInfoCmd):
@@ -91,7 +91,7 @@ def test_get_block_info_logic(mock_conn_cls):
mock_conn.execute.side_effect = mock_execute
# 我们主要测试循环是否正确
with patch("xmtdx.client.parse_block_dat") as mock_parse:
with patch("easy_tdx.client.parse_block_dat") as mock_parse:
mock_parse.return_value = [TdxBlock("Test", 1, 0, [])]
res = client.get_block_info("test.dat")
+1 -1
View File
@@ -2,7 +2,7 @@
import struct
from xmtdx.codec.datetime_ import get_datetime, get_datetime_day, get_datetime_minute, get_time
from easy_tdx.codec.datetime_ import get_datetime, get_datetime_day, get_datetime_minute, get_time
def _pack_minute(year: int, month: int, day: int, hour: int, minute: int) -> bytes:
+1 -1
View File
@@ -3,7 +3,7 @@
import struct
import zlib
from xmtdx.codec.frame import HEADER_SIZE, decompress_body, parse_header
from easy_tdx.codec.frame import HEADER_SIZE, decompress_body, parse_header
def _make_header(zipsize: int, unzipsize: int) -> bytes:
+1 -1
View File
@@ -1,6 +1,6 @@
"""get_price / put_price 单元测试,测试向量来自 pytdx 实际报文。"""
from xmtdx.codec.price import get_price, put_price
from easy_tdx.codec.price import get_price, put_price
class TestGetPrice:
+2 -2
View File
@@ -2,7 +2,7 @@
import struct
from xmtdx.codec.volume import get_volume
from easy_tdx.codec.volume import get_volume
def _pack(ivol: int) -> bytes:
@@ -18,7 +18,7 @@ class TestGetVolume:
def test_known_value_4098(self):
# pytdx 注释 "4098 ---> 3.0" 含义:raw 4098 对应真实股数 3.0亿,
# 但 get_volume(4098) ≈ 5.88e-39(接近零),说明 xdxr_info 里对股本字段
# 调用 get_volume 是错误用法。xmtdx 在 xdxr_info 命令中会用正确的解码方式。
# 调用 get_volume 是错误用法。easy-tdx 在 xdxr_info 命令中会用正确的解码方式。
val, pos = get_volume(_pack(4098), 0)
assert abs(val) < 1e-30 # 接近零,与 pytdx 行为一致
+30 -30
View File
@@ -21,8 +21,8 @@ def load_hex(name: str) -> bytes:
# ---------------------------------------------------------------------------
def test_security_count_parse():
from xmtdx.commands.security_count import GetSecurityCountCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.security_count import GetSecurityCountCmd
from easy_tdx.models.enums import Market
body = load_hex("security_count")
cmd = GetSecurityCountCmd(Market.SH)
@@ -39,8 +39,8 @@ def test_security_count_parse():
# ---------------------------------------------------------------------------
def test_security_list_parse():
from xmtdx.commands.security_list import GetSecurityListCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.security_list import GetSecurityListCmd
from easy_tdx.models.enums import Market
body = load_hex("security_list")
cmd = GetSecurityListCmd(Market.SH, 0)
@@ -58,8 +58,8 @@ def test_security_list_parse():
def test_security_list_pre_close_uses_tdx_float_for_a_share():
from xmtdx.commands.security_list import GetSecurityListCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.security_list import GetSecurityListCmd
from easy_tdx.models.enums import Market
body = (
struct.pack("<H", 1)
@@ -84,8 +84,8 @@ def test_security_list_pre_close_uses_tdx_float_for_a_share():
def test_security_list_gbk_no_crash():
"""Bug #2 修复验证:GBK 解码不崩溃,所有记录均有 code。"""
from xmtdx.commands.security_list import GetSecurityListCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.security_list import GetSecurityListCmd
from easy_tdx.models.enums import Market
body = load_hex("security_list")
cmd = GetSecurityListCmd(Market.SH, 0)
@@ -98,8 +98,8 @@ def test_security_list_gbk_no_crash():
# ---------------------------------------------------------------------------
def test_security_bars_parse():
from xmtdx.commands.security_bars import GetSecurityBarsCmd
from xmtdx.models.enums import KlineCategory, Market
from easy_tdx.commands.security_bars import GetSecurityBarsCmd
from easy_tdx.models.enums import KlineCategory, Market
body = load_hex("security_bars")
cmd = GetSecurityBarsCmd(Market.SH, "600000", KlineCategory.DAY, 0, 5)
@@ -129,8 +129,8 @@ def test_security_bars_parse():
# ---------------------------------------------------------------------------
def test_security_quotes_parse():
from xmtdx.commands.security_quotes import GetSecurityQuotesCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.security_quotes import GetSecurityQuotesCmd
from easy_tdx.models.enums import Market
body = load_hex("security_quotes")
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
@@ -162,8 +162,8 @@ def test_security_quotes_parse():
# ---------------------------------------------------------------------------
def test_minute_time_parse():
from xmtdx.commands.minute_time import GetMinuteTimeDataCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.minute_time import GetMinuteTimeDataCmd
from easy_tdx.models.enums import Market
body = load_hex("minute_time")
cmd = GetMinuteTimeDataCmd(Market.SH, "600000")
@@ -190,8 +190,8 @@ def test_minute_time_parse():
# ---------------------------------------------------------------------------
def test_history_minute_time_parse():
from xmtdx.commands.minute_time import GetHistoryMinuteTimeDataCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.minute_time import GetHistoryMinuteTimeDataCmd
from easy_tdx.models.enums import Market
body = load_hex("history_minute_time")
cmd = GetHistoryMinuteTimeDataCmd(Market.SH, "600000", 20250108)
@@ -211,8 +211,8 @@ def test_history_minute_time_parse():
# ---------------------------------------------------------------------------
def test_transaction_parse():
from xmtdx.commands.transaction import GetTransactionDataCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.transaction import GetTransactionDataCmd
from easy_tdx.models.enums import Market
body = load_hex("transaction")
cmd = GetTransactionDataCmd(Market.SH, "600000", 0, 10)
@@ -240,8 +240,8 @@ def test_transaction_parse():
# ---------------------------------------------------------------------------
def test_history_transaction_parse():
from xmtdx.commands.transaction import GetHistoryTransactionDataCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.transaction import GetHistoryTransactionDataCmd
from easy_tdx.models.enums import Market
body = load_hex("history_transaction")
cmd = GetHistoryTransactionDataCmd(Market.SH, "600000", 20250108, 0, 10)
@@ -267,8 +267,8 @@ def test_history_transaction_parse():
# ---------------------------------------------------------------------------
def test_xdxr_info_parse():
from xmtdx.commands.xdxr_info import GetXdxrInfoCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.xdxr_info import GetXdxrInfoCmd
from easy_tdx.models.enums import Market
body = load_hex("xdxr_info")
cmd = GetXdxrInfoCmd(Market.SH, "600000")
@@ -306,8 +306,8 @@ def test_xdxr_info_parse():
def test_xdxr_info_category_1_normalizes_per_10_share_fields():
from xmtdx.commands.xdxr_info import GetXdxrInfoCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.xdxr_info import GetXdxrInfoCmd
from easy_tdx.models.enums import Market
body = bytearray(b"\x00" * 9)
body.extend(struct.pack("<H", 1))
@@ -330,8 +330,8 @@ def test_xdxr_info_category_1_normalizes_per_10_share_fields():
# ---------------------------------------------------------------------------
def test_finance_info_parse():
from xmtdx.commands.finance_info import GetFinanceInfoCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.finance_info import GetFinanceInfoCmd
from easy_tdx.models.enums import Market
body = load_hex("finance_info")
cmd = GetFinanceInfoCmd(Market.SH, "600000")
@@ -355,8 +355,8 @@ def test_finance_info_parse():
# ---------------------------------------------------------------------------
def test_company_info_category_parse():
from xmtdx.commands.company_info import GetCompanyInfoCategoryCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.company_info import GetCompanyInfoCategoryCmd
from easy_tdx.models.enums import Market
body = load_hex("company_info_category")
cmd = GetCompanyInfoCategoryCmd(Market.SH, "600000")
@@ -376,8 +376,8 @@ def test_company_info_category_parse():
# ---------------------------------------------------------------------------
def test_company_info_content_parse():
from xmtdx.commands.company_info import GetCompanyInfoContentCmd
from xmtdx.models.enums import Market
from easy_tdx.commands.company_info import GetCompanyInfoContentCmd
from easy_tdx.models.enums import Market
body = load_hex("company_info_content")
cmd = GetCompanyInfoContentCmd(Market.SH, "600000", "600000.txt", 0, 11426)
+6 -6
View File
@@ -6,12 +6,12 @@ from pathlib import Path
import pytest
from xmtdx.codec.frame import FrameHeader, decompress_body
from xmtdx.commands.company_info import GetCompanyInfoCategoryCmd
from xmtdx.commands.security_count import GetSecurityCountCmd
from xmtdx.commands.xdxr_info import GetXdxrInfoCmd
from xmtdx.exceptions import TdxDecodeError
from xmtdx.models.enums import Market
from easy_tdx.codec.frame import FrameHeader, decompress_body
from easy_tdx.commands.company_info import GetCompanyInfoCategoryCmd
from easy_tdx.commands.security_count import GetSecurityCountCmd
from easy_tdx.commands.xdxr_info import GetXdxrInfoCmd
from easy_tdx.exceptions import TdxDecodeError
from easy_tdx.models.enums import Market
FIXTURES = Path(__file__).parent.parent / "fixtures"
+3 -3
View File
@@ -2,8 +2,8 @@
import struct
from xmtdx.codec.financial import parse_financial_dat, parse_financial_file_list
from xmtdx.models.finance import FinancialFileInfo, FinancialRecord
from easy_tdx.codec.financial import parse_financial_dat, parse_financial_file_list
from easy_tdx.models.finance import FinancialFileInfo, FinancialRecord
class TestParseFinancialFileList:
@@ -109,7 +109,7 @@ class TestFinancialModels:
assert fi.filesize == 100
def test_record(self) -> None:
from xmtdx.models.enums import Market
from easy_tdx.models.enums import Market
r = FinancialRecord(
code="600519", market=Market.SH, report_date=20260331, fields=[1.0, 2.0]
+3 -3
View File
@@ -3,13 +3,13 @@
import asyncio
from unittest.mock import AsyncMock, patch
from xmtdx import AsyncTdxClient
from easy_tdx import AsyncTdxClient
def test_heartbeat_sends_periodically():
async def run_test():
# 模拟连接和执行
with patch("xmtdx.client.AsyncTdxConnection") as mock_conn_cls:
with patch("easy_tdx.client.AsyncTdxConnection") as mock_conn_cls:
mock_conn = mock_conn_cls.return_value
mock_conn.connect = AsyncMock()
mock_conn.close = AsyncMock()
@@ -40,7 +40,7 @@ def test_heartbeat_sends_periodically():
def test_heartbeat_stops_on_close():
async def run_test():
with patch("xmtdx.client.AsyncTdxConnection") as mock_conn_cls:
with patch("easy_tdx.client.AsyncTdxConnection") as mock_conn_cls:
mock_conn = mock_conn_cls.return_value
mock_conn.connect = AsyncMock()
mock_conn.close = AsyncMock()
+9 -9
View File
@@ -3,12 +3,12 @@
import struct
from unittest.mock import patch
from xmtdx.codec.price_rules import compute_price_limits
from xmtdx.commands.fund_flow import GetHistoryFundFlowCmd
from xmtdx.commands.security_bars import GetSecurityBarsCmd
from xmtdx.commands.security_list import GetSecurityListCmd
from xmtdx.commands.security_quotes import GetSecurityQuotesCmd
from xmtdx.models.enums import KlineCategory, Market
from easy_tdx.codec.price_rules import compute_price_limits
from easy_tdx.commands.fund_flow import GetHistoryFundFlowCmd
from easy_tdx.commands.security_bars import GetSecurityBarsCmd
from easy_tdx.commands.security_list import GetSecurityListCmd
from easy_tdx.commands.security_quotes import GetSecurityQuotesCmd
from easy_tdx.models.enums import KlineCategory, Market
def test_security_bars_exact_layout():
@@ -53,7 +53,7 @@ def test_security_list_request_length():
def test_security_quotes_limit_mapping():
"""验证涨跌停价现在返回 None,且 pre_close 正确。"""
from xmtdx.codec.price import put_price
from easy_tdx.codec.price import put_price
cmd = GetSecurityQuotesCmd([(Market.SH, "600000")])
@@ -96,7 +96,7 @@ def test_security_quotes_limit_mapping():
def test_security_quotes_server_time_format():
"""服务器时间应按“小时 + 百万分之一小时”统一解码。"""
from xmtdx.commands.security_quotes import _format_server_time
from easy_tdx.commands.security_quotes import _format_server_time
assert _format_server_time(9500000) == "09:30:00.000"
assert _format_server_time(14999212) == "14:59:57.163"
@@ -156,7 +156,7 @@ def test_history_fund_flow_uses_uint32_volume_words():
seen.append(raw)
return float(raw)
with patch("xmtdx.commands.fund_flow._decode_volume", side_effect=fake_decode):
with patch("easy_tdx.commands.fund_flow._decode_volume", side_effect=fake_decode):
records = GetHistoryFundFlowCmd(Market.SH, "600000", 0, 1).parse_response(bytes(body))
assert seen == raw_words
+3 -3
View File
@@ -2,8 +2,8 @@
from unittest.mock import patch
from xmtdx.exceptions import TdxConnectionError
from xmtdx.transport.sync import TdxConnection
from easy_tdx.exceptions import TdxConnectionError
from easy_tdx.transport.sync import TdxConnection
class _FakeSocket:
@@ -26,7 +26,7 @@ def test_sync_connection_closes_socket_when_setup_fails() -> None:
sock = _FakeSocket()
conn = TdxConnection("127.0.0.1", port=7709, timeout=0.2)
with patch("xmtdx.transport.sync.socket.socket", return_value=sock), patch.object(
with patch("easy_tdx.transport.sync.socket.socket", return_value=sock), patch.object(
TdxConnection,
"_send_setup",
side_effect=TdxConnectionError("setup failed"),