AlgoTradeKit 0.7.2__tar.gz → 0.7.3__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.
Files changed (58) hide show
  1. {algotradekit-0.7.2 → algotradekit-0.7.3}/CHANGELOG.md +59 -0
  2. {algotradekit-0.7.2 → algotradekit-0.7.3}/PKG-INFO +49 -2
  3. {algotradekit-0.7.2 → algotradekit-0.7.3}/README.md +48 -1
  4. {algotradekit-0.7.2 → algotradekit-0.7.3}/pyproject.toml +1 -1
  5. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/__init__.py +1 -1
  6. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/__init__.py +2 -0
  7. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_config.py +52 -0
  8. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_engine.py +199 -56
  9. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_position.py +23 -6
  10. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_runner.py +9 -6
  11. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/strategy/_types.py +24 -0
  12. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_simulate.py +352 -0
  13. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_strategy.py +21 -0
  14. {algotradekit-0.7.2 → algotradekit-0.7.3}/.github/workflows/publish.yml +0 -0
  15. {algotradekit-0.7.2 → algotradekit-0.7.3}/.gitignore +0 -0
  16. {algotradekit-0.7.2 → algotradekit-0.7.3}/LICENSE +0 -0
  17. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/__init__.py +0 -0
  18. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/_utils.py +0 -0
  19. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/collector.py +0 -0
  20. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/converter.py +0 -0
  21. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/normalizer.py +0 -0
  22. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/sources/__init__.py +0 -0
  23. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/sources/base.py +0 -0
  24. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/sources/binance.py +0 -0
  25. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/storage/__init__.py +0 -0
  26. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/data/storage/csv_handler.py +0 -0
  27. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/__init__.py +0 -0
  28. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/_base.py +0 -0
  29. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/atr.py +0 -0
  30. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/ichimoku.py +0 -0
  31. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/ma.py +0 -0
  32. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/macd.py +0 -0
  33. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/indicator/rsi.py +0 -0
  34. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/report/__init__.py +0 -0
  35. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/report/_builder.py +0 -0
  36. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/report/_display.py +0 -0
  37. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/report/_server.py +0 -0
  38. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/report/static/report.html +0 -0
  39. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_lot.py +0 -0
  40. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_report.py +0 -0
  41. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/simulate/_session.py +0 -0
  42. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/strategy/__init__.py +0 -0
  43. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/strategy/_base.py +0 -0
  44. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/strategy/builtin/__init__.py +0 -0
  45. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/strategy/builtin/macd.py +0 -0
  46. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/visual/__init__.py +0 -0
  47. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/visual/chart.py +0 -0
  48. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/visual/indicator_renderer.py +0 -0
  49. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/visual/models.py +0 -0
  50. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/visual/server.py +0 -0
  51. {algotradekit-0.7.2 → algotradekit-0.7.3}/src/AlgoTradeKit/visual/static/index.html +0 -0
  52. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_data.py +0 -0
  53. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_data_converter.py +0 -0
  54. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_data_normalizer.py +0 -0
  55. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_indicator.py +0 -0
  56. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_report.py +0 -0
  57. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_visual.py +0 -0
  58. {algotradekit-0.7.2 → algotradekit-0.7.3}/tests/test_visual__0_4_1.py +0 -0
