sec-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sec_core/__init__.py +3 -0
- sec_core/market_data/__init__.py +66 -0
- sec_core/market_data/codec.py +19 -0
- sec_core/market_data/constants.py +11 -0
- sec_core/market_data/message_ids.py +38 -0
- sec_core/market_data/schemas.py +151 -0
- sec_core/market_data/subjects.py +223 -0
- sec_core/market_data/validators.py +43 -0
- sec_core-0.1.0.dist-info/METADATA +42 -0
- sec_core-0.1.0.dist-info/RECORD +12 -0
- sec_core-0.1.0.dist-info/WHEEL +4 -0
- sec_core-0.1.0.dist-info/licenses/LICENSE +21 -0
sec_core/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Market-data message protocol helpers."""
|
|
2
|
+
|
|
3
|
+
from sec_core.market_data.codec import decode_msgpack, encode_msgpack
|
|
4
|
+
from sec_core.market_data.constants import (
|
|
5
|
+
BACKFILL_SOURCE,
|
|
6
|
+
DEFAULT_KLINE_PERIOD,
|
|
7
|
+
DEFAULT_KLINE_SCHEMA_VERSION,
|
|
8
|
+
DEFAULT_TICK_SCHEMA_VERSION,
|
|
9
|
+
KLINE_1M_V1_DATASET,
|
|
10
|
+
REALTIME_SOURCE,
|
|
11
|
+
TICK_V2_DATASET,
|
|
12
|
+
)
|
|
13
|
+
from sec_core.market_data.message_ids import (
|
|
14
|
+
backfill_kline_1m_v1_msg_id,
|
|
15
|
+
backfill_tick_v2_msg_id,
|
|
16
|
+
kline_1m_v1_msg_id,
|
|
17
|
+
message_id_for_subject,
|
|
18
|
+
tick_v2_msg_id,
|
|
19
|
+
)
|
|
20
|
+
from sec_core.market_data.subjects import (
|
|
21
|
+
ParsedSubject,
|
|
22
|
+
format_backfill_kline_subject,
|
|
23
|
+
format_backfill_tick_subject,
|
|
24
|
+
format_kline_subject,
|
|
25
|
+
format_tick_subject,
|
|
26
|
+
normalize_subject_prefix,
|
|
27
|
+
parse_market_data_subject,
|
|
28
|
+
parse_stock_code,
|
|
29
|
+
subject_for_prefix,
|
|
30
|
+
wildcard_for_prefix,
|
|
31
|
+
)
|
|
32
|
+
from sec_core.market_data.validators import (
|
|
33
|
+
validate_kline_1m_v1_payload,
|
|
34
|
+
validate_payload,
|
|
35
|
+
validate_tick_v2_payload,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"BACKFILL_SOURCE",
|
|
40
|
+
"DEFAULT_KLINE_PERIOD",
|
|
41
|
+
"DEFAULT_KLINE_SCHEMA_VERSION",
|
|
42
|
+
"DEFAULT_TICK_SCHEMA_VERSION",
|
|
43
|
+
"KLINE_1M_V1_DATASET",
|
|
44
|
+
"ParsedSubject",
|
|
45
|
+
"REALTIME_SOURCE",
|
|
46
|
+
"TICK_V2_DATASET",
|
|
47
|
+
"backfill_kline_1m_v1_msg_id",
|
|
48
|
+
"backfill_tick_v2_msg_id",
|
|
49
|
+
"decode_msgpack",
|
|
50
|
+
"encode_msgpack",
|
|
51
|
+
"format_backfill_kline_subject",
|
|
52
|
+
"format_backfill_tick_subject",
|
|
53
|
+
"format_kline_subject",
|
|
54
|
+
"format_tick_subject",
|
|
55
|
+
"kline_1m_v1_msg_id",
|
|
56
|
+
"message_id_for_subject",
|
|
57
|
+
"normalize_subject_prefix",
|
|
58
|
+
"parse_market_data_subject",
|
|
59
|
+
"parse_stock_code",
|
|
60
|
+
"subject_for_prefix",
|
|
61
|
+
"tick_v2_msg_id",
|
|
62
|
+
"validate_kline_1m_v1_payload",
|
|
63
|
+
"validate_payload",
|
|
64
|
+
"validate_tick_v2_payload",
|
|
65
|
+
"wildcard_for_prefix",
|
|
66
|
+
]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""MessagePack codec helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import msgpack
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def encode_msgpack(payload: Mapping[str, Any]) -> bytes:
|
|
10
|
+
"""Encode payload using the shared MessagePack options."""
|
|
11
|
+
return msgpack.packb(dict(payload), use_bin_type=True, default=str)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def decode_msgpack(data: bytes) -> dict[str, Any]:
|
|
15
|
+
"""Decode MessagePack payload using the shared options."""
|
|
16
|
+
decoded = msgpack.unpackb(data, raw=False)
|
|
17
|
+
if not isinstance(decoded, dict):
|
|
18
|
+
raise ValueError("MessagePack payload must decode to a dict")
|
|
19
|
+
return decoded
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Market-data protocol constants."""
|
|
2
|
+
|
|
3
|
+
TICK_V2_DATASET = "tick_v2"
|
|
4
|
+
KLINE_1M_V1_DATASET = "kl_1m_v1"
|
|
5
|
+
|
|
6
|
+
DEFAULT_TICK_SCHEMA_VERSION = "v2"
|
|
7
|
+
DEFAULT_KLINE_SCHEMA_VERSION = "v1"
|
|
8
|
+
DEFAULT_KLINE_PERIOD = "1m"
|
|
9
|
+
|
|
10
|
+
REALTIME_SOURCE = "realtime"
|
|
11
|
+
BACKFILL_SOURCE = "backfill"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""NATS message-id helpers for JetStream de-duplication."""
|
|
2
|
+
|
|
3
|
+
from sec_core.market_data.constants import BACKFILL_SOURCE, KLINE_1M_V1_DATASET, TICK_V2_DATASET
|
|
4
|
+
from sec_core.market_data.subjects import parse_market_data_subject
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def tick_v2_msg_id(stock_code: str, time_ms: int) -> str:
|
|
8
|
+
"""Return realtime tick v2 message id."""
|
|
9
|
+
return f"tick:v2:{stock_code}:{int(time_ms)}"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def backfill_tick_v2_msg_id(stock_code: str, time_ms: int) -> str:
|
|
13
|
+
"""Return backfill tick v2 message id."""
|
|
14
|
+
return f"backfill:tick:v2:{stock_code}:{int(time_ms)}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def kline_1m_v1_msg_id(stock_code: str, time_ms: int) -> str:
|
|
18
|
+
"""Return realtime 1m K-line v1 message id."""
|
|
19
|
+
return f"1m:{stock_code}:{int(time_ms)}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def backfill_kline_1m_v1_msg_id(stock_code: str, time_ms: int) -> str:
|
|
23
|
+
"""Return backfill 1m K-line v1 message id."""
|
|
24
|
+
return f"backfill:kl:1m:{stock_code}:{int(time_ms)}"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def message_id_for_subject(subject: str, stock_code: str, time_ms: int) -> str:
|
|
28
|
+
"""Return message id matching a market-data subject."""
|
|
29
|
+
parsed = parse_market_data_subject(subject)
|
|
30
|
+
if parsed.dataset == TICK_V2_DATASET:
|
|
31
|
+
if parsed.source == BACKFILL_SOURCE:
|
|
32
|
+
return backfill_tick_v2_msg_id(stock_code, time_ms)
|
|
33
|
+
return tick_v2_msg_id(stock_code, time_ms)
|
|
34
|
+
if parsed.dataset == KLINE_1M_V1_DATASET:
|
|
35
|
+
if parsed.source == BACKFILL_SOURCE:
|
|
36
|
+
return backfill_kline_1m_v1_msg_id(stock_code, time_ms)
|
|
37
|
+
return kline_1m_v1_msg_id(stock_code, time_ms)
|
|
38
|
+
raise ValueError(f"Unsupported dataset: {parsed.dataset}")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Market-data schema field definitions."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from sec_core.market_data.constants import KLINE_1M_V1_DATASET, TICK_V2_DATASET
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FieldType(str, Enum):
|
|
9
|
+
"""Logical field type without storage-engine dependencies."""
|
|
10
|
+
|
|
11
|
+
STRING = "string"
|
|
12
|
+
BOOL = "bool"
|
|
13
|
+
PRICE = "price"
|
|
14
|
+
AMOUNT = "amount"
|
|
15
|
+
INT64 = "int64"
|
|
16
|
+
INT32 = "int32"
|
|
17
|
+
INT8 = "int8"
|
|
18
|
+
FLOAT = "float"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
TICK_V2_FIELD_TYPES: dict[str, FieldType] = {
|
|
22
|
+
"schema_version": FieldType.INT32,
|
|
23
|
+
"market": FieldType.STRING,
|
|
24
|
+
"stock_code": FieldType.STRING,
|
|
25
|
+
"time_ms": FieldType.INT64,
|
|
26
|
+
"last_price": FieldType.PRICE,
|
|
27
|
+
"open": FieldType.PRICE,
|
|
28
|
+
"high": FieldType.PRICE,
|
|
29
|
+
"low": FieldType.PRICE,
|
|
30
|
+
"last_close": FieldType.PRICE,
|
|
31
|
+
"amount": FieldType.AMOUNT,
|
|
32
|
+
"volume": FieldType.INT64,
|
|
33
|
+
"pvolume": FieldType.INT64,
|
|
34
|
+
"stock_status": FieldType.INT8,
|
|
35
|
+
"open_int": FieldType.INT32,
|
|
36
|
+
"transaction_num": FieldType.INT32,
|
|
37
|
+
"pe": FieldType.FLOAT,
|
|
38
|
+
"vol_ratio": FieldType.FLOAT,
|
|
39
|
+
"speed_1_min": FieldType.FLOAT,
|
|
40
|
+
"speed_5_min": FieldType.FLOAT,
|
|
41
|
+
"settlement_price": FieldType.PRICE,
|
|
42
|
+
"last_settlement_price": FieldType.PRICE,
|
|
43
|
+
"bid_price_1": FieldType.PRICE,
|
|
44
|
+
"bid_price_2": FieldType.PRICE,
|
|
45
|
+
"bid_price_3": FieldType.PRICE,
|
|
46
|
+
"bid_price_4": FieldType.PRICE,
|
|
47
|
+
"bid_price_5": FieldType.PRICE,
|
|
48
|
+
"ask_price_1": FieldType.PRICE,
|
|
49
|
+
"ask_price_2": FieldType.PRICE,
|
|
50
|
+
"ask_price_3": FieldType.PRICE,
|
|
51
|
+
"ask_price_4": FieldType.PRICE,
|
|
52
|
+
"ask_price_5": FieldType.PRICE,
|
|
53
|
+
"bid_vol_1": FieldType.INT32,
|
|
54
|
+
"bid_vol_2": FieldType.INT32,
|
|
55
|
+
"bid_vol_3": FieldType.INT32,
|
|
56
|
+
"bid_vol_4": FieldType.INT32,
|
|
57
|
+
"bid_vol_5": FieldType.INT32,
|
|
58
|
+
"ask_vol_1": FieldType.INT32,
|
|
59
|
+
"ask_vol_2": FieldType.INT32,
|
|
60
|
+
"ask_vol_3": FieldType.INT32,
|
|
61
|
+
"ask_vol_4": FieldType.INT32,
|
|
62
|
+
"ask_vol_5": FieldType.INT32,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
KLINE_1M_V1_FIELD_TYPES: dict[str, FieldType] = {
|
|
66
|
+
"schema_version": FieldType.INT32,
|
|
67
|
+
"market": FieldType.STRING,
|
|
68
|
+
"stock_code": FieldType.STRING,
|
|
69
|
+
"period": FieldType.STRING,
|
|
70
|
+
"time_ms": FieldType.INT64,
|
|
71
|
+
"open": FieldType.PRICE,
|
|
72
|
+
"high": FieldType.PRICE,
|
|
73
|
+
"low": FieldType.PRICE,
|
|
74
|
+
"close": FieldType.PRICE,
|
|
75
|
+
"volume": FieldType.INT64,
|
|
76
|
+
"amount": FieldType.AMOUNT,
|
|
77
|
+
"settlement_price": FieldType.PRICE,
|
|
78
|
+
"open_interest": FieldType.INT64,
|
|
79
|
+
"pre_close": FieldType.PRICE,
|
|
80
|
+
"suspend_flag": FieldType.INT8,
|
|
81
|
+
"is_final": FieldType.BOOL,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
TICK_V2_REQUIRED_FIELDS = (
|
|
85
|
+
"schema_version",
|
|
86
|
+
"market",
|
|
87
|
+
"stock_code",
|
|
88
|
+
"time_ms",
|
|
89
|
+
"last_price",
|
|
90
|
+
"volume",
|
|
91
|
+
"bid_price_1",
|
|
92
|
+
"bid_vol_1",
|
|
93
|
+
"ask_price_1",
|
|
94
|
+
"ask_vol_1",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
KLINE_1M_V1_REQUIRED_FIELDS = (
|
|
98
|
+
"schema_version",
|
|
99
|
+
"market",
|
|
100
|
+
"stock_code",
|
|
101
|
+
"period",
|
|
102
|
+
"time_ms",
|
|
103
|
+
"open",
|
|
104
|
+
"high",
|
|
105
|
+
"low",
|
|
106
|
+
"close",
|
|
107
|
+
"volume",
|
|
108
|
+
"amount",
|
|
109
|
+
"settlement_price",
|
|
110
|
+
"open_interest",
|
|
111
|
+
"pre_close",
|
|
112
|
+
"suspend_flag",
|
|
113
|
+
"is_final",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
_FIELD_TYPES_BY_DATASET = {
|
|
117
|
+
TICK_V2_DATASET: TICK_V2_FIELD_TYPES,
|
|
118
|
+
KLINE_1M_V1_DATASET: KLINE_1M_V1_FIELD_TYPES,
|
|
119
|
+
}
|
|
120
|
+
_REQUIRED_FIELDS_BY_DATASET = {
|
|
121
|
+
TICK_V2_DATASET: TICK_V2_REQUIRED_FIELDS,
|
|
122
|
+
KLINE_1M_V1_DATASET: KLINE_1M_V1_REQUIRED_FIELDS,
|
|
123
|
+
}
|
|
124
|
+
_SCHEMA_VERSION_BY_DATASET = {
|
|
125
|
+
TICK_V2_DATASET: 2,
|
|
126
|
+
KLINE_1M_V1_DATASET: 1,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def field_types_for_dataset(dataset: str) -> dict[str, FieldType]:
|
|
131
|
+
"""Return field types for a supported dataset."""
|
|
132
|
+
try:
|
|
133
|
+
return _FIELD_TYPES_BY_DATASET[dataset]
|
|
134
|
+
except KeyError as exc:
|
|
135
|
+
raise ValueError(f"Unsupported dataset: {dataset}") from exc
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def required_fields_for_dataset(dataset: str) -> tuple[str, ...]:
|
|
139
|
+
"""Return required payload fields for a supported dataset."""
|
|
140
|
+
try:
|
|
141
|
+
return _REQUIRED_FIELDS_BY_DATASET[dataset]
|
|
142
|
+
except KeyError as exc:
|
|
143
|
+
raise ValueError(f"Unsupported dataset: {dataset}") from exc
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def schema_version_for_dataset(dataset: str) -> int:
|
|
147
|
+
"""Return numeric schema version for a supported dataset."""
|
|
148
|
+
try:
|
|
149
|
+
return _SCHEMA_VERSION_BY_DATASET[dataset]
|
|
150
|
+
except KeyError as exc:
|
|
151
|
+
raise ValueError(f"Unsupported dataset: {dataset}") from exc
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""NATS subject helpers for market-data messages."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from sec_core.market_data.constants import (
|
|
7
|
+
BACKFILL_SOURCE,
|
|
8
|
+
DEFAULT_KLINE_PERIOD,
|
|
9
|
+
DEFAULT_KLINE_SCHEMA_VERSION,
|
|
10
|
+
DEFAULT_TICK_SCHEMA_VERSION,
|
|
11
|
+
KLINE_1M_V1_DATASET,
|
|
12
|
+
REALTIME_SOURCE,
|
|
13
|
+
TICK_V2_DATASET,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
MarketDataSource = Literal["realtime", "backfill"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ParsedSubject:
|
|
21
|
+
"""Parsed market-data subject."""
|
|
22
|
+
|
|
23
|
+
dataset: str
|
|
24
|
+
market: str
|
|
25
|
+
code: str
|
|
26
|
+
stock_code: str
|
|
27
|
+
source: MarketDataSource
|
|
28
|
+
schema_version: str
|
|
29
|
+
period: str | None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_stock_code(stock_code: str) -> tuple[str, str]:
|
|
33
|
+
"""Parse stock code like ``000001.SZ`` into ``("sz", "000001")``."""
|
|
34
|
+
if "." not in stock_code:
|
|
35
|
+
raise ValueError(f"Invalid stock code format: {stock_code}")
|
|
36
|
+
code, exchange = stock_code.split(".", 1)
|
|
37
|
+
market = exchange.lower()
|
|
38
|
+
if market not in {"sz", "sh", "bj"}:
|
|
39
|
+
raise ValueError(f"Unknown exchange: {exchange}")
|
|
40
|
+
if not code:
|
|
41
|
+
raise ValueError(f"Invalid stock code format: {stock_code}")
|
|
42
|
+
return market, code
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def normalize_schema_version(version: str | int) -> str:
|
|
46
|
+
"""Normalize schema version to the ``vN`` form."""
|
|
47
|
+
if isinstance(version, int):
|
|
48
|
+
version = f"v{version}"
|
|
49
|
+
value = str(version).lower()
|
|
50
|
+
normalized = value if value.startswith("v") else f"v{value}"
|
|
51
|
+
if normalized not in {"v1", "v2"}:
|
|
52
|
+
raise ValueError(f"Unsupported schema version: {version}")
|
|
53
|
+
return normalized
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def schema_version_number(version: str | int) -> int:
|
|
57
|
+
"""Return numeric schema version."""
|
|
58
|
+
return int(normalize_schema_version(version).removeprefix("v"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_tick_subject(
|
|
62
|
+
stock_code: str,
|
|
63
|
+
version: str | int = DEFAULT_TICK_SCHEMA_VERSION,
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Format realtime tick subject."""
|
|
66
|
+
market, code = parse_stock_code(stock_code)
|
|
67
|
+
return f"tick.{normalize_schema_version(version)}.{market}.{code}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_backfill_tick_subject(
|
|
71
|
+
stock_code: str,
|
|
72
|
+
version: str | int = DEFAULT_TICK_SCHEMA_VERSION,
|
|
73
|
+
) -> str:
|
|
74
|
+
"""Format historical backfill tick subject."""
|
|
75
|
+
market, code = parse_stock_code(stock_code)
|
|
76
|
+
return f"backfill.tick.{normalize_schema_version(version)}.{market}.{code}"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_kline_subject(
|
|
80
|
+
stock_code: str,
|
|
81
|
+
period: str = DEFAULT_KLINE_PERIOD,
|
|
82
|
+
version: str | int = DEFAULT_KLINE_SCHEMA_VERSION,
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Format realtime K-line subject."""
|
|
85
|
+
market, code = parse_stock_code(stock_code)
|
|
86
|
+
return f"kl.{normalize_schema_version(version)}.{period}.{market}.{code}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def format_backfill_kline_subject(
|
|
90
|
+
stock_code: str,
|
|
91
|
+
period: str = DEFAULT_KLINE_PERIOD,
|
|
92
|
+
version: str | int = DEFAULT_KLINE_SCHEMA_VERSION,
|
|
93
|
+
) -> str:
|
|
94
|
+
"""Format historical backfill K-line subject."""
|
|
95
|
+
market, code = parse_stock_code(stock_code)
|
|
96
|
+
return f"backfill.kl.{normalize_schema_version(version)}.{period}.{market}.{code}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def normalize_subject_prefix(prefix: str) -> str:
|
|
100
|
+
"""Normalize a subject prefix by stripping whitespace and dots."""
|
|
101
|
+
normalized = prefix.strip().strip(".")
|
|
102
|
+
if not normalized:
|
|
103
|
+
raise ValueError("subject prefix must not be empty")
|
|
104
|
+
return normalized
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def wildcard_for_prefix(prefix: str) -> str:
|
|
108
|
+
"""Return wildcard subject for a prefix."""
|
|
109
|
+
return f"{normalize_subject_prefix(prefix)}.>"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def subject_for_prefix(prefix: str, market: str, code: str) -> str:
|
|
113
|
+
"""Return concrete subject for a prefix, market, and code."""
|
|
114
|
+
return f"{normalize_subject_prefix(prefix)}.{market}.{code}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def parse_market_data_subject(
|
|
118
|
+
subject: str,
|
|
119
|
+
*,
|
|
120
|
+
tick_prefix: str = "tick.v2",
|
|
121
|
+
kline_prefix: str = "kl.v1.1m",
|
|
122
|
+
) -> ParsedSubject:
|
|
123
|
+
"""Parse realtime or backfill market-data subject."""
|
|
124
|
+
subject = subject.strip()
|
|
125
|
+
for parser in (_parse_tick_subject, _parse_kline_subject):
|
|
126
|
+
parsed = parser(subject)
|
|
127
|
+
if parsed is not None:
|
|
128
|
+
return parsed
|
|
129
|
+
for dataset, prefix in (
|
|
130
|
+
(TICK_V2_DATASET, normalize_subject_prefix(tick_prefix)),
|
|
131
|
+
(KLINE_1M_V1_DATASET, normalize_subject_prefix(kline_prefix)),
|
|
132
|
+
):
|
|
133
|
+
parsed = _parse_by_prefix(subject, dataset, prefix)
|
|
134
|
+
if parsed is not None:
|
|
135
|
+
return parsed
|
|
136
|
+
raise ValueError(f"Invalid market data subject: {subject}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _parse_tick_subject(subject: str) -> ParsedSubject | None:
|
|
140
|
+
parts = subject.split(".")
|
|
141
|
+
if len(parts) == 4 and parts[0] == "tick":
|
|
142
|
+
_, version, market, code = parts
|
|
143
|
+
if version == "v2":
|
|
144
|
+
return _parsed_tick(market, code, REALTIME_SOURCE, version)
|
|
145
|
+
if len(parts) == 5 and parts[:2] == ["backfill", "tick"]:
|
|
146
|
+
_, _, version, market, code = parts
|
|
147
|
+
if version == "v2":
|
|
148
|
+
return _parsed_tick(market, code, BACKFILL_SOURCE, version)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _parse_kline_subject(subject: str) -> ParsedSubject | None:
|
|
153
|
+
parts = subject.split(".")
|
|
154
|
+
if len(parts) == 5 and parts[0] == "kl":
|
|
155
|
+
_, version, period, market, code = parts
|
|
156
|
+
if version == "v1" and period == "1m":
|
|
157
|
+
return _parsed_kline(market, code, REALTIME_SOURCE, version, period)
|
|
158
|
+
if len(parts) == 6 and parts[:2] == ["backfill", "kl"]:
|
|
159
|
+
_, _, version, period, market, code = parts
|
|
160
|
+
if version == "v1" and period == "1m":
|
|
161
|
+
return _parsed_kline(market, code, BACKFILL_SOURCE, version, period)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _parse_by_prefix(
|
|
166
|
+
subject: str,
|
|
167
|
+
dataset: str,
|
|
168
|
+
prefix: str,
|
|
169
|
+
) -> ParsedSubject | None:
|
|
170
|
+
needle = f"{prefix}."
|
|
171
|
+
if not subject.startswith(needle):
|
|
172
|
+
return None
|
|
173
|
+
suffix = subject[len(needle):]
|
|
174
|
+
parts = suffix.split(".")
|
|
175
|
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
176
|
+
raise ValueError(f"Invalid market data subject: {subject}")
|
|
177
|
+
market, code = parts
|
|
178
|
+
source = BACKFILL_SOURCE if prefix.startswith("backfill.") else REALTIME_SOURCE
|
|
179
|
+
if dataset == TICK_V2_DATASET:
|
|
180
|
+
return _parsed_tick(market, code, source, "v2")
|
|
181
|
+
return _parsed_kline(market, code, source, "v1", "1m")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _parsed_tick(
|
|
185
|
+
market: str,
|
|
186
|
+
code: str,
|
|
187
|
+
source: MarketDataSource,
|
|
188
|
+
version: str,
|
|
189
|
+
) -> ParsedSubject:
|
|
190
|
+
_validate_market_code(market, code)
|
|
191
|
+
return ParsedSubject(
|
|
192
|
+
dataset=TICK_V2_DATASET,
|
|
193
|
+
market=market.lower(),
|
|
194
|
+
code=code,
|
|
195
|
+
stock_code=f"{code}.{market.lower()}",
|
|
196
|
+
source=source,
|
|
197
|
+
schema_version=version,
|
|
198
|
+
period=None,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _parsed_kline(
|
|
203
|
+
market: str,
|
|
204
|
+
code: str,
|
|
205
|
+
source: MarketDataSource,
|
|
206
|
+
version: str,
|
|
207
|
+
period: str,
|
|
208
|
+
) -> ParsedSubject:
|
|
209
|
+
_validate_market_code(market, code)
|
|
210
|
+
return ParsedSubject(
|
|
211
|
+
dataset=KLINE_1M_V1_DATASET,
|
|
212
|
+
market=market.lower(),
|
|
213
|
+
code=code,
|
|
214
|
+
stock_code=f"{code}.{market.upper()}",
|
|
215
|
+
source=source,
|
|
216
|
+
schema_version=version,
|
|
217
|
+
period=period,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _validate_market_code(market: str, code: str) -> None:
|
|
222
|
+
if market.lower() not in {"sz", "sh", "bj"} or not code:
|
|
223
|
+
raise ValueError(f"Invalid market data subject token: {market}.{code}")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Payload validators for market-data schemas."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from sec_core.market_data.constants import DEFAULT_KLINE_PERIOD, KLINE_1M_V1_DATASET, TICK_V2_DATASET
|
|
7
|
+
from sec_core.market_data.schemas import (
|
|
8
|
+
KLINE_1M_V1_REQUIRED_FIELDS,
|
|
9
|
+
TICK_V2_REQUIRED_FIELDS,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def validate_tick_v2_payload(payload: Mapping[str, Any]) -> None:
|
|
14
|
+
"""Validate a tick v2 payload contract."""
|
|
15
|
+
_require_fields(payload, TICK_V2_REQUIRED_FIELDS)
|
|
16
|
+
if int(payload["schema_version"]) != 2:
|
|
17
|
+
raise ValueError("tick v2 schema_version must be 2")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_kline_1m_v1_payload(payload: Mapping[str, Any]) -> None:
|
|
21
|
+
"""Validate a 1m K-line v1 payload contract."""
|
|
22
|
+
_require_fields(payload, KLINE_1M_V1_REQUIRED_FIELDS)
|
|
23
|
+
if int(payload["schema_version"]) != 1:
|
|
24
|
+
raise ValueError("kl 1m v1 schema_version must be 1")
|
|
25
|
+
if payload["period"] != DEFAULT_KLINE_PERIOD:
|
|
26
|
+
raise ValueError("kl 1m v1 period must be 1m")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate_payload(dataset: str, payload: Mapping[str, Any]) -> None:
|
|
30
|
+
"""Validate payload for a supported dataset."""
|
|
31
|
+
if dataset == TICK_V2_DATASET:
|
|
32
|
+
validate_tick_v2_payload(payload)
|
|
33
|
+
return
|
|
34
|
+
if dataset == KLINE_1M_V1_DATASET:
|
|
35
|
+
validate_kline_1m_v1_payload(payload)
|
|
36
|
+
return
|
|
37
|
+
raise ValueError(f"Unsupported dataset: {dataset}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _require_fields(payload: Mapping[str, Any], fields: tuple[str, ...]) -> None:
|
|
41
|
+
missing = [field for field in fields if field not in payload]
|
|
42
|
+
if missing:
|
|
43
|
+
raise ValueError(f"Missing required fields: {', '.join(missing)}")
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sec-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared securities market-data protocol contracts
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: msgpack>=1.1.2
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# sec-core
|
|
11
|
+
|
|
12
|
+
`sec-core` 是证券行情消息协议核心包,提供 subject 构造/解析、schema 常量、payload 校验、`Nats-Msg-Id` 生成和 MessagePack 编解码。
|
|
13
|
+
|
|
14
|
+
安装:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv add sec-core
|
|
18
|
+
pip install sec-core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
示例:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from sec_core.market_data.subjects import format_tick_subject
|
|
25
|
+
|
|
26
|
+
print(format_tick_subject("000001.SZ"))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
输出:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
tick.v2.sz.000001
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
第一版支持:
|
|
36
|
+
|
|
37
|
+
- `tick.v2.{market}.{code}`
|
|
38
|
+
- `backfill.tick.v2.{market}.{code}`
|
|
39
|
+
- `kl.v1.1m.{market}.{code}`
|
|
40
|
+
- `backfill.kl.v1.1m.{market}.{code}`
|
|
41
|
+
|
|
42
|
+
本包不依赖 `xtquant`、`nats-py` 或 `pyarrow`。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
sec_core/__init__.py,sha256=ZmkVoRzqCIjvtYJHiqN1Lr5Rl_q7CdwcNUMepfPRHFE,67
|
|
2
|
+
sec_core/market_data/__init__.py,sha256=sNCoW_j1sFxvZvcM0_uaeYMyFjtER2d7SX1HJl352N8,1758
|
|
3
|
+
sec_core/market_data/codec.py,sha256=NtN-KLfCwxT7075Iyq226RATgGF3ms7NEnsLUE_N860,593
|
|
4
|
+
sec_core/market_data/constants.py,sha256=NIL3LJFzsfiuZw9uK-xzZRHYA2oitHfeFFzwrbEU2dQ,259
|
|
5
|
+
sec_core/market_data/message_ids.py,sha256=b3MqSEJRFUV8ESIl7kFdftHRmVL5SUYOxQPf7ATqldM,1566
|
|
6
|
+
sec_core/market_data/schemas.py,sha256=el_Bk3vcCHMt6o9jngUVeVd9GIAiTuqLfVQQPkARRac,4142
|
|
7
|
+
sec_core/market_data/subjects.py,sha256=ZhJ3T9-5HW34yyI2Tmqiuy51-hBoXPeGgPCVFq-dT6k,7058
|
|
8
|
+
sec_core/market_data/validators.py,sha256=WSOVasSW0H6EpZ1XUVXxLjHNZ4sFZLF9BxjIDJFs5KE,1602
|
|
9
|
+
sec_core-0.1.0.dist-info/METADATA,sha256=hCjwfrooXMRzQF0v7PZsv6PmNGTbuZMHJrzNYgyhQvs,843
|
|
10
|
+
sec_core-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
11
|
+
sec_core-0.1.0.dist-info/licenses/LICENSE,sha256=dcEDwJI-xKgeyZMSoFiV-ceuHq-YNPOhqP91paUwWXQ,1086
|
|
12
|
+
sec_core-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chonglie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|