jcback 0.1.1__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.
jcback/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ """
2
+ jcback — 事件驱动 A 股回测框架。
3
+
4
+ 函数式策略(推荐,与几策平台 init / on_bar 写法一致)::
5
+
6
+ def init(context): ...
7
+ def on_bar(context, bars): ...
8
+
9
+ def run_main():
10
+ run(
11
+ strategy_id="demo",
12
+ mode=MODE_BACKTEST,
13
+ symbols="600000.SH",
14
+ backtest_start_time="2025-01-02",
15
+ backtest_end_time="2025-03-31",
16
+ backtest_match_mode=MATCH_NEXT_OPEN,
17
+ init_func=init,
18
+ on_bar=on_bar,
19
+ use_mock=True,
20
+ )
21
+
22
+ 类式策略(jcback.Strategy 子类)::
23
+
24
+ from jcback import run_backtest, Strategy
25
+ result = run_backtest(MyStrategy, use_mock=True, symbols="600000.SH", start_date="2025-01-02", ...)
26
+ """
27
+ from __future__ import annotations
28
+
29
+ from .sim.facade import Backtest, Strategy
30
+ from .runtime import (
31
+ ADJUST_NONE,
32
+ ADJUST_POST,
33
+ ADJUST_PREV,
34
+ Bar,
35
+ BacktestConfig,
36
+ BacktestEvent,
37
+ BacktestResult,
38
+ Context,
39
+ EventBacktest,
40
+ EventBus,
41
+ FunctionStrategy,
42
+ MATCH_CURRENT_CLOSE,
43
+ MATCH_NEXT_OPEN,
44
+ MODE_BACKTEST,
45
+ infer_asset_type,
46
+ jcdata_to_ohlcv,
47
+ load_from_jcdata,
48
+ load_multi_ohlcv,
49
+ make_mock_jcdata,
50
+ parse_symbols,
51
+ resolve_config,
52
+ )
53
+ from .runtime.export import emit_report_marker, get_last_result, serialize_for_ui
54
+ from .runtime.report import print_backtest_report, setup_logger
55
+ from .entry import default_backtest_window, run, run_backtest
56
+
57
+ __all__ = [
58
+ "ADJUST_NONE",
59
+ "ADJUST_POST",
60
+ "ADJUST_PREV",
61
+ "Backtest",
62
+ "BacktestConfig",
63
+ "BacktestEvent",
64
+ "BacktestResult",
65
+ "Bar",
66
+ "Context",
67
+ "EventBacktest",
68
+ "EventBus",
69
+ "FunctionStrategy",
70
+ "MATCH_CURRENT_CLOSE",
71
+ "MATCH_NEXT_OPEN",
72
+ "MODE_BACKTEST",
73
+ "Strategy",
74
+ "default_backtest_window",
75
+ "infer_asset_type",
76
+ "jcdata_to_ohlcv",
77
+ "load_from_jcdata",
78
+ "load_multi_ohlcv",
79
+ "make_mock_jcdata",
80
+ "parse_symbols",
81
+ "emit_report_marker",
82
+ "get_last_result",
83
+ "print_backtest_report",
84
+ "resolve_config",
85
+ "run",
86
+ "run_backtest",
87
+ "serialize_for_ui",
88
+ "setup_logger",
89
+ ]
jcback/config.yaml ADDED
@@ -0,0 +1,56 @@
1
+ # jcback 回测默认配置
2
+ # 手续费规则:按品种区分买卖、期货平昨/平今;支持固定费 + 成交额比例
3
+
4
+ commission:
5
+ # 未命中品种规则时的兜底
6
+ default:
7
+ buy_ratio: 0.0001
8
+ sell_ratio: 0.0001
9
+ fixed: 0.0
10
+ min_commission: 0.0
11
+ stamp_tax_ratio: 0.0
12
+
13
+ by_asset:
14
+ # A 股:买卖佣金不同,卖出另收印花税
15
+ stock:
16
+ buy_ratio: 0.00025
17
+ sell_ratio: 0.00025
18
+ fixed: 0.0
19
+ min_commission: 5.0
20
+ stamp_tax_ratio: 0.001
21
+
22
+ etf:
23
+ buy_ratio: 0.00025
24
+ sell_ratio: 0.00025
25
+ fixed: 0.0
26
+ min_commission: 5.0
27
+ stamp_tax_ratio: 0.0
28
+
29
+ # 可转债:买卖费率通常一致,无印花税
30
+ convertible_bond:
31
+ buy_ratio: 0.00005
32
+ sell_ratio: 0.00005
33
+ fixed: 0.0
34
+ min_commission: 0.0
35
+ stamp_tax_ratio: 0.0
36
+
37
+ bond:
38
+ buy_ratio: 0.00005
39
+ sell_ratio: 0.00005
40
+ fixed: 0.0
41
+ min_commission: 0.0
42
+ stamp_tax_ratio: 0.0
43
+
44
+ # 期货:通用兜底(未在 product_fees.yaml 列出的品种使用)
45
+ future:
46
+ multiplier: 1.0
47
+ open_ratio: 0.000023
48
+ open_fixed: 0.0
49
+ close_yesterday_ratio: 0.000023
50
+ close_yesterday_fixed: 0.0
51
+ close_today_ratio: 0.00023
52
+ close_today_fixed: 0.0
53
+ min_commission: 0.0
54
+
55
+ # 中国期货各品种交易所标准费率表(按品种代码 IF/RB/MA 等匹配)
56
+ future_fee_table: product_fees.yaml
jcback/entry.py ADDED
@@ -0,0 +1,355 @@
1
+ """
2
+ jcback 主入口:统一 run() 风格(与几策策略脚本一致)。
3
+
4
+ 策略文件定义 init / on_bar / on_backtest_finished,在 run_main() 中调用 run()。
5
+ 支持单标的与多标的组合回测。
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime, timedelta
10
+ from typing import Callable
11
+
12
+ import pandas as pd
13
+ from loguru import logger
14
+
15
+ from .sim.facade import Strategy
16
+ from .runtime.constants import (
17
+ ADJUST_NONE,
18
+ ADJUST_POST,
19
+ ADJUST_PREV,
20
+ MATCH_CURRENT_CLOSE,
21
+ MATCH_NEXT_OPEN,
22
+ MODE_BACKTEST,
23
+ )
24
+ from .runtime.data import jcdata_to_ohlcv, load_multi_ohlcv, parse_symbols
25
+ from .runtime.driver import EventBacktest, _configure_logging, _load_ohlcv
26
+ from .runtime.config import resolve_config
27
+ from .runtime.multi_runner import run_portfolio_backtest
28
+ from .runtime.export import finish_run
29
+ from .runtime.result import BacktestResult
30
+ from .runtime.strategy import FunctionStrategy
31
+
32
+ __all__ = [
33
+ "MODE_BACKTEST",
34
+ "ADJUST_NONE",
35
+ "ADJUST_PREV",
36
+ "ADJUST_POST",
37
+ "MATCH_NEXT_OPEN",
38
+ "MATCH_CURRENT_CLOSE",
39
+ "run",
40
+ "run_backtest",
41
+ "default_backtest_window",
42
+ ]
43
+
44
+
45
+ def _parse_dates(
46
+ backtest_start_time: str | None,
47
+ backtest_end_time: str | None,
48
+ start_date: str | None,
49
+ end_date: str | None,
50
+ ) -> tuple[str | None, str | None]:
51
+ """统一日期参数:backtest_* 与 start_date/end_date 互为别名。"""
52
+ start = (backtest_start_time or start_date or "")[:10] or None
53
+ end = (backtest_end_time or end_date or "")[:10] or None
54
+ return start, end
55
+
56
+
57
+ def _resolve_capital(
58
+ backtest_initial_cash: float | None,
59
+ initial_capital: float,
60
+ cash: float | None,
61
+ ) -> float:
62
+ if cash is not None:
63
+ return cash
64
+ if backtest_initial_cash is not None:
65
+ return backtest_initial_cash
66
+ return initial_capital
67
+
68
+
69
+ def _resolve_data(
70
+ data: pd.DataFrame | dict[str, pd.DataFrame] | None,
71
+ jcdata_df: pd.DataFrame | None,
72
+ sym_list: list[str],
73
+ ) -> tuple[pd.DataFrame | None, dict[str, pd.DataFrame] | None, list[str]]:
74
+ """
75
+ 统一数据入口:data 可为单标的 DataFrame 或多标的 dict。
76
+ jcdata_df 为 jcdata 原始格式,按 sym_list 转换。
77
+ """
78
+ if isinstance(data, dict):
79
+ if not sym_list:
80
+ sym_list = list(data.keys())
81
+ return None, data, sym_list
82
+ if data is not None:
83
+ return data, None, sym_list
84
+ if jcdata_df is not None:
85
+ if len(sym_list) > 1:
86
+ ohlcv_map = {sym: jcdata_to_ohlcv(jcdata_df, symbol=sym) for sym in sym_list}
87
+ return None, ohlcv_map, sym_list
88
+ sym = sym_list[0] if sym_list else None
89
+ return jcdata_to_ohlcv(jcdata_df, symbol=sym), None, sym_list
90
+ return None, None, sym_list
91
+
92
+
93
+ def run(
94
+ *,
95
+ mode: str = MODE_BACKTEST,
96
+ strategy_id: str | None = None,
97
+ symbols: str | list[str] | None = None,
98
+ backtest_start_time: str | None = None,
99
+ backtest_end_time: str | None = None,
100
+ start_date: str | None = None,
101
+ end_date: str | None = None,
102
+ backtest_adjust: str = ADJUST_PREV,
103
+ backtest_initial_cash: float | None = None,
104
+ initial_capital: float = 1_000_000,
105
+ cash: float | None = None,
106
+ backtest_commission_ratio: float | None = None,
107
+ commission: float | None = None,
108
+ backtest_slippage_ratio: float = 0.0001,
109
+ slippage: float | None = None,
110
+ backtest_match_mode: int = MATCH_NEXT_OPEN,
111
+ backtest_leverage_ratio: float = 1.0,
112
+ backtest_max_position_ratio: float | None = None,
113
+ backtest_max_single_position_ratio: float | None = None,
114
+ settlement_rule: str | None = None,
115
+ asset_type: str | None = None,
116
+ data: pd.DataFrame | dict[str, pd.DataFrame] | None = None,
117
+ jcdata_df: pd.DataFrame | None = None,
118
+ use_jcdata: bool = False,
119
+ use_mock: bool = False,
120
+ mock_seed: int = 42,
121
+ init_func: Callable | None = None,
122
+ on_bar: Callable | None = None,
123
+ on_backtest_finished: Callable | None = None,
124
+ on_error: Callable | None = None,
125
+ strategy_class: type[Strategy] | None = None,
126
+ strategy_params: dict | None = None,
127
+ debug_level: int = 0,
128
+ deposit: float = 0.0,
129
+ withdrawal: float = 0.0,
130
+ transaction_cost: float = 0.0,
131
+ margin: float | None = None,
132
+ **backtest_kwargs,
133
+ ) -> BacktestResult:
134
+ """
135
+ 统一回测入口。
136
+
137
+ symbols: 单标的 str、多标的 list 或逗号分隔字符串,自动选择单标的/组合引擎。
138
+ data: 单标的 OHLCV DataFrame,或多标的 {symbol: DataFrame} dict。
139
+ 日期/资金/佣金:backtest_* 与 start_date、initial_capital、commission 等互为别名。
140
+ 未传 commission 时使用包内 config.yaml 品种手续费规则。
141
+
142
+ 组合仓位:context.get_position(symbol)、order_target_percent(symbol, w)。
143
+ """
144
+ if mode != MODE_BACKTEST:
145
+ raise ValueError(f"暂不支持 mode={mode}")
146
+
147
+ _configure_logging(debug_level)
148
+ start, end = _parse_dates(backtest_start_time, backtest_end_time, start_date, end_date)
149
+ capital = _resolve_capital(backtest_initial_cash, initial_capital, cash)
150
+ comm = commission if commission is not None else backtest_commission_ratio
151
+ slip = slippage if slippage is not None else backtest_slippage_ratio
152
+ execution_price = "close" if backtest_match_mode == MATCH_CURRENT_CLOSE else "open"
153
+
154
+ sym_list = parse_symbols(symbols)
155
+ single_data, ohlcv_map, sym_list = _resolve_data(data, jcdata_df, sym_list)
156
+ is_portfolio = len(sym_list) > 1 or ohlcv_map is not None
157
+
158
+ config = resolve_config(
159
+ symbol=sym_list[0] if sym_list else None,
160
+ asset_type=asset_type,
161
+ execution_price=execution_price,
162
+ settlement_rule=settlement_rule,
163
+ adjust=backtest_adjust,
164
+ match_mode=backtest_match_mode,
165
+ initial_capital=capital,
166
+ deposit=deposit,
167
+ withdrawal=withdrawal,
168
+ commission=comm,
169
+ slippage=slip,
170
+ transaction_cost=transaction_cost,
171
+ leverage_ratio=backtest_leverage_ratio,
172
+ max_position_ratio=backtest_max_position_ratio,
173
+ max_single_position_ratio=backtest_max_single_position_ratio,
174
+ margin=margin,
175
+ )
176
+ config.trading_pair = ",".join(sym_list) if sym_list else None
177
+
178
+ if is_portfolio:
179
+ if strategy_class is not None:
180
+ raise ValueError("多标的组合模式暂仅支持 init_func/on_bar 函数式策略")
181
+ try:
182
+ if ohlcv_map is None:
183
+ ohlcv_map = load_multi_ohlcv(
184
+ sym_list,
185
+ start or "",
186
+ end or "",
187
+ use_jcdata=use_jcdata,
188
+ use_mock=use_mock,
189
+ mock_seed=mock_seed,
190
+ jcdata_df=jcdata_df,
191
+ adjust=backtest_adjust,
192
+ )
193
+ except Exception as exc:
194
+ if on_error:
195
+ on_error(None, -1, str(exc))
196
+ raise
197
+
198
+ if debug_level >= 1:
199
+ logger.info(
200
+ "jcback portfolio id={} n={} exec={} bars~={}",
201
+ strategy_id,
202
+ len(sym_list),
203
+ config.execution_price,
204
+ len(next(iter(ohlcv_map.values()))) if ohlcv_map else 0,
205
+ )
206
+
207
+ return run_portfolio_backtest(
208
+ symbols=sym_list,
209
+ config=config,
210
+ ohlcv_map=ohlcv_map,
211
+ init_func=init_func,
212
+ on_bar=on_bar,
213
+ on_backtest_finished=on_backtest_finished,
214
+ debug_level=debug_level,
215
+ )
216
+
217
+ # 单标的
218
+ try:
219
+ if single_data is not None:
220
+ ohlcv, sym = single_data, (sym_list[0] if sym_list else None)
221
+ else:
222
+ ohlcv, sym = _load_ohlcv(
223
+ data=None,
224
+ jcdata_df=jcdata_df,
225
+ symbol=sym_list[0] if sym_list else None,
226
+ start_date=start,
227
+ end_date=end,
228
+ use_jcdata=use_jcdata,
229
+ use_mock=use_mock,
230
+ mock_seed=mock_seed,
231
+ adjust=backtest_adjust,
232
+ )
233
+ except Exception as exc:
234
+ if on_error:
235
+ on_error(None, -1, str(exc))
236
+ raise
237
+
238
+ if not sym:
239
+ sym = sym_list[0] if sym_list else None
240
+
241
+ if strategy_class is not None:
242
+ strat_cls = strategy_class
243
+ elif init_func is not None or on_bar is not None:
244
+ if not sym:
245
+ raise ValueError("函数式策略须指定 symbols")
246
+ strat_cls = FunctionStrategy.attach_callbacks(
247
+ symbol=sym,
248
+ init_func=init_func,
249
+ on_bar_func=on_bar,
250
+ )
251
+ else:
252
+ raise ValueError("须提供 init_func/on_bar 或 strategy_class")
253
+
254
+ if debug_level >= 1:
255
+ logger.info(
256
+ "jcback run id={} pair={} asset={} exec={} settle={} bars={}",
257
+ strategy_id, sym, config.asset_type,
258
+ config.execution_price, config.settlement_rule, len(ohlcv),
259
+ )
260
+
261
+ bt = EventBacktest(ohlcv, strat_cls, config=config, debug_level=debug_level, **backtest_kwargs)
262
+ try:
263
+ stats = bt.run(**(strategy_params or {}))
264
+ result = bt.build_result(stats)
265
+ except Exception as exc:
266
+ if on_error:
267
+ on_error(getattr(strat_cls, "_context", None), -2, str(exc))
268
+ raise
269
+
270
+ ctx = getattr(strat_cls, "_context", None)
271
+ if on_backtest_finished:
272
+ on_backtest_finished(ctx, result.indicator)
273
+
274
+ if debug_level >= 1:
275
+ logger.info(
276
+ "jcback done return={:.2f}% trades={}",
277
+ result.indicator.get("return_pct", 0),
278
+ result.indicator.get("num_trades", 0),
279
+ )
280
+ return finish_run(result, strategy_id=strategy_id)
281
+
282
+
283
+ def run_backtest(
284
+ strategy: type[Strategy],
285
+ *,
286
+ data: pd.DataFrame | dict[str, pd.DataFrame] | None = None,
287
+ jcdata_df: pd.DataFrame | None = None,
288
+ symbols: str | list[str] | None = None,
289
+ start_date: str | None = None,
290
+ end_date: str | None = None,
291
+ use_jcdata: bool = False,
292
+ use_mock: bool = False,
293
+ mock_seed: int = 42,
294
+ cash: float | None = None,
295
+ initial_capital: float = 100_000,
296
+ deposit: float = 0.0,
297
+ withdrawal: float = 0.0,
298
+ commission: float | None = None,
299
+ slippage: float = 0.0,
300
+ transaction_cost: float = 0.0,
301
+ leverage_ratio: float = 1.0,
302
+ max_position_ratio: float | None = None,
303
+ max_single_position_ratio: float | None = None,
304
+ margin: float | None = None,
305
+ asset_type: str | None = None,
306
+ execution_price: str = "open",
307
+ settlement_rule: str | None = None,
308
+ adjust: str | None = None,
309
+ match_mode: int | None = None,
310
+ debug_level: int = 0,
311
+ event_handlers: list | None = None,
312
+ finalize_trades: bool = True,
313
+ strategy_params: dict | None = None,
314
+ **backtest_kwargs,
315
+ ) -> BacktestResult:
316
+ """类式策略入口(jcback.Strategy 子类),委托 run() 执行。"""
317
+ if match_mode is None:
318
+ match_mode = MATCH_CURRENT_CLOSE if execution_price.lower() == "close" else MATCH_NEXT_OPEN
319
+ return run(
320
+ strategy_class=strategy,
321
+ symbols=symbols,
322
+ data=data,
323
+ jcdata_df=jcdata_df,
324
+ start_date=start_date,
325
+ end_date=end_date,
326
+ use_jcdata=use_jcdata,
327
+ use_mock=use_mock,
328
+ mock_seed=mock_seed,
329
+ cash=cash,
330
+ initial_capital=initial_capital,
331
+ deposit=deposit,
332
+ withdrawal=withdrawal,
333
+ commission=commission,
334
+ slippage=slippage,
335
+ transaction_cost=transaction_cost,
336
+ backtest_leverage_ratio=leverage_ratio,
337
+ backtest_max_position_ratio=max_position_ratio,
338
+ backtest_max_single_position_ratio=max_single_position_ratio,
339
+ backtest_match_mode=match_mode,
340
+ backtest_adjust=adjust or ADJUST_PREV,
341
+ settlement_rule=settlement_rule,
342
+ asset_type=asset_type,
343
+ debug_level=debug_level,
344
+ strategy_params=strategy_params,
345
+ margin=margin,
346
+ event_handlers=event_handlers,
347
+ finalize_trades=finalize_trades,
348
+ **backtest_kwargs,
349
+ )
350
+
351
+
352
+ def default_backtest_window(days: int = 60) -> tuple[str, str]:
353
+ end = datetime.now()
354
+ start = end - timedelta(days=days)
355
+ return start.strftime("%Y-%m-%d %H:%M:%S"), end.strftime("%Y-%m-%d %H:%M:%S")