@@ -7,6 +7,65 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.7.3] — 2026-06-21
11
+
12
+ ### Added
13
+
14
+ #### `strategy` module
15
+ - **`Signal.risk_multiplier`** — new optional field (default `1.0`) that scales
16
+ the size/risk a single signal receives relative to the run's global
17
+ `SimulateConfig` sizing (`risk_percent` / `fixed_amount` / `fixed_lot`).
18
+ Lets a strategy vary risk per signal instead of using one fixed sizing
19
+ rule for every trade in a backtest — e.g. half size on a tentative
20
+ "limit" entry vs. full size on a confirmed one, or splitting one trade
21
+ idea into several sub-positions whose sizes sum to one risk unit
22
+ (three signals at `1/3` each, targeting 1R/2R/3R, to emulate scaling out
23
+ of a position). Default `1.0` is fully backward compatible.
24
+
25
+ #### `simulate` module
26
+ - **`SimulateConfig.tp_level_close_fractions`** — optional list, parallel to
27
+ `tp_levels`, that turns on **true partial closes** for `tp_mode="multi_rr"`.
28
+ Each entry is the fraction of the position's *original* size to realise
29
+ when the corresponding level is reached, instead of only moving the SL.
30
+ `None` (default) preserves the exact pre-0.7.3 behaviour (SL-only
31
+ advancement, single full close at the final level). When set:
32
+ - fractions summing to `1.0` give a full progressive scale-out
33
+ (e.g. `[1/3, 1/3, 1/3]`) — profit is banked level by level instead of
34
+ all-or-nothing at the end.
35
+ - all-zero fractions give a "let it run" position: the SL just walks
36
+ through every level and the full size stays open, trailing at the
37
+ last level's price, until eventually stopped out.
38
+ - any mix is valid — partially bank some levels, let others purely
39
+ advance the SL.
40
+
41
+ Each partial close produces its own `ClosedTrade` sharing the position's
42
+ `trade_id`, so `SimulateReport` statistics (win rate, profit factor,
43
+ equity curve) count every realised slice on its own line, the same way
44
+ a real scaled-out position would appear on a broker statement.
45
+ Commission (pre-charged once, in full, at entry) is split proportionally
46
+ across every slice so the total charged across one position's full
47
+ lifetime never changes.
48
+ - **`CLOSE_REASON_TP_PARTIAL`** is now exported from `AlgoTradeKit.simulate`.
49
+ It existed on `ClosedTrade` since an earlier release (with `is_tp`
50
+ already wired to recognise it) but no code path had ever produced it
51
+ until `tp_level_close_fractions` above.
52
+ - `run_multi()` gained the identical partial-close handling as the
53
+ single-pair `Simulate` engine, so shared-wallet multi-symbol portfolios
54
+ behave the same way; `run_batch()` needed no changes (it only wraps
55
+ `Simulate(cfg).run()`).
56
+
57
+ ### Tests
58
+ - `tests/test_strategy.py` — `TestSignal` extended with 5 tests for
59
+ `risk_multiplier` (default, custom value, `<= 0` validation, positional-
60
+ construction backward compatibility).
61
+ - `tests/test_simulate.py` — new `TestEngineRiskMultiplier` (4 tests) and
62
+ `TestEngineMultiRRPartialClose` (8 tests, including a hand-derived PnL
63
+ ground truth and an end-to-end wallet-conservation check) and
64
+ `TestSimulateConfigPartialTPValidation` (8 tests). All 171 pre-existing
65
+ tests in these two files continue to pass unmodified.
66
+
67
+ ---
68
+
10
69
  ## [0.7.2] — 2026-06-20
11
70
 
12
71
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AlgoTradeKit
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Algorithmic Trading Toolkit — data collection, indicators, strategy, simulation, and live trading
5
5
  Project-URL: Homepage, https://github.com/AmirMohammadBazdar/AlgoTradeKit
6
6
  Project-URL: Repository, https://github.com/AmirMohammadBazdar/AlgoTradeKit
@@ -308,12 +308,48 @@ report = Simulate(config).run(strategy_result)
308
308
  | `tp_mode` | `"signal"` | `"signal"/"fixed_rr"/"multi_rr"/"none"` |
309
309
  | `tp_rr` | 2.0 | R:R ratio for `fixed_rr` mode |
310
310
  | `tp_levels` | [1,2,3] | R levels for `multi_rr` mode |
311
+ | `tp_level_close_fractions` | `None` | **v0.7.3** Fraction of original size to realise at each `tp_levels` entry (`multi_rr` only) — see below |
311
312
  | `sl_mode` | `"signal"` | `"signal"` or `"trailing"` |
312
313
  | `risk_free_enabled` | False | Move SL to break-even at `risk_free_at_rr` |
313
314
  | `show_chart` | **False** | **v0.7.0** Open candle chart after run |
314
315
  | `report_mode` | **`"none"`** | **v0.7.0** Post-run report rendering |
315
316
  | `report_save_path` | `"report.html"` | **v0.7.0** HTML save path |
316
317
 
