akquant 0.1.4__cp310-abi3-win_amd64.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.
Potentially problematic release.
This version of akquant might be problematic. Click here for more details.
- akquant/__init__.py +98 -0
- akquant/akquant.pyd +0 -0
- akquant/akquant.pyi +683 -0
- akquant/backtest.py +659 -0
- akquant/config.py +65 -0
- akquant/data.py +136 -0
- akquant/indicator.py +81 -0
- akquant/log.py +135 -0
- akquant/ml/__init__.py +3 -0
- akquant/ml/model.py +234 -0
- akquant/py.typed +0 -0
- akquant/risk.py +40 -0
- akquant/sizer.py +96 -0
- akquant/strategy.py +824 -0
- akquant/utils.py +386 -0
- akquant-0.1.4.dist-info/METADATA +219 -0
- akquant-0.1.4.dist-info/RECORD +19 -0
- akquant-0.1.4.dist-info/WHEEL +4 -0
- akquant-0.1.4.dist-info/licenses/LICENSE +21 -0
akquant/utils.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Tuple, Union, cast
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from .akquant import Bar, from_arrays
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_bar_from_df(
|
|
10
|
+
df: pd.DataFrame,
|
|
11
|
+
symbol: Optional[str] = None,
|
|
12
|
+
column_map: Optional[Dict[str, str]] = None,
|
|
13
|
+
) -> List[Bar]:
|
|
14
|
+
r"""
|
|
15
|
+
Convert DataFrame to list of akquant.Bar.
|
|
16
|
+
|
|
17
|
+
:param df: Historical market data
|
|
18
|
+
:type df: pandas.DataFrame
|
|
19
|
+
:param symbol: Symbol code; if not provided, try to use "symbol" column
|
|
20
|
+
:type symbol: str, optional
|
|
21
|
+
:param column_map: Mapping from DataFrame columns to standard fields.
|
|
22
|
+
Defaults: date->timestamp, open->open, etc.
|
|
23
|
+
:type column_map: Dict[str, str], optional
|
|
24
|
+
:return: List of Bar objects
|
|
25
|
+
:rtype: List[Bar]
|
|
26
|
+
"""
|
|
27
|
+
if df.empty:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
# Default mapping
|
|
31
|
+
required_map = {
|
|
32
|
+
"date": "timestamp",
|
|
33
|
+
"open": "open",
|
|
34
|
+
"high": "high",
|
|
35
|
+
"low": "low",
|
|
36
|
+
"close": "close",
|
|
37
|
+
"volume": "volume",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if column_map:
|
|
41
|
+
required_map.update(column_map)
|
|
42
|
+
|
|
43
|
+
# Reverse map to find dataframe columns
|
|
44
|
+
# We need to find which df column corresponds to 'timestamp', 'open', etc.
|
|
45
|
+
# required_map is DF_COL -> STANDARD_FIELD
|
|
46
|
+
# So we want to check if keys of required_map exist in df.columns
|
|
47
|
+
|
|
48
|
+
# Actually, let's flip logic slightly to be more robust.
|
|
49
|
+
# Users pass { "my_date": "date", "my_open": "open" } ?
|
|
50
|
+
# Or { "date": "my_date", "open": "my_open" } ?
|
|
51
|
+
# The previous implementation had: required_map = {"日期": "timestamp", ...}
|
|
52
|
+
# implying Key is DF Column, Value is Internal Field.
|
|
53
|
+
|
|
54
|
+
# Let's keep that convention.
|
|
55
|
+
|
|
56
|
+
# Check for required internal fields
|
|
57
|
+
internal_fields = ["timestamp", "open", "high", "low", "close", "volume"]
|
|
58
|
+
|
|
59
|
+
# Find which DF column maps to which internal field
|
|
60
|
+
field_to_col = {}
|
|
61
|
+
for col, field in required_map.items():
|
|
62
|
+
if field in internal_fields:
|
|
63
|
+
field_to_col[field] = col
|
|
64
|
+
|
|
65
|
+
# Check if all internal fields have a corresponding column in DF
|
|
66
|
+
missing_fields = []
|
|
67
|
+
for field in internal_fields:
|
|
68
|
+
if field not in field_to_col:
|
|
69
|
+
# try finding exact match in df
|
|
70
|
+
if field in df.columns:
|
|
71
|
+
field_to_col[field] = field
|
|
72
|
+
else:
|
|
73
|
+
missing_fields.append(field)
|
|
74
|
+
else:
|
|
75
|
+
if field_to_col[field] not in df.columns:
|
|
76
|
+
missing_fields.append(f"{field} (mapped to {field_to_col[field]})")
|
|
77
|
+
|
|
78
|
+
if missing_fields:
|
|
79
|
+
raise ValueError(f"DataFrame missing columns for fields: {missing_fields}")
|
|
80
|
+
|
|
81
|
+
# Vectorized Preprocessing
|
|
82
|
+
|
|
83
|
+
# 1. Handle Timestamp
|
|
84
|
+
col_date = field_to_col["timestamp"]
|
|
85
|
+
# Convert to datetime with error coercion (invalid dates becomes NaT)
|
|
86
|
+
dt_series = pd.to_datetime(df[col_date], errors="coerce")
|
|
87
|
+
# Fill NaT with 0 (Epoch 0) or handle appropriately
|
|
88
|
+
dt_series = dt_series.fillna(pd.Timestamp(0))
|
|
89
|
+
# type: ignore
|
|
90
|
+
if dt_series.dt.tz is None:
|
|
91
|
+
dt_series = dt_series.dt.tz_localize("Asia/Shanghai")
|
|
92
|
+
dt_series = dt_series.dt.tz_convert("UTC")
|
|
93
|
+
timestamps = dt_series.astype("int64").values
|
|
94
|
+
|
|
95
|
+
# 2. Extract numeric columns
|
|
96
|
+
# Use astype(float) to ensure correct type, fillna(0.0) for safety
|
|
97
|
+
opens = df[field_to_col["open"]].fillna(0.0).astype(float).values
|
|
98
|
+
highs = df[field_to_col["high"]].fillna(0.0).astype(float).values
|
|
99
|
+
lows = df[field_to_col["low"]].fillna(0.0).astype(float).values
|
|
100
|
+
closes = df[field_to_col["close"]].fillna(0.0).astype(float).values
|
|
101
|
+
volumes = df[field_to_col["volume"]].fillna(0.0).astype(float).values
|
|
102
|
+
|
|
103
|
+
# 3. Handle Symbol
|
|
104
|
+
symbols_list: Optional[List[str]] = None
|
|
105
|
+
symbol_val = None
|
|
106
|
+
|
|
107
|
+
if symbol:
|
|
108
|
+
symbol_val = symbol
|
|
109
|
+
elif "股票代码" in df.columns:
|
|
110
|
+
# Convert to string
|
|
111
|
+
symbols_list = cast(List[str], df["股票代码"].astype(str).tolist())
|
|
112
|
+
else:
|
|
113
|
+
symbol_val = "UNKNOWN"
|
|
114
|
+
|
|
115
|
+
# Call Rust extension
|
|
116
|
+
return from_arrays(
|
|
117
|
+
timestamps, opens, highs, lows, closes, volumes, symbol_val, symbols_list, None
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def parse_duration_to_bars(duration: Union[str, int], frequency: str = "1d") -> int:
|
|
122
|
+
"""
|
|
123
|
+
Parse duration string to number of bars.
|
|
124
|
+
|
|
125
|
+
Assumes A-share trading hours for intraday frequencies.
|
|
126
|
+
|
|
127
|
+
:param duration: Duration string (e.g. "1y", "3m", "20d") or integer bars.
|
|
128
|
+
:param frequency: Data frequency ("1d", "1h", "1m"). Default "1d".
|
|
129
|
+
:return: Estimated number of bars.
|
|
130
|
+
"""
|
|
131
|
+
if isinstance(duration, int):
|
|
132
|
+
return duration
|
|
133
|
+
|
|
134
|
+
import re
|
|
135
|
+
|
|
136
|
+
match = re.match(r"(\d+)([ymwd])", duration.lower())
|
|
137
|
+
if not match:
|
|
138
|
+
# Try to parse as int string
|
|
139
|
+
try:
|
|
140
|
+
return int(duration)
|
|
141
|
+
except ValueError:
|
|
142
|
+
raise ValueError(f"Invalid duration format: {duration}")
|
|
143
|
+
|
|
144
|
+
value = int(match.group(1))
|
|
145
|
+
unit = match.group(2)
|
|
146
|
+
|
|
147
|
+
# Estimated bars per day (A-share)
|
|
148
|
+
if frequency == "1d":
|
|
149
|
+
bars_per_day = 1
|
|
150
|
+
elif frequency == "1h":
|
|
151
|
+
bars_per_day = 4
|
|
152
|
+
elif frequency == "30m":
|
|
153
|
+
bars_per_day = 8
|
|
154
|
+
elif frequency == "15m":
|
|
155
|
+
bars_per_day = 16
|
|
156
|
+
elif frequency == "5m":
|
|
157
|
+
bars_per_day = 48
|
|
158
|
+
elif frequency == "1m":
|
|
159
|
+
bars_per_day = 240
|
|
160
|
+
else:
|
|
161
|
+
bars_per_day = 1
|
|
162
|
+
|
|
163
|
+
if unit == "y":
|
|
164
|
+
return int(value * 252 * bars_per_day)
|
|
165
|
+
elif unit == "m":
|
|
166
|
+
return int(value * 21 * bars_per_day)
|
|
167
|
+
elif unit == "w":
|
|
168
|
+
return int(value * 5 * bars_per_day)
|
|
169
|
+
elif unit == "d":
|
|
170
|
+
return int(value * bars_per_day)
|
|
171
|
+
|
|
172
|
+
return value
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def df_to_arrays(
|
|
176
|
+
df: pd.DataFrame, symbol: Optional[str] = None
|
|
177
|
+
) -> Tuple[
|
|
178
|
+
np.ndarray,
|
|
179
|
+
np.ndarray,
|
|
180
|
+
np.ndarray,
|
|
181
|
+
np.ndarray,
|
|
182
|
+
np.ndarray,
|
|
183
|
+
np.ndarray,
|
|
184
|
+
Optional[str],
|
|
185
|
+
Optional[List[str]],
|
|
186
|
+
Optional[Dict[str, np.ndarray]],
|
|
187
|
+
]:
|
|
188
|
+
r"""
|
|
189
|
+
将 DataFrame 转换为用于 DataFeed.add_arrays 的数组元组.
|
|
190
|
+
|
|
191
|
+
:param df: 输入的 DataFrame
|
|
192
|
+
:param symbol: 标的代码 (可选)
|
|
193
|
+
:return: (timestamps, opens, highs, lows, closes, volumes, symbol, symbols, extra)
|
|
194
|
+
"""
|
|
195
|
+
if df.empty:
|
|
196
|
+
return (
|
|
197
|
+
np.array([], dtype=np.int64),
|
|
198
|
+
np.array([], dtype=np.float64),
|
|
199
|
+
np.array([], dtype=np.float64),
|
|
200
|
+
np.array([], dtype=np.float64),
|
|
201
|
+
np.array([], dtype=np.float64),
|
|
202
|
+
np.array([], dtype=np.float64),
|
|
203
|
+
symbol,
|
|
204
|
+
None,
|
|
205
|
+
None,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Column Mapping Strategy
|
|
209
|
+
# Priority:
|
|
210
|
+
# 1. AKShare Chinese columns: "日期", "开盘", ...
|
|
211
|
+
# 2. Standard English columns: "date", "open", ...
|
|
212
|
+
# 3. Lowercase normalized check
|
|
213
|
+
|
|
214
|
+
# Define targets
|
|
215
|
+
targets = {
|
|
216
|
+
"timestamp": ["日期", "date", "datetime", "time", "timestamp"],
|
|
217
|
+
"open": ["开盘", "open"],
|
|
218
|
+
"high": ["最高", "high"],
|
|
219
|
+
"low": ["最低", "low"],
|
|
220
|
+
"close": ["收盘", "close"],
|
|
221
|
+
"volume": ["成交量", "volume", "vol"],
|
|
222
|
+
"symbol": ["股票代码", "symbol", "code", "ticker"],
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Resolve columns
|
|
226
|
+
df_cols = df.columns
|
|
227
|
+
df_cols_lower = [str(c).lower() for c in df_cols]
|
|
228
|
+
|
|
229
|
+
resolved = {}
|
|
230
|
+
|
|
231
|
+
for key, candidates in targets.items():
|
|
232
|
+
found = None
|
|
233
|
+
for cand in candidates:
|
|
234
|
+
if cand in df_cols:
|
|
235
|
+
found = cand
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
# If not found, try case-insensitive
|
|
239
|
+
if not found:
|
|
240
|
+
for cand in candidates:
|
|
241
|
+
if cand.lower() in df_cols_lower:
|
|
242
|
+
idx = df_cols_lower.index(cand.lower())
|
|
243
|
+
found = str(df_cols[idx])
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
if found:
|
|
247
|
+
resolved[key] = found
|
|
248
|
+
|
|
249
|
+
# Check essential columns
|
|
250
|
+
missing = []
|
|
251
|
+
for essential in ["timestamp", "open", "high", "low", "close", "volume"]:
|
|
252
|
+
if essential not in resolved:
|
|
253
|
+
missing.append(essential)
|
|
254
|
+
|
|
255
|
+
if missing:
|
|
256
|
+
# If timestamp is index, handle it
|
|
257
|
+
if isinstance(df.index, pd.DatetimeIndex):
|
|
258
|
+
resolved["timestamp"] = "__index__"
|
|
259
|
+
missing = [m for m in missing if m != "timestamp"]
|
|
260
|
+
|
|
261
|
+
if missing:
|
|
262
|
+
msg = f"Missing columns: {missing}. Available: {df.columns.tolist()}"
|
|
263
|
+
raise ValueError(msg)
|
|
264
|
+
|
|
265
|
+
# 1. Handle Timestamp
|
|
266
|
+
dt_series: Union[pd.Series, pd.Index]
|
|
267
|
+
if resolved.get("timestamp") == "__index__":
|
|
268
|
+
dt_series = df.index
|
|
269
|
+
else:
|
|
270
|
+
dt_series = pd.to_datetime(df[resolved["timestamp"]], errors="coerce")
|
|
271
|
+
|
|
272
|
+
if not isinstance(dt_series, pd.DatetimeIndex):
|
|
273
|
+
dt_series = pd.to_datetime(dt_series)
|
|
274
|
+
|
|
275
|
+
dt_series = dt_series.fillna(pd.Timestamp(0))
|
|
276
|
+
|
|
277
|
+
# Handle timezone (support both Series and DatetimeIndex)
|
|
278
|
+
# Convert to Series for consistent handling
|
|
279
|
+
dt_series_s: pd.Series
|
|
280
|
+
if isinstance(dt_series, pd.Index):
|
|
281
|
+
dt_series_s = dt_series.to_series(index=dt_series)
|
|
282
|
+
else:
|
|
283
|
+
dt_series_s = cast(pd.Series, dt_series)
|
|
284
|
+
|
|
285
|
+
# Help mypy know it's a Series
|
|
286
|
+
if dt_series_s.dt.tz is None:
|
|
287
|
+
dt_series_s = dt_series_s.dt.tz_localize("Asia/Shanghai")
|
|
288
|
+
dt_series_s = dt_series_s.dt.tz_convert("UTC")
|
|
289
|
+
|
|
290
|
+
timestamps = cast(np.ndarray, dt_series_s.astype("int64").values)
|
|
291
|
+
|
|
292
|
+
# 2. Extract numeric columns
|
|
293
|
+
def get_col(name: str) -> np.ndarray:
|
|
294
|
+
return cast(np.ndarray, df[resolved[name]].fillna(0.0).astype(float).values)
|
|
295
|
+
|
|
296
|
+
opens = get_col("open")
|
|
297
|
+
highs = get_col("high")
|
|
298
|
+
lows = get_col("low")
|
|
299
|
+
closes = get_col("close")
|
|
300
|
+
volumes = get_col("volume")
|
|
301
|
+
|
|
302
|
+
# 3. Handle Symbol
|
|
303
|
+
symbols_list: Optional[List[str]] = None
|
|
304
|
+
symbol_val = None
|
|
305
|
+
|
|
306
|
+
if symbol:
|
|
307
|
+
symbol_val = symbol
|
|
308
|
+
elif "symbol" in resolved:
|
|
309
|
+
symbols_list = cast(List[str], df[resolved["symbol"]].astype(str).tolist())
|
|
310
|
+
else:
|
|
311
|
+
symbol_val = "UNKNOWN"
|
|
312
|
+
|
|
313
|
+
# 4. Handle Extra Columns
|
|
314
|
+
extra = {}
|
|
315
|
+
used_columns = set(resolved.values())
|
|
316
|
+
|
|
317
|
+
# Iterate over all columns to find numeric ones not in resolved
|
|
318
|
+
for col in df.columns:
|
|
319
|
+
if col in used_columns:
|
|
320
|
+
continue
|
|
321
|
+
|
|
322
|
+
# Try to convert to float
|
|
323
|
+
try:
|
|
324
|
+
# We use fillna(0.0) for safety, similar to other fields
|
|
325
|
+
# Check if column is numeric
|
|
326
|
+
if pd.api.types.is_numeric_dtype(df[col]):
|
|
327
|
+
extra[str(col)] = cast(
|
|
328
|
+
np.ndarray, df[col].fillna(0.0).astype(float).values
|
|
329
|
+
)
|
|
330
|
+
except Exception:
|
|
331
|
+
# Skip non-numeric extra columns
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
return (
|
|
335
|
+
timestamps,
|
|
336
|
+
opens,
|
|
337
|
+
highs,
|
|
338
|
+
lows,
|
|
339
|
+
closes,
|
|
340
|
+
volumes,
|
|
341
|
+
symbol_val,
|
|
342
|
+
symbols_list,
|
|
343
|
+
extra if extra else None,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def prepare_dataframe(
|
|
348
|
+
df: pd.DataFrame, date_col: Optional[str] = None, tz: str = "Asia/Shanghai"
|
|
349
|
+
) -> pd.DataFrame:
|
|
350
|
+
r"""
|
|
351
|
+
自动预处理 DataFrame,处理时区并生成标准时间戳列.
|
|
352
|
+
|
|
353
|
+
:param df: 输入 DataFrame
|
|
354
|
+
:param date_col: 日期列名 (若为 None 则自动探测)
|
|
355
|
+
:param tz: 默认时区 (若数据为 Naive 时间,则假定为此时区)
|
|
356
|
+
:return: 处理后的 DataFrame (包含 'timestamp' 列)
|
|
357
|
+
"""
|
|
358
|
+
df = df.copy()
|
|
359
|
+
|
|
360
|
+
# 1. Auto-detect date column
|
|
361
|
+
if date_col is None:
|
|
362
|
+
candidates = ["date", "datetime", "time", "timestamp", "日期", "时间"]
|
|
363
|
+
for c in candidates:
|
|
364
|
+
if c in df.columns:
|
|
365
|
+
date_col = c
|
|
366
|
+
break
|
|
367
|
+
|
|
368
|
+
if date_col and date_col in df.columns:
|
|
369
|
+
# 2. Convert to datetime
|
|
370
|
+
dt = pd.to_datetime(df[date_col], errors="coerce")
|
|
371
|
+
|
|
372
|
+
# 3. Handle Timezone
|
|
373
|
+
if dt.dt.tz is None:
|
|
374
|
+
dt = dt.dt.tz_localize(tz)
|
|
375
|
+
|
|
376
|
+
# 4. Convert to UTC
|
|
377
|
+
dt = dt.dt.tz_convert("UTC")
|
|
378
|
+
|
|
379
|
+
# 5. Assign back
|
|
380
|
+
df[date_col] = dt
|
|
381
|
+
df["timestamp"] = dt.astype("int64")
|
|
382
|
+
else:
|
|
383
|
+
# Warn or ignore? For now silent, user might be processing non-time data?
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
return df
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: akquant
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Classifier: Programming Language :: Rust
|
|
5
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
|
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Dist: pandas>=3.0.0
|
|
14
|
+
Requires-Dist: numpy>=2.4.1
|
|
15
|
+
Requires-Dist: pyarrow>=14.0.0
|
|
16
|
+
Requires-Dist: ruff>=0.1.0 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: pre-commit>=3.0.0 ; extra == 'dev'
|
|
18
|
+
Requires-Dist: mypy>=1.0.0 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
|
|
20
|
+
Requires-Dist: mkdocs>=1.5.0 ; extra == 'dev'
|
|
21
|
+
Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: matplotlib>=3.0.0 ; extra == 'full'
|
|
23
|
+
Requires-Dist: scikit-learn>=1.0.0 ; extra == 'ml'
|
|
24
|
+
Requires-Dist: torch>=2.0.0 ; extra == 'ml'
|
|
25
|
+
Requires-Dist: matplotlib>=3.0.0 ; extra == 'plot'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Provides-Extra: full
|
|
28
|
+
Provides-Extra: ml
|
|
29
|
+
Provides-Extra: plot
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Summary: High-performance quantitative trading framework based on Rust and Python
|
|
32
|
+
Author: Akquant Developers
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="assets/logo.svg" alt="AKQuant" width="400">
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
# AKQuant
|
|
41
|
+
|
|
42
|
+
**AKQuant** 是一个基于 **Rust** 和 **Python** 构建的高性能量化投研框架。它旨在结合 Rust 的极致性能和 Python 的易用性,为量化交易者提供强大的回测和研究工具。
|
|
43
|
+
|
|
44
|
+
最新版本参考了 [NautilusTrader](https://github.com/nautechsystems/nautilus_trader) 和 [PyBroker](https://github.com/edtechre/pybroker) 的架构理念,引入了模块化设计、独立的投资组合管理、高级订单类型支持以及便捷的数据加载与缓存机制。
|
|
45
|
+
|
|
46
|
+
📖 **[设计与开发指南 (DESIGN.md)](DESIGN.md)**: 如果你想深入了解内部架构、学习如何设计此类系统或进行二次开发,请阅读此文档。
|
|
47
|
+
|
|
48
|
+
## 核心特性
|
|
49
|
+
|
|
50
|
+
* **极致性能**: 核心回测引擎采用 Rust 编写,通过 PyO3 提供 Python 接口。
|
|
51
|
+
* **基准测试**: 在 200k K线数据的 SMA 策略回测中,AKQuant 耗时仅 **1.31s** (吞吐量 ~152k bars/sec),相比 Backtrader (26.55s) 和 PyBroker (23.61s) 快约 **20倍**。
|
|
52
|
+
* **Zero-Copy Access (New)**: 历史数据 (`ctx.history`) 通过 PyO3 Buffer Protocol / Numpy View 直接映射 Rust 内存,实现零拷贝访问,大幅提升 Python 端指标计算性能。
|
|
53
|
+
* **模块化架构**:
|
|
54
|
+
* **Engine**: 事件驱动的核心撮合引擎,采用二进制堆 (BinaryHeap) 管理事件队列。
|
|
55
|
+
* **Clock**: 参考 NautilusTrader 设计的交易时钟,精确管理交易时段 (TradingSession) 和时间流逝。
|
|
56
|
+
* **Portfolio**: 独立的投资组合管理,支持实时权益计算。
|
|
57
|
+
* **MarketModel**: 可插拔的市场模型,内置 A 股 T+1 和期货 T+0 规则。
|
|
58
|
+
* **T+1 严格风控**: 针对股票/基金,严格执行 T+1 可用持仓检查,防止当日买入当日卖出(除非配置为 T+0 市场)。
|
|
59
|
+
* **可用持仓管理**: 自动维护 `available_positions`,并扣除未成交的卖单冻结数量,防止超卖。
|
|
60
|
+
* **事件系统**:
|
|
61
|
+
* **Timer**: 支持 `schedule(timestamp, payload)` 注册定时事件,触发 `on_timer` 回调,实现复杂的盘中定时逻辑。
|
|
62
|
+
* **风控系统 (New)**:
|
|
63
|
+
* **独立拦截层**: 内置 `RiskManager`,在 Rust 引擎层直接拦截违规订单。
|
|
64
|
+
* **可用持仓检查**: 下单前实时检查可用持仓(Available - Pending Sell),防止超卖违规。
|
|
65
|
+
* **灵活配置**: 通过 `RiskConfig` 可配置最大单笔金额、最大持仓比例、黑名单等。
|
|
66
|
+
* **机器学习 (New)**:
|
|
67
|
+
* **Walk-forward Validation**: 内置滚动训练框架,彻底杜绝未来函数。
|
|
68
|
+
* **统一适配器**: 提供 `SklearnAdapter` 和 `PyTorchAdapter`,统一 Scikit-learn 和 PyTorch 接口。
|
|
69
|
+
* **信号解耦**: 提倡 Signal (预测) 与 Action (执行) 分离的设计模式。
|
|
70
|
+
* **数据生态**:
|
|
71
|
+
* **Streaming CSV (New)**: 支持流式加载超大 CSV 文件 (`DataFeed.from_csv`),极大降低内存占用。
|
|
72
|
+
* **Live Trading (New)**: 支持通过 `DataFeed.create_live()` 创建实时数据源,支持 CTP/Gateway 实时数据推送。
|
|
73
|
+
* **Parquet Data Catalog (New)**: 采用 Apache Parquet 格式存储数据,相比 Pickle 读写速度更快,压缩率更高,便于跨语言使用。
|
|
74
|
+
* **Pandas 集成**: 支持直接加载 Pandas DataFrame 数据,兼容各类数据源。
|
|
75
|
+
* **显式订阅**: 策略通过 `subscribe` 方法明确声明所需数据,引擎自动按需加载。
|
|
76
|
+
* **多资产支持**:
|
|
77
|
+
* **股票 (Stock)**: 默认支持 T+1,买入 100 股一手限制,印花税/过户费。
|
|
78
|
+
* **基金 (Fund)**: 支持基金特有费率配置。
|
|
79
|
+
* **期货 (Futures)**: 支持 T+0,保证金交易,合约乘数。
|
|
80
|
+
* **期权 (Option)**: 支持 Call/Put,行权价,按张收费模式。
|
|
81
|
+
* **高级订单 (New)**:
|
|
82
|
+
* **Stop Orders**: Rust 引擎原生支持止损单触发,提供 StopMarket 和 StopLimit。
|
|
83
|
+
* **Target Position**: 内置 `order_target_value` 等辅助函数,自动计算调仓数量。
|
|
84
|
+
* **架构抽象 (New)**:
|
|
85
|
+
* **ExecutionClient**: 抽象执行层,支持 `SimulatedExecutionClient` (内存撮合) 和 `RealtimeExecutionClient` (实盘对接)。
|
|
86
|
+
* **DataClient**: 抽象数据层,支持 `SimulatedDataClient` (内存/回放) 和 `RealtimeDataClient` (实时流)。
|
|
87
|
+
* **无缝切换**: 策略代码无需修改,仅需通过 `engine.use_realtime_execution()` 和 `DataFeed.create_live()` 即可切换至实盘模式。
|
|
88
|
+
* **灵活配置**:
|
|
89
|
+
* **Typed Config (New)**: 引入 `BacktestConfig`, `StrategyConfig`, `RiskConfig` 类型化配置对象,替代散乱的 `**kwargs`,提供更好的 IDE 提示和参数校验。
|
|
90
|
+
* **ExecutionMode**: 支持 `CurrentClose` (信号当根K线收盘成交) 和 `NextOpen` (次日开盘成交) 模式。
|
|
91
|
+
* **丰富的分析工具**:
|
|
92
|
+
* **PerformanceMetrics**:
|
|
93
|
+
* **收益**: Total Return, Annualized Return, Alpha.
|
|
94
|
+
* **风险**: Max Drawdown, Sharpe Ratio, Sortino Ratio, Ulcer Index, UPI (Ulcer Performance Index).
|
|
95
|
+
* **拟合**: Equity R² (线性回归拟合度).
|
|
96
|
+
* **TradeAnalyzer**: 包含胜率、盈亏比、最大连续盈亏等详细交易统计,支持未结盈亏 (Unrealized PnL) 计算。
|
|
97
|
+
* **仿真增强**:
|
|
98
|
+
* **滑点模型 (Slippage)**: 支持 Fixed (固定金额) 和 Percent (百分比) 滑点模型,模拟真实交易成本。
|
|
99
|
+
* **成交量限制 (Volume Limit)**: 支持按 K 线成交量比例限制单笔撮合数量,并实现分批成交 (Partial Fill)。
|
|
100
|
+
|
|
101
|
+
## 为什么选择 AKQuant?
|
|
102
|
+
|
|
103
|
+
传统的 Python 回测框架(如 backtrader)在处理大规模数据或复杂逻辑时往往面临性能瓶颈。纯 C++/Rust 框架虽然性能优越,但开发和调试门槛较高。
|
|
104
|
+
|
|
105
|
+
**AKQuant** 试图在两者之间找到平衡点:
|
|
106
|
+
|
|
107
|
+
1. **性能**: Rust 核心保证了回测速度,特别适合大规模参数优化。
|
|
108
|
+
2. **易用**: 策略编写完全使用 Python,提供类似 PyBroker 的简洁 API。
|
|
109
|
+
3. **专业**: 严格遵守中国市场交易规则(T+1、印花税、最低佣金等)。
|
|
110
|
+
|
|
111
|
+
## 前置要求
|
|
112
|
+
|
|
113
|
+
- **Rust**: [安装 Rust](https://www.rust-lang.org/tools/install)
|
|
114
|
+
- **Python**: 3.10+
|
|
115
|
+
- **Maturin**: `pip install maturin`
|
|
116
|
+
|
|
117
|
+
## 安装说明
|
|
118
|
+
|
|
119
|
+
### 开发模式(推荐)
|
|
120
|
+
|
|
121
|
+
如果你正在开发该项目并希望更改即时生效:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
maturin develop
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 快速开始
|
|
128
|
+
|
|
129
|
+
### 1. 使用 helper 快速回测 (推荐)
|
|
130
|
+
|
|
131
|
+
`AKQuant` 提供了一个类似 Zipline 的便捷入口 `run_backtest`,可以快速运行策略。
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
import akquant
|
|
135
|
+
from akquant.backtest import run_backtest
|
|
136
|
+
from akquant import Strategy
|
|
137
|
+
from akquant.config import BacktestConfig
|
|
138
|
+
|
|
139
|
+
# 1. 定义策略
|
|
140
|
+
class MyStrategy(Strategy):
|
|
141
|
+
def on_start(self):
|
|
142
|
+
# 显式订阅数据
|
|
143
|
+
self.subscribe("600000")
|
|
144
|
+
|
|
145
|
+
def on_bar(self, bar):
|
|
146
|
+
# 简单的双均线逻辑 (示例)
|
|
147
|
+
# 实际回测推荐使用 IndicatorSet 进行向量化计算
|
|
148
|
+
if self.ctx.position.size == 0:
|
|
149
|
+
self.buy(symbol=bar.symbol, quantity=100)
|
|
150
|
+
elif bar.close > self.ctx.position.avg_price * 1.1:
|
|
151
|
+
self.sell(symbol=bar.symbol, quantity=100)
|
|
152
|
+
|
|
153
|
+
# 2. 配置回测
|
|
154
|
+
config = BacktestConfig(
|
|
155
|
+
start_date="20230101",
|
|
156
|
+
end_date="20241231",
|
|
157
|
+
cash=500_000.0,
|
|
158
|
+
commission=0.0003
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# 3. 运行回测
|
|
162
|
+
# 自动加载数据、设置资金、费率等
|
|
163
|
+
result = run_backtest(
|
|
164
|
+
strategy=MyStrategy, # 传递类
|
|
165
|
+
config=config # 传递配置对象
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# 4. 查看结果
|
|
169
|
+
print(f"Total Return: {result.metrics.total_return_pct:.2f}%")
|
|
170
|
+
print(f"Sharpe Ratio: {result.metrics.sharpe_ratio:.2f}")
|
|
171
|
+
print(f"Max Drawdown: {result.metrics.max_drawdown_pct:.2f}%")
|
|
172
|
+
|
|
173
|
+
# 4. 获取详细数据 (DataFrame)
|
|
174
|
+
# 绩效指标表
|
|
175
|
+
print(result.metrics_df)
|
|
176
|
+
# 交易记录表
|
|
177
|
+
print(result.trades_df)
|
|
178
|
+
# 每日持仓表
|
|
179
|
+
print(result.daily_positions_df)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 2. 函数式 API (Zipline 风格)
|
|
183
|
+
|
|
184
|
+
如果你习惯 Zipline 或 Backtrader 的函数式写法,也可以直接使用:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from akquant.backtest import run_backtest
|
|
188
|
+
|
|
189
|
+
def initialize(ctx):
|
|
190
|
+
ctx.stop_loss_pct = 0.05
|
|
191
|
+
|
|
192
|
+
def on_bar(ctx, bar):
|
|
193
|
+
if ctx.position.size == 0:
|
|
194
|
+
ctx.buy(symbol=bar.symbol, quantity=100)
|
|
195
|
+
elif bar.close < ctx.position.avg_price * (1 - ctx.stop_loss_pct):
|
|
196
|
+
ctx.sell(symbol=bar.symbol, quantity=100)
|
|
197
|
+
|
|
198
|
+
run_backtest(
|
|
199
|
+
strategy=on_bar,
|
|
200
|
+
initialize=initialize,
|
|
201
|
+
symbol="600000",
|
|
202
|
+
start_date="20230101",
|
|
203
|
+
end_date="20231231"
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
更多示例请参考 `examples/` 目录。
|
|
208
|
+
|
|
209
|
+
## 结果分析
|
|
210
|
+
`run_backtest` 返回的 `BacktestResult` 对象提供了丰富的数据用于后续分析:
|
|
211
|
+
|
|
212
|
+
* **`result.metrics`**: 包含 Total Return, Sharpe, Max Drawdown 等核心指标的对象。
|
|
213
|
+
* **`result.metrics_df`**: 包含上述指标的 Pandas DataFrame (单行)。
|
|
214
|
+
* **`result.trades_df`**: 包含所有已平仓交易的详细记录 (Entry/Exit Time/Price, PnL, Commission 等)。
|
|
215
|
+
* **`result.daily_positions_df`**: 包含每日持仓快照的 DataFrame。
|
|
216
|
+
* **`result.equity_curve`**: 权益曲线数据列表 `[(timestamp, equity), ...]`。
|
|
217
|
+
|
|
218
|
+
## 快速链接
|
|
219
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
akquant\__init__.py,sha256=zvuDLn0Z18mgCgStp9w2XP48UpDJwN0BCOcjL2TwBU8,2782
|
|
2
|
+
akquant\akquant.pyd,sha256=WhshCrT5ywvajOowrBalOCdBaXOBR7FV2vfvy4lErao,1351680
|
|
3
|
+
akquant\akquant.pyi,sha256=kUzpmcduLiTdRVkOL6r6JAzEdyCxqTXQ_E3Qdh7ZFPc,17470
|
|
4
|
+
akquant\backtest.py,sha256=9iWyai2FduT1wIRUZQaZpYSky2rQLAQYJVH4_zvxBnk,23238
|
|
5
|
+
akquant\config.py,sha256=_KdbGZIUlbtmvDcca8zmWCOnxjOR1xkxrI3LsjiLYlw,1587
|
|
6
|
+
akquant\data.py,sha256=P_pHhuIJLXqfo3Bz9ZqlQTLnINitL630LZGP0wgElA8,4492
|
|
7
|
+
akquant\indicator.py,sha256=EzWyrRp3Wihmg52WwOsOTFWs-xHy-D_NiVxKXZe8giA,2832
|
|
8
|
+
akquant\log.py,sha256=hbouqiBsL_rxkuEXldBLeYyEHMqXcN6eAGCaar0BOOY,3872
|
|
9
|
+
akquant\ml\__init__.py,sha256=85AriDIeIUl2oWZsfgRyQW4t5MRmJ5CGvZ1m2-ru0S0,124
|
|
10
|
+
akquant\ml\model.py,sha256=ADUW6eno6Qj57znZysEiHd6lGsxCPT68Kt1tbRvNTs8,7379
|
|
11
|
+
akquant\py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
akquant\risk.py,sha256=xmugILoPrxosjuUhKZ2mNS29hm8pNBgbQkJLxLrlHWU,1285
|
|
13
|
+
akquant\sizer.py,sha256=mt3LbvVcDV1vQaGkTKCqnIlq2m2c7VH8cg6aE1ThzFI,2231
|
|
14
|
+
akquant\strategy.py,sha256=UmmSjq2Dysw6c0Ske004BqMC7_utXgvPwyQMIS3rRB8,27415
|
|
15
|
+
akquant\utils.py,sha256=VdEQLw4uK_jLLW8arFYGq4lt1rIJp9uPPtrgr5Ayf9E,12213
|
|
16
|
+
akquant-0.1.4.dist-info\METADATA,sha256=ZNE89F9PTS70jDyXl8Z0PXbwQh-G25Psyp9KFue7UzQ,10749
|
|
17
|
+
akquant-0.1.4.dist-info\WHEEL,sha256=ZMDDxh9OPoaLQ4P2dJmgI1XsENYSzjzq8fErKKVw5iE,96
|
|
18
|
+
akquant-0.1.4.dist-info\licenses\LICENSE,sha256=VpuNU4v5Lu61sALJ2QfDitlg4BLocmPdAce7tQvBz3w,1075
|
|
19
|
+
akquant-0.1.4.dist-info\RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 akquant developers
|
|
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.
|