yjdata 0.0.1__tar.gz

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.
yjdata-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: yjdata
3
+ Version: 0.0.1
4
+ Summary: yjdata SDK for Python
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: polars>=1.30
8
+ Requires-Dist: pyarrow>=25.0.1
yjdata-0.0.1/README.md ADDED
File without changes
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70.0.0", "wheel>=0.36.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "yjdata"
7
+ version = "0.0.1"
8
+ description = "yjdata SDK for Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "polars>=1.30",
13
+ "pyarrow>=25.0.1",
14
+ ]
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src/"]
yjdata-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,19 @@
1
+ from importlib import import_module
2
+ from types import ModuleType
3
+
4
+
5
+ def import_optional_dependency(
6
+ name: str, *, install: str | None = None,
7
+ ) -> ModuleType:
8
+ """Import a dependency with an optional installation requirement hint."""
9
+ root = name.split(".", 1)[0]
10
+ try:
11
+ return import_module(name)
12
+ except ModuleNotFoundError as exc:
13
+ if exc.name != root:
14
+ raise
15
+ message = (
16
+ f"Missing optional dependency '{root}'. "
17
+ f'Install it with: pip install "{install or root}"'
18
+ )
19
+ raise ImportError(message) from exc
@@ -0,0 +1,8 @@
1
+ from .common import (
2
+ normalize_polars_df,
3
+ write_chunked_parquet,
4
+ write_seq_parquet
5
+ )
6
+ from .quote import get_step_quote_fields
7
+ from .snapshot import get_snapshot_l2_fields
8
+ from .trade import get_step_trade_pair_fields, get_step_trade_side_fields
@@ -0,0 +1,245 @@
1
+ import dataclasses
2
+ import enum
3
+ import json
4
+ import polars as pl
5
+ import pyarrow as pa
6
+ import pyarrow.parquet as pq
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from ..dtype import DType
12
+
13
+
14
+ __all__ = [
15
+ 'DataField',
16
+ 'DataScaleType',
17
+
18
+ 'normalize_polars_df',
19
+ 'write_chunked_parquet',
20
+ 'write_seq_parquet'
21
+ ]
22
+
23
+
24
+ _PYARROW_DTYPE_MAP: dict[DType, pa.DataType] = {
25
+ DType.INT8: pa.int8(),
26
+ DType.UINT8: pa.uint8(),
27
+ DType.INT32: pa.int32(),
28
+ DType.INT64: pa.int64(),
29
+ DType.FLOAT64: pa.float64(),
30
+ DType.STRING: pa.string()
31
+ }
32
+
33
+ _POLARS_DTYPE_MAP: dict[DType, Any] = {
34
+ DType.INT8: pl.Int8,
35
+ DType.UINT8: pl.UInt8,
36
+ DType.INT32: pl.Int32,
37
+ DType.INT64: pl.Int64,
38
+ DType.FLOAT64: pl.Float64,
39
+ DType.STRING: pl.String,
40
+ }
41
+
42
+
43
+ class DataScaleType(enum.IntEnum):
44
+ """Scaling factors for data fields."""
45
+ NONE = 0
46
+ PRICE = 1 # scale by 1e6
47
+ AMOUNT = 2 # scale by 1e3
48
+
49
+
50
+ @dataclasses.dataclass
51
+ class DataField:
52
+ """Define a data column and its conversion rules.
53
+
54
+ Parameters
55
+ ----------
56
+ name : str
57
+ Column name.
58
+ dtype : DType
59
+ Target data type.
60
+ allow_empty : bool, default False
61
+ Whether missing or null values are allowed.
62
+ default_value : object, optional
63
+ Value used for missing columns and null entries.
64
+ scale_type : DataScaleType, default DataScaleType.NONE
65
+ Scaling rule applied when scale conversion is enabled.
66
+ """
67
+ name: str
68
+ dtype: DType
69
+ allow_empty: bool = dataclasses.field(default=False)
70
+ default_value: Any = dataclasses.field(default=None)
71
+ scale_type: DataScaleType = dataclasses.field(default=DataScaleType.NONE)
72
+
73
+ @property
74
+ def is_int_type(self):
75
+ return self.dtype in (
76
+ DType.INT8,
77
+ DType.UINT8,
78
+ DType.INT32,
79
+ DType.INT64,
80
+ )
81
+
82
+ @property
83
+ def polars_dtype(self):
84
+ try:
85
+ return _POLARS_DTYPE_MAP[self.dtype]
86
+ except KeyError as error:
87
+ raise ValueError(f"Unsupported dtype '{self.dtype}' for polars conversion.") from error
88
+
89
+ @property
90
+ def pyarrow_dtype(self):
91
+ try:
92
+ return _PYARROW_DTYPE_MAP[self.dtype]
93
+ except KeyError as error:
94
+ raise ValueError(f"Unsupported dtype '{self.dtype}' for pyarrow conversion.") from error
95
+
96
+
97
+ def normalize_polars_df(
98
+ data: pl.DataFrame,
99
+ fields: list[DataField],
100
+ convert_scale: bool = False
101
+ ) -> pl.DataFrame:
102
+ """Normalize column order, types, and optional scaling.
103
+
104
+ Parameters
105
+ ----------
106
+ data : polars.DataFrame
107
+ Input rows.
108
+ fields : list of DataField
109
+ Target columns in output order. Unlisted columns are omitted.
110
+ convert_scale : bool, default False
111
+ Multiply prices by 1e6 and amounts by 1e3 according to scale_type.
112
+
113
+ Returns
114
+ -------
115
+ polars.DataFrame
116
+ Selected columns with defaults, scaling, and type conversions applied.
117
+ Floating-point values are rounded before conversion to integer types.
118
+
119
+ Raises
120
+ ------
121
+ ValueError
122
+ If a required column is missing, contains nulls without a default,
123
+ or has an unsupported target type.
124
+ """
125
+ expressions = []
126
+ column_names = set(data.columns)
127
+
128
+ # Track null checks to report the offending column.
129
+ null_filter_map = {}
130
+
131
+ for field in fields:
132
+ name = field.name
133
+ polars_dtype = field.polars_dtype
134
+ expr = pl.col(name)
135
+
136
+ if name not in column_names:
137
+ if not field.allow_empty and field.default_value is None:
138
+ raise ValueError(f"Missing required column '{name}' which does not allow empty.")
139
+ expr = pl.lit(field.default_value, dtype=polars_dtype).alias(name)
140
+
141
+ column_dtype = data.schema.get(name, polars_dtype)
142
+ if not field.allow_empty and field.default_value is None:
143
+ null_filter_map[name] = pl.col(name).is_null()
144
+
145
+ if field.default_value is not None:
146
+ expr = expr.fill_null(field.default_value)
147
+
148
+ if convert_scale:
149
+ if field.scale_type == DataScaleType.PRICE:
150
+ expr = expr * 1_000_000
151
+ elif field.scale_type == DataScaleType.AMOUNT:
152
+ expr = expr * 1_000
153
+
154
+ if column_dtype in (pl.Float32, pl.Float64) and field.is_int_type:
155
+ expr = expr.round(0).cast(polars_dtype, strict=False)
156
+
157
+ expressions.append(expr.cast(polars_dtype).alias(name))
158
+
159
+ # Check required columns for null values.
160
+ null_filter = pl.lit(False)
161
+ for f in null_filter_map.values():
162
+ null_filter |= f
163
+
164
+ if data.filter(null_filter).height:
165
+ # Identify the column containing null values.
166
+ for name, f in null_filter_map.items():
167
+ if data.filter(f).height:
168
+ raise ValueError(f'Column "{name}" contains null value, but it is marked as not allowed.')
169
+
170
+ return data.select(expressions)
171
+
172
+
173
+ def _normalize_string_type(tb: pa.Table) -> pa.Table:
174
+ columns = []
175
+ for col in tb.schema:
176
+ if pa.types.is_large_string(col.type):
177
+ t = pa.string()
178
+ else:
179
+ t = col.type
180
+ columns.append(pa.field(col.name, t))
181
+ schema = pa.schema(columns)
182
+ return tb.cast(schema)
183
+
184
+
185
+ def write_chunked_parquet(
186
+ df: pl.DataFrame,
187
+ path: str | Path,
188
+ chunked_by: str = 'Symbol',
189
+ sort_chunks: bool = True,
190
+ metadata_key: bytes = b'group_symbols'
191
+ ):
192
+ """Write grouped rows to a single Parquet file.
193
+
194
+ Parameters
195
+ ----------
196
+ df : polars.DataFrame
197
+ Rows to write.
198
+ path : str or pathlib.Path
199
+ Output file path.
200
+ chunked_by : str, default 'Symbol'
201
+ Column used to group rows.
202
+ sort_chunks : bool, default True
203
+ Sort groups by key. Otherwise, use the order returned by Polars.
204
+ metadata_key : bytes, default b'group_symbols'
205
+ Schema metadata key for a JSON mapping from stringified group keys
206
+ to zero-based group indices.
207
+
208
+ Notes
209
+ -----
210
+ Each group is written in a separate write call. Arrow large string
211
+ columns are converted to string columns before writing.
212
+ """
213
+ groups = list(df.group_by(chunked_by))
214
+ if sort_chunks:
215
+ groups.sort(key=lambda g: g[0])
216
+
217
+ base_table = _normalize_string_type(df.head(0).to_arrow())
218
+ metadata = dict(base_table.schema.metadata or {})
219
+ metadata[metadata_key] = ','.join(str(key) for key, _ in groups).encode('utf-8')
220
+
221
+ with pq.ParquetWriter(path, base_table.schema.with_metadata(metadata)) as writer:
222
+ for _, group in groups:
223
+ table = _normalize_string_type(group.to_arrow())
224
+ writer.write_table(table)
225
+
226
+
227
+ def write_seq_parquet(
228
+ df: pl.DataFrame,
229
+ path: str | Path,
230
+ ):
231
+ """Write rows to a Parquet file in their existing order.
232
+
233
+ Parameters
234
+ ----------
235
+ df : polars.DataFrame
236
+ Rows to write without additional grouping or sorting.
237
+ path : str or pathlib.Path
238
+ Output file path.
239
+
240
+ Notes
241
+ -----
242
+ Arrow large string columns are converted to string columns before writing.
243
+ """
244
+ table = _normalize_string_type(df.to_arrow())
245
+ pq.write_table(table, path)
@@ -0,0 +1,30 @@
1
+ from typing import List
2
+
3
+ from .common import DataField, DataScaleType
4
+ from ..dtype import DType
5
+
6
+
7
+ __all__ = [
8
+ 'get_step_quote_fields',
9
+ ]
10
+
11
+
12
+ def get_step_quote_fields() -> List[DataField]:
13
+ return [
14
+ DataField('Symbol', DType.STRING),
15
+ DataField('Exchange', DType.STRING),
16
+ DataField('TradeDate', DType.INT32),
17
+ DataField('Time', DType.INT64),
18
+ DataField('RecvTime', DType.INT64, True),
19
+ DataField('Price', DType.INT64, scale_type=DataScaleType.PRICE),
20
+ DataField('Qty', DType.INT64),
21
+ DataField('ModType', DType.UINT8),
22
+ DataField('PriceType', DType.UINT8, False, 0),
23
+ DataField('OrderType', DType.UINT8, False, 0),
24
+ DataField('Side', DType.UINT8),
25
+ DataField('PositionOffset', DType.UINT8, True),
26
+ DataField('ChannelId', DType.INT64, False, 0),
27
+ DataField('OrderNo', DType.INT64, False, 0),
28
+ DataField('RecNo', DType.INT64, False, 0),
29
+ DataField('MessageNo', DType.INT64, False, 0),
30
+ ]
@@ -0,0 +1,51 @@
1
+ from typing import List
2
+
3
+ from .common import DataField, DataScaleType
4
+ from ..dtype import DType
5
+
6
+
7
+ __all__ = [
8
+ 'get_snapshot_l2_fields'
9
+ ]
10
+
11
+
12
+ def get_snapshot_l2_fields() -> List[DataField]:
13
+ required_level_fields = []
14
+ for prefix in ['Bid', 'BidQty', 'Ask', 'AskQty']:
15
+ for i in range(1, 11):
16
+ if prefix.endswith('Qty'):
17
+ required_level_fields.append(DataField(f'{prefix}{i}', DType.INT64, True, 0))
18
+ else:
19
+ required_level_fields.append(DataField(f'{prefix}{i}', DType.INT64, True, scale_type=DataScaleType.PRICE))
20
+
21
+ return [
22
+ DataField('Symbol', DType.STRING),
23
+ DataField('Exchange', DType.STRING),
24
+ DataField('TradeDate', DType.INT32),
25
+ DataField('Time', DType.INT64),
26
+ DataField('RecvTime', DType.INT64, True),
27
+ DataField('Phase', DType.UINT8, True),
28
+ DataField('PreClose', DType.INT64, True, scale_type=DataScaleType.PRICE),
29
+ DataField('PreSettle', DType.INT64, True, scale_type=DataScaleType.PRICE),
30
+ DataField('PreOpenInterest', DType.INT64, True),
31
+ DataField('Open', DType.INT64, True, scale_type=DataScaleType.PRICE),
32
+ DataField('High', DType.INT64, True, scale_type=DataScaleType.PRICE),
33
+ DataField('Low', DType.INT64, True, scale_type=DataScaleType.PRICE),
34
+ DataField('Last', DType.INT64, scale_type=DataScaleType.PRICE),
35
+ DataField('Qty', DType.INT64),
36
+ DataField('Amount', DType.INT64, scale_type=DataScaleType.AMOUNT),
37
+ *required_level_fields,
38
+ DataField('UpperLimit', DType.INT64, True, scale_type=DataScaleType.PRICE),
39
+ DataField('LowerLimit', DType.INT64, True, scale_type=DataScaleType.PRICE),
40
+ DataField('OpenInterest', DType.INT64, True),
41
+ DataField('TradeCount', DType.INT64, True),
42
+ DataField('BidQty', DType.INT64, True),
43
+ DataField('AskQty', DType.INT64, True),
44
+ DataField('BidCount', DType.INT64, True),
45
+ DataField('AskCount', DType.INT64, True),
46
+ DataField('BidAvg', DType.INT64, True, scale_type=DataScaleType.PRICE),
47
+ DataField('AskAvg', DType.INT64, True, scale_type=DataScaleType.PRICE),
48
+ DataField('Close', DType.INT64, True, scale_type=DataScaleType.PRICE),
49
+ DataField('Settle', DType.INT64, True, scale_type=DataScaleType.PRICE),
50
+ DataField('MessageNo', DType.INT64, False, 0),
51
+ ]
@@ -0,0 +1,45 @@
1
+ from typing import List
2
+
3
+ from .common import DataField, DataScaleType
4
+ from ..dtype import DType
5
+
6
+
7
+ __all__ = [
8
+ 'get_step_trade_side_fields',
9
+ 'get_step_trade_pair_fields'
10
+ ]
11
+
12
+
13
+ def get_step_trade_side_fields() -> List[DataField]:
14
+ return [
15
+ DataField('Symbol', DType.STRING),
16
+ DataField('Exchange', DType.STRING),
17
+ DataField('TradeDate', DType.INT32),
18
+ DataField('Time', DType.INT64),
19
+ DataField('RecvTime', DType.INT64, True),
20
+ DataField('Price', DType.INT64, scale_type=DataScaleType.PRICE),
21
+ DataField('Qty', DType.INT64),
22
+ DataField('OrderId', DType.INT64, False, 0),
23
+ DataField('Side', DType.UINT8),
24
+ DataField('PairNo', DType.INT64, False, 0),
25
+ DataField('ChannelId', DType.INT64, False, 0),
26
+ DataField('RecNo', DType.INT64, False, 0),
27
+ DataField('MessageNo', DType.INT64, False, 0),
28
+ ]
29
+
30
+
31
+ def get_step_trade_pair_fields() -> List[DataField]:
32
+ return [
33
+ DataField('Symbol', DType.STRING),
34
+ DataField('Exchange', DType.STRING),
35
+ DataField('TradeDate', DType.INT32),
36
+ DataField('Time', DType.INT64),
37
+ DataField('RecvTime', DType.INT64, True),
38
+ DataField('Price', DType.INT64, scale_type=DataScaleType.PRICE),
39
+ DataField('Qty', DType.INT64),
40
+ DataField('BidOrderId', DType.INT64, False, 0),
41
+ DataField('AskOrderId', DType.INT64, False, 0),
42
+ DataField('ChannelId', DType.INT64, False, 0),
43
+ DataField('RecNo', DType.INT64, False, 0),
44
+ DataField('MessageNo', DType.INT64, False, 0),
45
+ ]
@@ -0,0 +1,15 @@
1
+ from .adapter import DbAdapter
2
+ from .expression import Expression, col, lit
3
+ from .sql import SqlAdapter
4
+ from .mysql import MysqlAdapter
5
+ from .postgres import PostgresAdapter
6
+
7
+ __all__ = [
8
+ "DbAdapter",
9
+ "SqlAdapter",
10
+ "Expression",
11
+ "col",
12
+ "lit",
13
+ "PostgresAdapter",
14
+ "MysqlAdapter"
15
+ ]