318
+ ### Per-Signal Sizing & Partial Take-Profit (v0.7.3)
319
+
320
+ `Signal.risk_multiplier` scales one signal's size relative to the run's
321
+ sizing config — useful for varying risk by context, or for splitting one
322
+ trade idea into several sub-positions whose sizes sum to one risk unit:
323
+
324
+ ```python
325
+ # Three signals at the same candle, each risking 1/3 of a normal trade,
326
+ # targeting 1R / 2R / 3R — equivalent to scaling out of one position.
327
+ for i, tp_r in enumerate([1, 2, 3], start=1):
328
+ signals.append(Signal(
329
+ direction="long", entry_price=entry, stop_loss=sl,
330
+ take_profit=entry + tp_r * (entry - sl),
331
+ timestamp=ts, candle_index=idx, timeframe="1h",
332
+ risk_multiplier=1 / 3,
333
+ ))
334
+ ```
335
+
336
+ `SimulateConfig.tp_level_close_fractions` turns `multi_rr` into a true
337
+ scale-out — each level realises a real, partial close instead of only
338
+ moving the SL:
339
+
340
+ ```python
341
+ config = SimulateConfig(
342
+ ...,
343
+ tp_mode="multi_rr",
344
+ tp_levels=[1.0, 2.0, 3.0],
345
+ tp_level_close_fractions=[1 / 3, 1 / 3, 1 / 3], # bank 1/3 at each level
346
+ )
347
+ ```
348
+
349
+ Set every fraction to `0.0` instead to get the opposite pattern — SL walks
350
+ through every level but nothing closes until the position is eventually
351
+ stopped out, letting winners run with a trailing stop and no fixed target.
352
+
317
353
  ### Batch Sweep
318
354
 
319
355
  ```python
@@ -562,9 +598,20 @@ Two browser tabs open:
562
598
  |---|---|---|
563
599
  | `TP_MODE_SIGNAL` | `"signal"` | Use Signal's take_profit |
564
600
  | `TP_MODE_FIXED_RR` | `"fixed_rr"` | entry ± `tp_rr` × SL distance |
565
- | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails |
601
+ | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails (and optionally partially closes — see `tp_level_close_fractions` *(v0.7.3)*) |
566
602
  | `TP_MODE_NONE` | `"none"` | No TP |
567
603
 
604
+ ### Close Reason Constants
605
+
606
+ | Constant | Value | Meaning |
607
+ |---|---|---|
608
+ | `CLOSE_REASON_SL` | `"sl"` | Stop loss hit |
609
+ | `CLOSE_REASON_TP` | `"tp"` | Take profit hit — nothing left open afterwards |
610
+ | `CLOSE_REASON_TP_PARTIAL` | `"tp_rr"` | **v0.7.3** Intermediate `multi_rr` level partially realised — position still open with reduced size |
611
+ | `CLOSE_REASON_RF` | `"rf"` | Risk-free / trailed SL hit |
612
+ | `CLOSE_REASON_FC` | `"force_close"` | Closed by an `ExitSignal` |
613
+ | `CLOSE_REASON_EOD` | `"end_of_data"` | Still open when data ran out |
614
+
568
615
  ### SL Mode Constants
569
616
 
570
617
  | Constant | Value | Behaviour |
@@ -252,12 +252,48 @@ report = Simulate(config).run(strategy_result)
252
252
  | `tp_mode` | `"signal"` | `"signal"/"fixed_rr"/"multi_rr"/"none"` |
253
253
  | `tp_rr` | 2.0 | R:R ratio for `fixed_rr` mode |
254
254
  | `tp_levels` | [1,2,3] | R levels for `multi_rr` mode |
255
+ | `tp_level_close_fractions` | `None` | **v0.7.3** Fraction of original size to realise at each `tp_levels` entry (`multi_rr` only) — see below |
255
256
  | `sl_mode` | `"signal"` | `"signal"` or `"trailing"` |
256
257
  | `risk_free_enabled` | False | Move SL to break-even at `risk_free_at_rr` |
257
258
  | `show_chart` | **False** | **v0.7.0** Open candle chart after run |
258
259
  | `report_mode` | **`"none"`** | **v0.7.0** Post-run report rendering |
259
260
  | `report_save_path` | `"report.html"` | **v0.7.0** HTML save path |
260
261
 
262
+ ### Per-Signal Sizing & Partial Take-Profit (v0.7.3)
263
+
264
+ `Signal.risk_multiplier` scales one signal's size relative to the run's
265
+ sizing config — useful for varying risk by context, or for splitting one
266
+ trade idea into several sub-positions whose sizes sum to one risk unit:
267
+
268
+ ```python
269
+ # Three signals at the same candle, each risking 1/3 of a normal trade,
270
+ # targeting 1R / 2R / 3R — equivalent to scaling out of one position.
271
+ for i, tp_r in enumerate([1, 2, 3], start=1):
272
+ signals.append(Signal(
273
+ direction="long", entry_price=entry, stop_loss=sl,
274
+ take_profit=entry + tp_r * (entry - sl),
275
+ timestamp=ts, candle_index=idx, timeframe="1h",
276
+ risk_multiplier=1 / 3,
277
+ ))
278
+ ```
279
+
280
+ `SimulateConfig.tp_level_close_fractions` turns `multi_rr` into a true
281
+ scale-out — each level realises a real, partial close instead of only
282
+ moving the SL:
283
+
284
+ ```python
285
+ config = SimulateConfig(
286
+ ...,
287
+ tp_mode="multi_rr",
288
+ tp_levels=[1.0, 2.0, 3.0],
289
+ tp_level_close_fractions=[1 / 3, 1 / 3, 1 / 3], # bank 1/3 at each level
290
+ )
291
+ ```
292
+
293
+ Set every fraction to `0.0` instead to get the opposite pattern — SL walks
294
+ through every level but nothing closes until the position is eventually
295
+ stopped out, letting winners run with a trailing stop and no fixed target.
296
+
261
297
  ### Batch Sweep
262
298
 
263
299
  ```python
