vikron-nautilus 0.1.0__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.
- vikron_nautilus-0.1.0/.gitignore +13 -0
- vikron_nautilus-0.1.0/LICENSE +21 -0
- vikron_nautilus-0.1.0/PKG-INFO +212 -0
- vikron_nautilus-0.1.0/README.md +196 -0
- vikron_nautilus-0.1.0/pyproject.toml +26 -0
- vikron_nautilus-0.1.0/python/vikron_nautilus/__init__.py +12 -0
- vikron_nautilus-0.1.0/python/vikron_nautilus/_compat.py +17 -0
- vikron_nautilus-0.1.0/python/vikron_nautilus/catalog.py +86 -0
- vikron_nautilus-0.1.0/python/vikron_nautilus/data.py +142 -0
- vikron_nautilus-0.1.0/tests/test_catalog.py +72 -0
- vikron_nautilus-0.1.0/tests/test_compat.py +40 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 vikron-data
|
|
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.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vikron-nautilus
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Read Vikron Catalog data with NautilusTrader Python
|
|
5
|
+
Project-URL: Repository, https://gitee.com/vikron-data/vikron-nautilus
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: packaging>=24
|
|
10
|
+
Requires-Dist: pyarrow<27,>=21
|
|
11
|
+
Provides-Extra: v1
|
|
12
|
+
Requires-Dist: nautilus-trader<2.0.0,>=1.226.0; extra == 'v1'
|
|
13
|
+
Provides-Extra: v2
|
|
14
|
+
Requires-Dist: nautilus-trader<2.1.0,>=2.0.0rc6; extra == 'v2'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# vikron-nautilus
|
|
18
|
+
|
|
19
|
+
在 Python 或 Rust 中读取 Vikron Downloader 生成的 Nautilus Catalog 数据,用于行情分析及 NautilusTrader 策略开发。
|
|
20
|
+
|
|
21
|
+
本项目提供 Python 包和 Rust crate,按使用语言安装其中一个即可。
|
|
22
|
+
|
|
23
|
+
## 使用范围
|
|
24
|
+
|
|
25
|
+
先在 Vikron Downloader 中选择对应的 Catalog 输出格式,再使用本库读取下载后的目录。
|
|
26
|
+
|
|
27
|
+
| 输出格式 | 数据目录 | Python:NautilusTrader 预计兼容范围 | Rust:Nautilus 预计兼容范围 |
|
|
28
|
+
| --- | --- | --- | --- |
|
|
29
|
+
| Catalog v1 | `vikron/catalog-v1` | `>=1.226.0, <2.0.0` | `>=0.64.0, <0.65.0`(`0.64.x`) |
|
|
30
|
+
| Catalog v2 | `vikron/catalog-v2` | `>=2.0.0rc6, <2.1.0` | `>=0.65.0, <0.66.0`(`0.65.x`) |
|
|
31
|
+
|
|
32
|
+
预计兼容范围用于安装依赖和选择 Catalog 版本,不代表范围内所有版本均已测试。Catalog v1 的 Python 下限 `1.226.0` 根据[官方发布说明](https://github.com/nautechsystems/nautilus_trader/blob/develop/RELEASES.md#nautilustrader-12260-beta)中的数据读取和混合回测修复推断;`1.226.0–1.229.0` 尚未进行实际数据读取测试。范围内未来发布的版本也仍需验证。
|
|
33
|
+
|
|
34
|
+
这里的 Python 1.x / 2.x 指 NautilusTrader 包版本,不是 Python 解释器版本。Rust `0.x` 的次版本可能不兼容,因此不跨次版本放宽。
|
|
35
|
+
|
|
36
|
+
已测试版本(macOS ARM64):
|
|
37
|
+
|
|
38
|
+
| 输出格式 | Python:NautilusTrader 已通过测试 | Rust:Nautilus 已通过测试 |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| Catalog v1 | `1.230.0`、`1.231.0` | `0.64.0` |
|
|
41
|
+
| Catalog v2 | `2.0.0rc6` | `0.65.0` |
|
|
42
|
+
|
|
43
|
+
Python 已验证原生及自定义数据的读取、空数据、数值精度和查询过滤;Rust 已验证四种自定义数据通过官方 Catalog 读取。`2.0.0rc5` 经测试缺少所需查询接口,不支持。当前验证的 Python 2.x 仍为预发布版,建议生产环境固定使用已测试版本。
|
|
44
|
+
|
|
45
|
+
支持读取的数据:
|
|
46
|
+
|
|
47
|
+
| 数据 | 类型归属 | Python 类型 | Rust 类型 |
|
|
48
|
+
| --- | --- | --- | --- |
|
|
49
|
+
| 报价 | Nautilus 原生 | `QuoteTick` | `QuoteTick` |
|
|
50
|
+
| 成交 | Nautilus 原生 | `TradeTick` | `TradeTick` |
|
|
51
|
+
| 订单簿增量 | Nautilus 原生 | `OrderBookDelta` | `OrderBookDelta` |
|
|
52
|
+
| 标记价格 | Nautilus 原生 | `MarkPriceUpdate` | `MarkPriceUpdate` |
|
|
53
|
+
| 指数价格 | Nautilus 原生 | `IndexPriceUpdate` | `IndexPriceUpdate` |
|
|
54
|
+
| 资金费率 | Nautilus 原生 | `FundingRateUpdate` | `FundingRateUpdate` |
|
|
55
|
+
| 强平 | Vikron 自定义 | `Liquidation` | `Liquidation` |
|
|
56
|
+
| 持仓量 | Vikron 自定义 | `OpenInterestUpdate` | `OpenInterestUpdate` |
|
|
57
|
+
| 最新价格 | Vikron 自定义 | `LastPriceUpdate` | `LastPriceUpdate` |
|
|
58
|
+
| 预测资金费率 | Vikron 自定义 | `PredictedFundingRateUpdate` | `PredictedFundingRateUpdate` |
|
|
59
|
+
|
|
60
|
+
具体可用的数据取决于 Downloader 的输出。本库不下载行情、不转换 CSV,也不创建或补全交易品种的 `Instrument` 信息。
|
|
61
|
+
|
|
62
|
+
## Python 使用方法
|
|
63
|
+
|
|
64
|
+
### 安装
|
|
65
|
+
|
|
66
|
+
需要 Python 3.12 或以上版本,同时满足对应 NautilusTrader 版本的平台要求。建议为两个版本分别创建虚拟环境,不要混装。
|
|
67
|
+
|
|
68
|
+
使用 Catalog v1:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
python -m pip install "vikron-nautilus[v1]==0.1.0"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
使用 Catalog v2:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
python -m pip install "vikron-nautilus[v2]==0.1.0"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 读取数据
|
|
81
|
+
|
|
82
|
+
下面以 Catalog v2 的强平数据为例。路径指向 Catalog 根目录,不是其中的 `data` 子目录;将路径和品种替换成自己的数据。
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from vikron_nautilus import Catalog, Liquidation
|
|
86
|
+
|
|
87
|
+
catalog = Catalog("./data/vikron/catalog-v2", version=2)
|
|
88
|
+
events = catalog.query(
|
|
89
|
+
Liquidation,
|
|
90
|
+
identifiers=["ETHUSDT-PERP.BINANCE"],
|
|
91
|
+
)
|
|
92
|
+
for event in events:
|
|
93
|
+
print(event.instrument_id, event.price, event.size, event.ts_init)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
使用 v1 时,将路径改成 `./data/vikron/catalog-v1`,并设置 `version=1`。
|
|
97
|
+
|
|
98
|
+
读取原生行情类型时,替换查询类型即可。例如,v2 的报价数据:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from nautilus_trader.model import QuoteTick
|
|
102
|
+
|
|
103
|
+
quotes = catalog.query(QuoteTick, identifiers=["ETHUSDT-PERP.BINANCE"])
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
v1 的原生行情类型从 `nautilus_trader.model.data` 导入:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from nautilus_trader.model.data import QuoteTick
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 限定查询范围
|
|
113
|
+
|
|
114
|
+
`identifiers` 使用完整的 Nautilus `InstrumentId`。`start` 和 `end` 是 Unix 纳秒时间戳,按 `ts_init` 筛选,包含起止边界;省略参数表示不限制该条件。
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
events = catalog.query(
|
|
118
|
+
Liquidation,
|
|
119
|
+
identifiers=["ETHUSDT-PERP.BINANCE"],
|
|
120
|
+
start=1788220800000000000, # 2026-09-01 00:00:00 UTC
|
|
121
|
+
end=1788307199999999999, # 2026-09-01 当天结束(UTC)
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
查询返回对象列表,空数据返回空列表。较大的历史范围建议按日期和品种分批读取。
|
|
126
|
+
|
|
127
|
+
### 使用自定义数据
|
|
128
|
+
|
|
129
|
+
本库的 `Catalog` 会自动注册上表中的四种自定义数据。若直接使用 Nautilus 官方接口或在策略中使用这些类型,请先调用:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from vikron_nautilus import register_custom_types
|
|
133
|
+
|
|
134
|
+
register_custom_types()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Python 1.x 读取 Downloader 的 Catalog v1 时,请使用本库的 `Catalog`。
|
|
138
|
+
|
|
139
|
+
## Rust 使用方法
|
|
140
|
+
|
|
141
|
+
### 添加依赖
|
|
142
|
+
|
|
143
|
+
需要 Rust 1.98.1 或以上版本。在项目的 `Cargo.toml` 中添加以下依赖。
|
|
144
|
+
|
|
145
|
+
Catalog v2(默认):
|
|
146
|
+
|
|
147
|
+
```toml
|
|
148
|
+
[dependencies]
|
|
149
|
+
anyhow = "1"
|
|
150
|
+
vikron-nautilus = "0.1.0"
|
|
151
|
+
nautilus-model = { version = "0.65", features = ["high-precision"] }
|
|
152
|
+
nautilus-persistence = { version = "0.65", features = ["high-precision"] }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Catalog v1:
|
|
156
|
+
|
|
157
|
+
```toml
|
|
158
|
+
[dependencies]
|
|
159
|
+
anyhow = "1"
|
|
160
|
+
vikron-nautilus = { version = "0.1.0", default-features = false, features = ["catalog-v1"] }
|
|
161
|
+
nautilus-model = { version = "0.64", features = ["high-precision"] }
|
|
162
|
+
nautilus-persistence = { version = "0.64", features = ["high-precision"] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`catalog-v1` 和 `catalog-v2` 必须二选一,不要同时启用。
|
|
166
|
+
|
|
167
|
+
### 读取自定义数据
|
|
168
|
+
|
|
169
|
+
以下为 v2 示例:
|
|
170
|
+
|
|
171
|
+
```rust
|
|
172
|
+
use nautilus_model::data::Data;
|
|
173
|
+
use nautilus_persistence::backend::parquet::catalog::ParquetDataCatalog;
|
|
174
|
+
use vikron_nautilus::{Liquidation, register_custom_types};
|
|
175
|
+
|
|
176
|
+
fn main() -> anyhow::Result<()> {
|
|
177
|
+
register_custom_types()?;
|
|
178
|
+
let path = std::path::Path::new("./data/vikron/catalog-v2");
|
|
179
|
+
let mut catalog = ParquetDataCatalog::new(path, None, None, None, None);
|
|
180
|
+
let identifiers = vec!["ETHUSDT-PERP.BINANCE".to_string()];
|
|
181
|
+
let events = catalog.query_custom_data_dynamic(
|
|
182
|
+
"VikronLiquidation", Some(&identifiers), None, None, None, None, false,
|
|
183
|
+
)?;
|
|
184
|
+
|
|
185
|
+
for event in events {
|
|
186
|
+
if let Data::Custom(custom) = event {
|
|
187
|
+
if let Some(value) = custom.data.as_any().downcast_ref::<Liquidation>() {
|
|
188
|
+
println!("{} {} {}", value.instrument_id, value.price, value.size);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
Ok(())
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
使用 v1 时,将目录改为 `catalog-v1`,并将 `ParquetDataCatalog` 的导入改为:
|
|
197
|
+
|
|
198
|
+
```rust
|
|
199
|
+
use nautilus_persistence::backend::catalog::ParquetDataCatalog;
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
自定义数据的查询名称为类型名前加 `Vikron`,例如 `VikronOpenInterestUpdate`,读取后转换为对应 Rust 类型即可。Python 使用 `Catalog.query(Liquidation)` 等类型查询,无需手动添加前缀。已有 Catalog 文件无需修改。报价、成交等原生数据使用对应版本的 Nautilus 官方查询接口。
|
|
203
|
+
|
|
204
|
+
## 使用注意事项
|
|
205
|
+
|
|
206
|
+
- 不要混用 v1 与 v2 数据目录,选择与 Nautilus 版本匹配的输出格式。
|
|
207
|
+
- Python 自定义数据中的价格、数量和费率使用 `Decimal`,需要精确保留数值时避免转成 `float`。
|
|
208
|
+
- 回测或交易所需的 `Instrument` 信息需要自行提供。本库的自定义数据是行情观察事件,不会自动触发撮合或资金费结算。
|
|
209
|
+
|
|
210
|
+
## 许可证
|
|
211
|
+
|
|
212
|
+
本库采用 [MIT 许可证](LICENSE)。该许可不改变 Vikron Downloader 及依赖的许可证,也不授予行情数据的访问或再分发权利。
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# vikron-nautilus
|
|
2
|
+
|
|
3
|
+
在 Python 或 Rust 中读取 Vikron Downloader 生成的 Nautilus Catalog 数据,用于行情分析及 NautilusTrader 策略开发。
|
|
4
|
+
|
|
5
|
+
本项目提供 Python 包和 Rust crate,按使用语言安装其中一个即可。
|
|
6
|
+
|
|
7
|
+
## 使用范围
|
|
8
|
+
|
|
9
|
+
先在 Vikron Downloader 中选择对应的 Catalog 输出格式,再使用本库读取下载后的目录。
|
|
10
|
+
|
|
11
|
+
| 输出格式 | 数据目录 | Python:NautilusTrader 预计兼容范围 | Rust:Nautilus 预计兼容范围 |
|
|
12
|
+
| --- | --- | --- | --- |
|
|
13
|
+
| Catalog v1 | `vikron/catalog-v1` | `>=1.226.0, <2.0.0` | `>=0.64.0, <0.65.0`(`0.64.x`) |
|
|
14
|
+
| Catalog v2 | `vikron/catalog-v2` | `>=2.0.0rc6, <2.1.0` | `>=0.65.0, <0.66.0`(`0.65.x`) |
|
|
15
|
+
|
|
16
|
+
预计兼容范围用于安装依赖和选择 Catalog 版本,不代表范围内所有版本均已测试。Catalog v1 的 Python 下限 `1.226.0` 根据[官方发布说明](https://github.com/nautechsystems/nautilus_trader/blob/develop/RELEASES.md#nautilustrader-12260-beta)中的数据读取和混合回测修复推断;`1.226.0–1.229.0` 尚未进行实际数据读取测试。范围内未来发布的版本也仍需验证。
|
|
17
|
+
|
|
18
|
+
这里的 Python 1.x / 2.x 指 NautilusTrader 包版本,不是 Python 解释器版本。Rust `0.x` 的次版本可能不兼容,因此不跨次版本放宽。
|
|
19
|
+
|
|
20
|
+
已测试版本(macOS ARM64):
|
|
21
|
+
|
|
22
|
+
| 输出格式 | Python:NautilusTrader 已通过测试 | Rust:Nautilus 已通过测试 |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| Catalog v1 | `1.230.0`、`1.231.0` | `0.64.0` |
|
|
25
|
+
| Catalog v2 | `2.0.0rc6` | `0.65.0` |
|
|
26
|
+
|
|
27
|
+
Python 已验证原生及自定义数据的读取、空数据、数值精度和查询过滤;Rust 已验证四种自定义数据通过官方 Catalog 读取。`2.0.0rc5` 经测试缺少所需查询接口,不支持。当前验证的 Python 2.x 仍为预发布版,建议生产环境固定使用已测试版本。
|
|
28
|
+
|
|
29
|
+
支持读取的数据:
|
|
30
|
+
|
|
31
|
+
| 数据 | 类型归属 | Python 类型 | Rust 类型 |
|
|
32
|
+
| --- | --- | --- | --- |
|
|
33
|
+
| 报价 | Nautilus 原生 | `QuoteTick` | `QuoteTick` |
|
|
34
|
+
| 成交 | Nautilus 原生 | `TradeTick` | `TradeTick` |
|
|
35
|
+
| 订单簿增量 | Nautilus 原生 | `OrderBookDelta` | `OrderBookDelta` |
|
|
36
|
+
| 标记价格 | Nautilus 原生 | `MarkPriceUpdate` | `MarkPriceUpdate` |
|
|
37
|
+
| 指数价格 | Nautilus 原生 | `IndexPriceUpdate` | `IndexPriceUpdate` |
|
|
38
|
+
| 资金费率 | Nautilus 原生 | `FundingRateUpdate` | `FundingRateUpdate` |
|
|
39
|
+
| 强平 | Vikron 自定义 | `Liquidation` | `Liquidation` |
|
|
40
|
+
| 持仓量 | Vikron 自定义 | `OpenInterestUpdate` | `OpenInterestUpdate` |
|
|
41
|
+
| 最新价格 | Vikron 自定义 | `LastPriceUpdate` | `LastPriceUpdate` |
|
|
42
|
+
| 预测资金费率 | Vikron 自定义 | `PredictedFundingRateUpdate` | `PredictedFundingRateUpdate` |
|
|
43
|
+
|
|
44
|
+
具体可用的数据取决于 Downloader 的输出。本库不下载行情、不转换 CSV,也不创建或补全交易品种的 `Instrument` 信息。
|
|
45
|
+
|
|
46
|
+
## Python 使用方法
|
|
47
|
+
|
|
48
|
+
### 安装
|
|
49
|
+
|
|
50
|
+
需要 Python 3.12 或以上版本,同时满足对应 NautilusTrader 版本的平台要求。建议为两个版本分别创建虚拟环境,不要混装。
|
|
51
|
+
|
|
52
|
+
使用 Catalog v1:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
python -m pip install "vikron-nautilus[v1]==0.1.0"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
使用 Catalog v2:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
python -m pip install "vikron-nautilus[v2]==0.1.0"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 读取数据
|
|
65
|
+
|
|
66
|
+
下面以 Catalog v2 的强平数据为例。路径指向 Catalog 根目录,不是其中的 `data` 子目录;将路径和品种替换成自己的数据。
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from vikron_nautilus import Catalog, Liquidation
|
|
70
|
+
|
|
71
|
+
catalog = Catalog("./data/vikron/catalog-v2", version=2)
|
|
72
|
+
events = catalog.query(
|
|
73
|
+
Liquidation,
|
|
74
|
+
identifiers=["ETHUSDT-PERP.BINANCE"],
|
|
75
|
+
)
|
|
76
|
+
for event in events:
|
|
77
|
+
print(event.instrument_id, event.price, event.size, event.ts_init)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
使用 v1 时,将路径改成 `./data/vikron/catalog-v1`,并设置 `version=1`。
|
|
81
|
+
|
|
82
|
+
读取原生行情类型时,替换查询类型即可。例如,v2 的报价数据:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from nautilus_trader.model import QuoteTick
|
|
86
|
+
|
|
87
|
+
quotes = catalog.query(QuoteTick, identifiers=["ETHUSDT-PERP.BINANCE"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
v1 的原生行情类型从 `nautilus_trader.model.data` 导入:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from nautilus_trader.model.data import QuoteTick
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 限定查询范围
|
|
97
|
+
|
|
98
|
+
`identifiers` 使用完整的 Nautilus `InstrumentId`。`start` 和 `end` 是 Unix 纳秒时间戳,按 `ts_init` 筛选,包含起止边界;省略参数表示不限制该条件。
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
events = catalog.query(
|
|
102
|
+
Liquidation,
|
|
103
|
+
identifiers=["ETHUSDT-PERP.BINANCE"],
|
|
104
|
+
start=1788220800000000000, # 2026-09-01 00:00:00 UTC
|
|
105
|
+
end=1788307199999999999, # 2026-09-01 当天结束(UTC)
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
查询返回对象列表,空数据返回空列表。较大的历史范围建议按日期和品种分批读取。
|
|
110
|
+
|
|
111
|
+
### 使用自定义数据
|
|
112
|
+
|
|
113
|
+
本库的 `Catalog` 会自动注册上表中的四种自定义数据。若直接使用 Nautilus 官方接口或在策略中使用这些类型,请先调用:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from vikron_nautilus import register_custom_types
|
|
117
|
+
|
|
118
|
+
register_custom_types()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Python 1.x 读取 Downloader 的 Catalog v1 时,请使用本库的 `Catalog`。
|
|
122
|
+
|
|
123
|
+
## Rust 使用方法
|
|
124
|
+
|
|
125
|
+
### 添加依赖
|
|
126
|
+
|
|
127
|
+
需要 Rust 1.98.1 或以上版本。在项目的 `Cargo.toml` 中添加以下依赖。
|
|
128
|
+
|
|
129
|
+
Catalog v2(默认):
|
|
130
|
+
|
|
131
|
+
```toml
|
|
132
|
+
[dependencies]
|
|
133
|
+
anyhow = "1"
|
|
134
|
+
vikron-nautilus = "0.1.0"
|
|
135
|
+
nautilus-model = { version = "0.65", features = ["high-precision"] }
|
|
136
|
+
nautilus-persistence = { version = "0.65", features = ["high-precision"] }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Catalog v1:
|
|
140
|
+
|
|
141
|
+
```toml
|
|
142
|
+
[dependencies]
|
|
143
|
+
anyhow = "1"
|
|
144
|
+
vikron-nautilus = { version = "0.1.0", default-features = false, features = ["catalog-v1"] }
|
|
145
|
+
nautilus-model = { version = "0.64", features = ["high-precision"] }
|
|
146
|
+
nautilus-persistence = { version = "0.64", features = ["high-precision"] }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
`catalog-v1` 和 `catalog-v2` 必须二选一,不要同时启用。
|
|
150
|
+
|
|
151
|
+
### 读取自定义数据
|
|
152
|
+
|
|
153
|
+
以下为 v2 示例:
|
|
154
|
+
|
|
155
|
+
```rust
|
|
156
|
+
use nautilus_model::data::Data;
|
|
157
|
+
use nautilus_persistence::backend::parquet::catalog::ParquetDataCatalog;
|
|
158
|
+
use vikron_nautilus::{Liquidation, register_custom_types};
|
|
159
|
+
|
|
160
|
+
fn main() -> anyhow::Result<()> {
|
|
161
|
+
register_custom_types()?;
|
|
162
|
+
let path = std::path::Path::new("./data/vikron/catalog-v2");
|
|
163
|
+
let mut catalog = ParquetDataCatalog::new(path, None, None, None, None);
|
|
164
|
+
let identifiers = vec!["ETHUSDT-PERP.BINANCE".to_string()];
|
|
165
|
+
let events = catalog.query_custom_data_dynamic(
|
|
166
|
+
"VikronLiquidation", Some(&identifiers), None, None, None, None, false,
|
|
167
|
+
)?;
|
|
168
|
+
|
|
169
|
+
for event in events {
|
|
170
|
+
if let Data::Custom(custom) = event {
|
|
171
|
+
if let Some(value) = custom.data.as_any().downcast_ref::<Liquidation>() {
|
|
172
|
+
println!("{} {} {}", value.instrument_id, value.price, value.size);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
使用 v1 时,将目录改为 `catalog-v1`,并将 `ParquetDataCatalog` 的导入改为:
|
|
181
|
+
|
|
182
|
+
```rust
|
|
183
|
+
use nautilus_persistence::backend::catalog::ParquetDataCatalog;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
自定义数据的查询名称为类型名前加 `Vikron`,例如 `VikronOpenInterestUpdate`,读取后转换为对应 Rust 类型即可。Python 使用 `Catalog.query(Liquidation)` 等类型查询,无需手动添加前缀。已有 Catalog 文件无需修改。报价、成交等原生数据使用对应版本的 Nautilus 官方查询接口。
|
|
187
|
+
|
|
188
|
+
## 使用注意事项
|
|
189
|
+
|
|
190
|
+
- 不要混用 v1 与 v2 数据目录,选择与 Nautilus 版本匹配的输出格式。
|
|
191
|
+
- Python 自定义数据中的价格、数量和费率使用 `Decimal`,需要精确保留数值时避免转成 `float`。
|
|
192
|
+
- 回测或交易所需的 `Instrument` 信息需要自行提供。本库的自定义数据是行情观察事件,不会自动触发撮合或资金费结算。
|
|
193
|
+
|
|
194
|
+
## 许可证
|
|
195
|
+
|
|
196
|
+
本库采用 [MIT 许可证](LICENSE)。该许可不改变 Vikron Downloader 及依赖的许可证,也不授予行情数据的访问或再分发权利。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vikron-nautilus"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Read Vikron Catalog data with NautilusTrader Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = ["pyarrow>=21,<27", "packaging>=24"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Repository = "https://gitee.com/vikron-data/vikron-nautilus"
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
v1 = ["nautilus-trader>=1.226.0,<2.0.0"]
|
|
20
|
+
v2 = ["nautilus-trader>=2.0.0rc6,<2.1.0"]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["python/vikron_nautilus"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.sdist]
|
|
26
|
+
include = ["python", "tests/test_catalog.py", "tests/test_compat.py", "pyproject.toml", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Consumer-side registration and reading; importing does not mutate catalogs."""
|
|
2
|
+
from .data import (
|
|
3
|
+
Liquidation, OpenInterestUpdate, LastPriceUpdate,
|
|
4
|
+
PredictedFundingRateUpdate, register_custom_types,
|
|
5
|
+
)
|
|
6
|
+
from .catalog import Catalog
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Catalog", "register_custom_types", "Liquidation",
|
|
10
|
+
"OpenInterestUpdate", "LastPriceUpdate",
|
|
11
|
+
"PredictedFundingRateUpdate",
|
|
12
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Supported Nautilus Python version families."""
|
|
2
|
+
from packaging.specifiers import SpecifierSet
|
|
3
|
+
from packaging.version import Version
|
|
4
|
+
|
|
5
|
+
SUPPORTED_VERSIONS = {
|
|
6
|
+
1: SpecifierSet(">=1.226.0,<2.0.0"),
|
|
7
|
+
2: SpecifierSet(">=2.0.0rc6,<2.1.0"),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def catalog_version(nautilus_version):
|
|
12
|
+
version = Version(nautilus_version)
|
|
13
|
+
for catalog, supported in SUPPORTED_VERSIONS.items():
|
|
14
|
+
if supported.contains(version, prereleases=True):
|
|
15
|
+
return catalog
|
|
16
|
+
ranges = "; ".join(f"v{catalog}: {spec}" for catalog, spec in SUPPORTED_VERSIONS.items())
|
|
17
|
+
raise ImportError(f"Unsupported Nautilus version {version}; supported ranges: {ranges}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from decimal import Decimal, localcontext
|
|
3
|
+
|
|
4
|
+
import pyarrow.parquet as pq
|
|
5
|
+
|
|
6
|
+
from .data import CUSTOM_TYPES, V2, InstrumentId, register_custom_types
|
|
7
|
+
|
|
8
|
+
_DIRECTORIES = {
|
|
9
|
+
"QuoteTick": "quotes", "TradeTick": "trades",
|
|
10
|
+
"OrderBookDelta": "order_book_deltas", "MarkPriceUpdate": "mark_prices",
|
|
11
|
+
"IndexPriceUpdate": "index_prices", "FundingRateUpdate": "funding_rate_update",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Catalog:
|
|
16
|
+
"""Read-only adapter for the supported Nautilus Python 1.x and 2.0.x ranges.
|
|
17
|
+
|
|
18
|
+
query() returns native events/custom observations, sorted by ts_init.
|
|
19
|
+
start/end are inclusive UNIX nanoseconds; identifiers are InstrumentId strings.
|
|
20
|
+
No directory aliases or instrument downloads are performed.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, path, *, version=None):
|
|
24
|
+
expected = 2 if V2 else 1
|
|
25
|
+
if version is not None and version != expected:
|
|
26
|
+
raise ValueError(f"This Nautilus installation requires Catalog v{expected}")
|
|
27
|
+
self.path = Path(path).resolve()
|
|
28
|
+
if not self.path.is_dir():
|
|
29
|
+
raise FileNotFoundError(self.path)
|
|
30
|
+
register_custom_types()
|
|
31
|
+
if V2:
|
|
32
|
+
from nautilus_trader.persistence import ParquetDataCatalog
|
|
33
|
+
else:
|
|
34
|
+
from nautilus_trader.persistence.catalog import ParquetDataCatalog
|
|
35
|
+
self.native = ParquetDataCatalog(str(self.path))
|
|
36
|
+
|
|
37
|
+
def query(self, data_cls, *, identifiers=None, start=None, end=None):
|
|
38
|
+
if start is not None and end is not None and start > end:
|
|
39
|
+
raise ValueError("start must not exceed end")
|
|
40
|
+
name = data_cls.__name__
|
|
41
|
+
custom = data_cls in CUSTOM_TYPES
|
|
42
|
+
if custom:
|
|
43
|
+
name = data_cls.type_name_static()
|
|
44
|
+
if not custom and name not in _DIRECTORIES:
|
|
45
|
+
raise ValueError(f"Unsupported type {name}")
|
|
46
|
+
if V2:
|
|
47
|
+
from nautilus_trader.model import NautilusDataType
|
|
48
|
+
kind = NautilusDataType.Custom(name) if custom else getattr(NautilusDataType, name)
|
|
49
|
+
method = self.native.query_custom_data if custom else self.native.query
|
|
50
|
+
rows = method(kind, identifiers=identifiers, start=start, end=end)
|
|
51
|
+
# Nautilus versions can wrap observations in CustomData.
|
|
52
|
+
return [getattr(row, "data", row) if custom else row for row in rows]
|
|
53
|
+
|
|
54
|
+
directory = self.path / "data" / ("custom/" + name if custom else _DIRECTORIES[name])
|
|
55
|
+
files = sorted(str(f) for f in directory.rglob("*.parquet"))
|
|
56
|
+
if not files:
|
|
57
|
+
return []
|
|
58
|
+
if custom or name in ("MarkPriceUpdate", "IndexPriceUpdate"):
|
|
59
|
+
rows = []
|
|
60
|
+
for file in files:
|
|
61
|
+
parquet = pq.ParquetFile(file)
|
|
62
|
+
metadata = parquet.schema_arrow.metadata or {}
|
|
63
|
+
for batch in parquet.iter_batches():
|
|
64
|
+
if custom:
|
|
65
|
+
rows.extend(data_cls.decode_record_batch_py(metadata, batch))
|
|
66
|
+
else:
|
|
67
|
+
from nautilus_trader.model.objects import Price
|
|
68
|
+
for row in batch.to_pylist():
|
|
69
|
+
# Rust high-precision fixed point: signed little endian / 10^16.
|
|
70
|
+
with localcontext() as context:
|
|
71
|
+
context.prec = 50
|
|
72
|
+
value = Decimal(int.from_bytes(row["value"], "little", signed=True)).scaleb(-16)
|
|
73
|
+
rows.append(data_cls(
|
|
74
|
+
InstrumentId.from_str(metadata[b"instrument_id"].decode()),
|
|
75
|
+
Price.from_str(format(value, ".16f")),
|
|
76
|
+
row["ts_event"], row["ts_init"],
|
|
77
|
+
))
|
|
78
|
+
else:
|
|
79
|
+
rows = self.native.query(data_cls, files=files)
|
|
80
|
+
return sorted(
|
|
81
|
+
(row for row in rows
|
|
82
|
+
if (identifiers is None or str(row.instrument_id) in identifiers)
|
|
83
|
+
and (start is None or row.ts_init >= start)
|
|
84
|
+
and (end is None or row.ts_init <= end)),
|
|
85
|
+
key=lambda row: row.ts_init,
|
|
86
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
import pyarrow as pa
|
|
6
|
+
|
|
7
|
+
from ._compat import catalog_version
|
|
8
|
+
|
|
9
|
+
NAUTILUS_VERSION = version("nautilus_trader")
|
|
10
|
+
V2 = catalog_version(NAUTILUS_VERSION) == 2
|
|
11
|
+
if V2:
|
|
12
|
+
from nautilus_trader.model import InstrumentId
|
|
13
|
+
BaseData = object
|
|
14
|
+
else:
|
|
15
|
+
from nautilus_trader.core.data import Data as BaseData
|
|
16
|
+
from nautilus_trader.model.identifiers import InstrumentId
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _Observation(BaseData):
|
|
20
|
+
fields = ()
|
|
21
|
+
decimals = ()
|
|
22
|
+
|
|
23
|
+
def __init__(self, instrument_id, ts_event, ts_init, **values):
|
|
24
|
+
if set(values) != set(self.fields):
|
|
25
|
+
raise ValueError(f"{type(self).__name__} requires fields {self.fields}")
|
|
26
|
+
self.instrument_id = InstrumentId.from_str(str(instrument_id))
|
|
27
|
+
self._ts_event = int(ts_event)
|
|
28
|
+
self._ts_init = int(ts_init)
|
|
29
|
+
if not (0 <= self._ts_event < 2**64 and 0 <= self._ts_init < 2**64):
|
|
30
|
+
raise ValueError("invalid event/init timestamps")
|
|
31
|
+
for name, value in values.items():
|
|
32
|
+
setattr(self, name, Decimal(str(value)) if name in self.decimals else value)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def ts_event(self):
|
|
36
|
+
return self._ts_event
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def ts_init(self):
|
|
40
|
+
return self._ts_init
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def type_name_static(cls):
|
|
44
|
+
return f"Vikron{cls.__name__}"
|
|
45
|
+
|
|
46
|
+
def to_dict(self):
|
|
47
|
+
return dict(
|
|
48
|
+
type=self.type_name_static(), instrument_id=str(self.instrument_id),
|
|
49
|
+
ts_event=self.ts_event, ts_init=self.ts_init,
|
|
50
|
+
**{name: str(getattr(self, name)) if name in self.decimals else getattr(self, name)
|
|
51
|
+
for name in self.fields},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, values):
|
|
56
|
+
values = dict(values)
|
|
57
|
+
if values.pop("type", cls.type_name_static()) != cls.type_name_static():
|
|
58
|
+
raise ValueError("unexpected custom data type")
|
|
59
|
+
return cls(**values)
|
|
60
|
+
|
|
61
|
+
def to_json(self):
|
|
62
|
+
return json.dumps(self.to_dict())
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_json(cls, value):
|
|
66
|
+
return cls.from_dict(value if isinstance(value, dict) else json.loads(value))
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def arrow_schema_py(cls):
|
|
70
|
+
timestamp = pa.timestamp("ns", tz="UTC") if V2 else pa.uint64()
|
|
71
|
+
return pa.schema([
|
|
72
|
+
pa.field("payload", pa.string(), nullable=False),
|
|
73
|
+
pa.field("ts_event", timestamp, nullable=False),
|
|
74
|
+
pa.field("ts_init", timestamp, nullable=False),
|
|
75
|
+
])
|
|
76
|
+
|
|
77
|
+
def encode_record_batch_py(self, items):
|
|
78
|
+
return pa.RecordBatch.from_arrays([
|
|
79
|
+
pa.array([item.to_json() for item in items]),
|
|
80
|
+
pa.array([item.ts_event for item in items], type=pa.int64() if V2 else pa.uint64()).cast(self.arrow_schema_py().field("ts_event").type),
|
|
81
|
+
pa.array([item.ts_init for item in items], type=pa.int64() if V2 else pa.uint64()).cast(self.arrow_schema_py().field("ts_init").type),
|
|
82
|
+
], schema=self.arrow_schema_py())
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def decode_record_batch_py(cls, metadata, batch):
|
|
86
|
+
times = batch.column("ts_init").cast(pa.int64() if V2 else pa.uint64()).to_pylist()
|
|
87
|
+
events = batch.column("ts_event").cast(pa.int64() if V2 else pa.uint64()).to_pylist()
|
|
88
|
+
rows = []
|
|
89
|
+
for payload, event, init in zip(batch.column("payload").to_pylist(), events, times):
|
|
90
|
+
values = json.loads(payload)
|
|
91
|
+
values.update(ts_event=event, ts_init=init)
|
|
92
|
+
rows.append(cls.from_dict(values))
|
|
93
|
+
return rows
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Liquidation(_Observation):
|
|
97
|
+
fields = ("source_id", "side", "price", "size")
|
|
98
|
+
decimals = ("price", "size")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OpenInterestUpdate(_Observation):
|
|
102
|
+
fields = ("open_interest",)
|
|
103
|
+
decimals = fields
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class LastPriceUpdate(_Observation):
|
|
107
|
+
fields = ("price",)
|
|
108
|
+
decimals = fields
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class PredictedFundingRateUpdate(_Observation):
|
|
112
|
+
fields = ("rate", "funding_timestamp")
|
|
113
|
+
decimals = ("rate",)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
CUSTOM_TYPES = (
|
|
117
|
+
Liquidation, OpenInterestUpdate,
|
|
118
|
+
LastPriceUpdate, PredictedFundingRateUpdate,
|
|
119
|
+
)
|
|
120
|
+
_registered = False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def register_custom_types():
|
|
124
|
+
"""Register once per interpreter, before querying/subscribing to custom data."""
|
|
125
|
+
global _registered
|
|
126
|
+
if _registered:
|
|
127
|
+
return
|
|
128
|
+
if V2:
|
|
129
|
+
from nautilus_trader.model import register_custom_data_class
|
|
130
|
+
for cls in CUSTOM_TYPES:
|
|
131
|
+
register_custom_data_class(cls)
|
|
132
|
+
else:
|
|
133
|
+
from nautilus_trader.serialization import register_serializable_type
|
|
134
|
+
from nautilus_trader.serialization.arrow.serializer import register_arrow
|
|
135
|
+
for cls in CUSTOM_TYPES:
|
|
136
|
+
register_serializable_type(cls, cls.to_dict, cls.from_dict)
|
|
137
|
+
register_arrow(
|
|
138
|
+
cls, cls.arrow_schema_py(),
|
|
139
|
+
encoder=lambda obj: obj.encode_record_batch_py([obj]),
|
|
140
|
+
decoder=lambda batch, cls=cls: cls.decode_record_batch_py({}, batch),
|
|
141
|
+
)
|
|
142
|
+
_registered = True
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Integration checks against fixtures exported by downloader's versions test."""
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from vikron_nautilus import (
|
|
7
|
+
Catalog, register_custom_types, Liquidation, OpenInterestUpdate,
|
|
8
|
+
LastPriceUpdate, PredictedFundingRateUpdate,
|
|
9
|
+
)
|
|
10
|
+
from vikron_nautilus.data import V2, CUSTOM_TYPES
|
|
11
|
+
|
|
12
|
+
if V2:
|
|
13
|
+
from nautilus_trader import model
|
|
14
|
+
else:
|
|
15
|
+
from nautilus_trader.model import data as model
|
|
16
|
+
|
|
17
|
+
base = Path(sys.argv[1])
|
|
18
|
+
if not V2:
|
|
19
|
+
base = base / "v1"
|
|
20
|
+
register_custom_types()
|
|
21
|
+
register_custom_types()
|
|
22
|
+
assert CUSTOM_TYPES == (Liquidation, OpenInterestUpdate, LastPriceUpdate, PredictedFundingRateUpdate)
|
|
23
|
+
for cls in CUSTOM_TYPES:
|
|
24
|
+
assert not cls.__name__.startswith("Vikron")
|
|
25
|
+
assert cls.type_name_static() == f"Vikron{cls.__name__}"
|
|
26
|
+
count = 0
|
|
27
|
+
for kind, types in [
|
|
28
|
+
("BookTicker", [model.QuoteTick]),
|
|
29
|
+
("Trades", [model.TradeTick]),
|
|
30
|
+
("BookDeltas", [model.OrderBookDelta]),
|
|
31
|
+
("Liquidations", [CUSTOM_TYPES[0]]),
|
|
32
|
+
("DerivativeTicker", [
|
|
33
|
+
model.MarkPriceUpdate, model.IndexPriceUpdate, model.FundingRateUpdate,
|
|
34
|
+
*CUSTOM_TYPES[1:],
|
|
35
|
+
]),
|
|
36
|
+
]:
|
|
37
|
+
for empty in (False, True):
|
|
38
|
+
catalog = Catalog(base / f"{kind}-{str(empty).lower()}", version=2 if V2 else 1)
|
|
39
|
+
for cls in types:
|
|
40
|
+
rows = catalog.query(cls)
|
|
41
|
+
assert len(rows) == (0 if empty else (2 if kind == "BookDeltas" else 1)), (kind, cls, rows)
|
|
42
|
+
for row in rows:
|
|
43
|
+
assert row.ts_event == 1788220800000000000
|
|
44
|
+
assert row.ts_init == 1788220800000001000
|
|
45
|
+
assert str(row.instrument_id) == "ETHUSDT-PERP.BINANCE"
|
|
46
|
+
if rows and kind == "BookTicker":
|
|
47
|
+
assert str(rows[0].bid_price) == "1.0000000000000001"
|
|
48
|
+
if rows and kind == "Liquidations":
|
|
49
|
+
assert rows[0].price == Decimal("1.0000000000000001")
|
|
50
|
+
assert rows[0].size == Decimal("3")
|
|
51
|
+
assert rows[0].side == "BUY"
|
|
52
|
+
assert rows[0].source_id == "001"
|
|
53
|
+
if rows and cls in CUSTOM_TYPES:
|
|
54
|
+
event = rows[0]
|
|
55
|
+
assert type(event) is cls
|
|
56
|
+
assert event.to_dict()["type"] == cls.type_name_static()
|
|
57
|
+
assert cls.from_json(event.to_json()).to_dict() == event.to_dict()
|
|
58
|
+
if V2:
|
|
59
|
+
from nautilus_trader.model import CustomData, DataType
|
|
60
|
+
wrapper = CustomData(DataType(cls.type_name_static(), {"instrument_id": str(event.instrument_id)}, str(event.instrument_id)), event)
|
|
61
|
+
restored = CustomData.from_json_bytes(bytes(wrapper.to_json_bytes()))
|
|
62
|
+
assert restored.data.to_dict() == event.to_dict()
|
|
63
|
+
else:
|
|
64
|
+
from nautilus_trader.model.data import CustomData, DataType
|
|
65
|
+
wrapper = CustomData(DataType(cls, {"instrument_id": str(event.instrument_id)}), event)
|
|
66
|
+
assert wrapper.ts_init == event.ts_init
|
|
67
|
+
assert catalog.query(cls, start=1788220800000001001) == []
|
|
68
|
+
assert catalog.query(cls, identifiers=["OTHER.BINANCE"]) == []
|
|
69
|
+
assert len(catalog.query(cls, start=1788220800000001000, end=1788220800000001000)) == len(rows)
|
|
70
|
+
count += 1
|
|
71
|
+
print("PASS", kind, cls.__name__, "empty=", empty)
|
|
72
|
+
print(f"Passed {count} cases; Nautilus Python {'2' if V2 else '1'}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Version selection and dependency-range consistency checks."""
|
|
2
|
+
import tomllib
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from packaging.requirements import Requirement
|
|
7
|
+
|
|
8
|
+
from vikron_nautilus._compat import SUPPORTED_VERSIONS, catalog_version
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VersionTests(unittest.TestCase):
|
|
12
|
+
def test_supported_version_selection(self):
|
|
13
|
+
for version, expected in [
|
|
14
|
+
("1.226.0", 1), ("1.227.0", 1), ("1.228.0", 1), ("1.229.0", 1),
|
|
15
|
+
("1.230.0", 1), ("1.231.0", 1), ("1.231.1", 1),
|
|
16
|
+
("2.0.0rc6", 2), ("2.0.0", 2), ("2.0.1", 2),
|
|
17
|
+
]:
|
|
18
|
+
with self.subTest(version=version):
|
|
19
|
+
self.assertEqual(catalog_version(version), expected)
|
|
20
|
+
|
|
21
|
+
def test_unsupported_version_selection(self):
|
|
22
|
+
for version in [
|
|
23
|
+
"1.225.0", "1.226.0rc1", "2.0.0a1", "2.0.0rc4", "2.0.0rc5",
|
|
24
|
+
"2.1.0rc1", "2.1.0", "3.0.0",
|
|
25
|
+
]:
|
|
26
|
+
with self.subTest(version=version):
|
|
27
|
+
with self.assertRaises(ImportError):
|
|
28
|
+
catalog_version(version)
|
|
29
|
+
|
|
30
|
+
def test_install_ranges_match_runtime(self):
|
|
31
|
+
metadata = tomllib.loads((Path(__file__).parents[1] / "pyproject.toml").read_text())
|
|
32
|
+
extras = metadata["project"]["optional-dependencies"]
|
|
33
|
+
for catalog, supported in SUPPORTED_VERSIONS.items():
|
|
34
|
+
requirement = Requirement(extras[f"v{catalog}"][0])
|
|
35
|
+
self.assertEqual(requirement.name, "nautilus-trader")
|
|
36
|
+
self.assertEqual(requirement.specifier, supported)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
unittest.main()
|