@@ -506,9 +542,20 @@ Two browser tabs open:
506
542
  |---|---|---|
507
543
  | `TP_MODE_SIGNAL` | `"signal"` | Use Signal's take_profit |
508
544
  | `TP_MODE_FIXED_RR` | `"fixed_rr"` | entry ± `tp_rr` × SL distance |
509
- | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails |
545
+ | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails (and optionally partially closes — see `tp_level_close_fractions` *(v0.7.3)*) |
510
546
  | `TP_MODE_NONE` | `"none"` | No TP |
511
547
 
548
+ ### Close Reason Constants
549
+
550
+ | Constant | Value | Meaning |
551
+ |---|---|---|
552
+ | `CLOSE_REASON_SL` | `"sl"` | Stop loss hit |
553
+ | `CLOSE_REASON_TP` | `"tp"` | Take profit hit — nothing left open afterwards |
554
+ | `CLOSE_REASON_TP_PARTIAL` | `"tp_rr"` | **v0.7.3** Intermediate `multi_rr` level partially realised — position still open with reduced size |
555
+ | `CLOSE_REASON_RF` | `"rf"` | Risk-free / trailed SL hit |
556
+ | `CLOSE_REASON_FC` | `"force_close"` | Closed by an `ExitSignal` |
557
+ | `CLOSE_REASON_EOD` | `"end_of_data"` | Still open when data ran out |
558
+
512
559
  ### SL Mode Constants
513
560
 
514
561
  | Constant | Value | Behaviour |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "AlgoTradeKit"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "Algorithmic Trading Toolkit — data collection, indicators, strategy, simulation, and live trading"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -1,4 +1,4 @@
1
1
  """AlgoTradeKit — Algorithmic Trading Toolkit"""
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.7.3"
4
4
  __author__ = "Amir Mohammad Bazdar"
@@ -99,6 +99,7 @@ from ._position import (
99
99
  CLOSE_REASON_RF,
100
100
  CLOSE_REASON_SL,
101
101
  CLOSE_REASON_TP,
102
+ CLOSE_REASON_TP_PARTIAL,
102
103
  ClosedTrade,
103
104
  )
104
105
  from ._report import (
@@ -139,6 +140,7 @@ __all__ = [
139
140
  "ClosedTrade",
140
141
  "CLOSE_REASON_SL",
141
142
  "CLOSE_REASON_TP",
143
+ "CLOSE_REASON_TP_PARTIAL",
142
144
  "CLOSE_REASON_RF",
143
145
  "CLOSE_REASON_FC",
144
146
  "CLOSE_REASON_EOD",
@@ -121,6 +121,37 @@ class SimulateConfig:
121
121
  For ``"multi_rr"`` mode: list of R-multiples at which TP levels sit
122
122
  (default: ``[1.0, 2.0, 3.0]``). After each level is hit, SL moves
123
123
  to the previous level (0.0 = entry, 1.0 = first TP, etc.).
124
+ tp_level_close_fractions : list[float] | None
125
+ For ``"multi_rr"`` mode only. Optional list, same length as
126
+ ``tp_levels``, giving the fraction of the position's **original**
127
+ size to actually realise (partially close) when each corresponding
128
+ level is reached. Each value must be in ``[0.0, 1.0]`` and the
129
+ values must sum to ``<= 1.0``.
130
+
131
+ ``None`` (default) preserves the exact pre-v0.7.3 behaviour: no
132
+ level closes anything — the position's SL simply walks forward
133
+ through the levels, and only the *final* level fully closes the
134
+ position. This default is unchanged for full backward compatibility.
135
+
136
+ When provided, every level (including the last) closes **only**
137
+ the fraction you specify — there is no automatic "close everything"
138
+ special-case for the final level anymore. Two common patterns:
139
+
140
+ * **Scale out completely** — fractions summing to ``1.0``
141
+ (e.g. ``[1/3, 1/3, 1/3]`` for ``tp_levels=[1, 2, 3]``) realises
142
+ profit progressively and leaves nothing open after the last level.
143
+ * **Trail forever, never auto-realise** — all-zero fractions
144
+ (e.g. ``[0.0] * 20`` for ``tp_levels=[1, 2, ..., 20]``) never
145
+ partially closes anything; the SL just keeps stepping up through
146
+ every level and the full size stays open, trailing at the last
147
+ level's price, until it is eventually stopped out, force-closed,
148
+ or hits end-of-data.
149
+
150
+ Each partial close produces its own ``ClosedTrade`` (sharing the
151
+ same ``trade_id`` as the rest of that position's closes), so
152
+ ``SimulateReport`` statistics count each realised slice separately
153
+ — matching how a real scaled-out position would show up on a
154
+ broker statement.
124
155
  sl_mode : str
125
156
  Stop-loss mode: ``"signal"`` (use SL from Signal) or ``"trailing"``
126
157
  (default: ``"signal"``).
@@ -199,6 +230,7 @@ class SimulateConfig:
199
230
  tp_mode: str = TP_MODE_SIGNAL
200
231
  tp_rr: float = 2.0
201
232
  tp_levels: list[float] = field(default_factory=lambda: [1.0, 2.0, 3.0])
233
+ tp_level_close_fractions: list[float] | None = None
202
234
 
203
235
  # ------------------------------------------------------------------
204
236
  # Stop-loss
@@ -291,6 +323,26 @@ class SimulateConfig:
291
323
  raise ValueError(
292
324
  "SimulateConfig.tp_levels must be a non-empty list when tp_mode='multi_rr'."
293
325
  )
326
+ if self.tp_level_close_fractions is not None:
327
+ if self.tp_mode != TP_MODE_MULTI_RR:
328
+ raise ValueError(
329
+ "SimulateConfig.tp_level_close_fractions requires tp_mode='multi_rr'."
330
+ )
331
+ if len(self.tp_level_close_fractions) != len(self.tp_levels):
332
+ raise ValueError(
333
+ "SimulateConfig.tp_level_close_fractions must be the same length as "
334
+ f"tp_levels ({len(self.tp_levels)}), got "
335
+ f"{len(self.tp_level_close_fractions)}."
336
+ )
337
+ if any(not (0.0 <= f <= 1.0) for f in self.tp_level_close_fractions):
338
+ raise ValueError(
339
+ "SimulateConfig.tp_level_close_fractions values must all be in [0.0, 1.0]."
340
+ )
341
+ if sum(self.tp_level_close_fractions) > 1.0 + 1e-9:
342
+ raise ValueError(
343
+ "SimulateConfig.tp_level_close_fractions must sum to <= 1.0, got "
344
+ f"{sum(self.tp_level_close_fractions)!r}."
345
+ )
294
346
  if not self.config_id:
295
347
  self.config_id = self._auto_id()
296
348
  if self.report_mode not in (