AlgoTradeKit 0.7.2__tar.gz → 0.7.4__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.4}/CHANGELOG.md +171 -0
  2. {algotradekit-0.7.2 → algotradekit-0.7.4}/PKG-INFO +80 -3
  3. {algotradekit-0.7.2 → algotradekit-0.7.4}/README.md +79 -2
  4. {algotradekit-0.7.2 → algotradekit-0.7.4}/pyproject.toml +1 -1
  5. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/__init__.py +1 -1
  6. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/__init__.py +2 -0
  7. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_config.py +52 -0
  8. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_engine.py +335 -86
  9. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_position.py +61 -7
  10. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_report.py +15 -0
  11. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_runner.py +12 -9
  12. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/strategy/_types.py +24 -0
  13. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/visual/chart.py +222 -0
  14. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/visual/indicator_renderer.py +230 -21
  15. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/visual/models.py +18 -16
  16. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/visual/static/index.html +267 -0
  17. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_simulate.py +680 -0
  18. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_strategy.py +21 -0
  19. {algotradekit-0.7.2 → algotradekit-0.7.4}/.github/workflows/publish.yml +0 -0
  20. {algotradekit-0.7.2 → algotradekit-0.7.4}/.gitignore +0 -0
  21. {algotradekit-0.7.2 → algotradekit-0.7.4}/LICENSE +0 -0
  22. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/__init__.py +0 -0
  23. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/_utils.py +0 -0
  24. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/collector.py +0 -0
  25. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/converter.py +0 -0
  26. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/normalizer.py +0 -0
  27. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/sources/__init__.py +0 -0
  28. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/sources/base.py +0 -0
  29. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/sources/binance.py +0 -0
  30. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/storage/__init__.py +0 -0
  31. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/data/storage/csv_handler.py +0 -0
  32. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/__init__.py +0 -0
  33. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/_base.py +0 -0
  34. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/atr.py +0 -0
  35. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/ichimoku.py +0 -0
  36. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/ma.py +0 -0
  37. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/macd.py +0 -0
  38. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/indicator/rsi.py +0 -0
  39. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/report/__init__.py +0 -0
  40. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/report/_builder.py +0 -0
  41. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/report/_display.py +0 -0
  42. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/report/_server.py +0 -0
  43. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/report/static/report.html +0 -0
  44. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_lot.py +0 -0
  45. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/simulate/_session.py +0 -0
  46. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/strategy/__init__.py +0 -0
  47. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/strategy/_base.py +0 -0
  48. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/strategy/builtin/__init__.py +0 -0
  49. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/strategy/builtin/macd.py +0 -0
  50. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/visual/__init__.py +0 -0
  51. {algotradekit-0.7.2 → algotradekit-0.7.4}/src/AlgoTradeKit/visual/server.py +0 -0
  52. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_data.py +0 -0
  53. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_data_converter.py +0 -0
  54. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_data_normalizer.py +0 -0
  55. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_indicator.py +0 -0
  56. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_report.py +0 -0
  57. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_visual.py +0 -0
  58. {algotradekit-0.7.2 → algotradekit-0.7.4}/tests/test_visual__0_4_1.py +0 -0
@@ -7,6 +7,177 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.7.4] — 2026-06-24
11
+
12
+ ### Added
13
+
14
+ #### `simulate` module — dynamic SL/TP visualisation
15
+
16
+ - **`ClosedTrade.sl_history`** — new `tuple` field recording every SL-state
17
+ change throughout a trade's lifetime as an ordered sequence of dicts
18
+ `{"time": int_ms, "sl": float, "next_tp": float | None}`. An entry is
19
+ appended at position open (initial state) and again every time the SL
20
+ advances: after each `multi_rr` TP level hit (SL walks to break-even /
21
+ previous TP) or whenever the trailing SL moves to a new high-water mark.
22
+ An empty history means the SL never moved; a single entry means the
23
+ position opened but the SL never advanced before closing.
24
+
25
+ - **`ClosedTrade.final_next_tp`** — new `float | None` field that records
26
+ the TP target that was active at the exact moment the trade closed. For
27
+ a `multi_rr` position that hit TP3 (moving next target to TP4) and was
28
+ then stopped out at the TP3-advanced SL, this is TP4. `None` when all
29
+ levels were consumed (position closed at the final TP) or no TP was
30
+ configured.
31
+
32
+ - **`ClosedTrade.peak_price`** — new `float` field storing the most
33
+ favourable price reached during the trade (highest high for longs, lowest
34
+ low for shorts). Used by the visual module as the top of the position box
35
+ for trailing-SL trades (the peak marks the last price that drove the
36
+ trailing SL to its tightest level).
37
+
38
+ - **`SimulateReport.trade_markers`** now includes `sl_history`, `final_next_tp`,
39
+ `peak_price`, and `rr_levels_hit` for every marker, enabling the
40
+ chart-side rendering of dynamic SL/TP lines.
41
+
42
+ #### `visual` module — dynamic SL/TP lines on chart
43
+
44
+ - **`add_simulation_positions`** (in `indicator_renderer.py`) enhanced with:
45
+ - **Dynamic SL line segments** — for `multi_rr` and `trailing` positions,
46
+ coloured horizontal `TrendLine` drawings are added alongside each position
47
+ box showing where the stop-loss was at each stage of the trade:
48
+ - **Red** — SL is in the loss zone (below entry for long)
49
+ - **Amber/yellow** — SL is at break-even (entry price)
50
+ - **Cyan** — SL is in profit territory (above entry for long)
51
+ - **Green TP lines** (`multi_rr` only) — for each SL segment, a matching
52
+ green horizontal line shows the next TP target that was being aimed at.
53
+ - **Posbox TP fix** — the profit-zone boundary of the position box now
54
+ shows `final_next_tp` (the TP at closing time) rather than the initial
55
+ TP at entry. After hitting TP3 with next target TP4, the box correctly
56
+ displays TP4 instead of staying stuck at TP1.
57
+ - **Trailing posbox top** — for `sl_mode="trailing"`, the top of the
58
+ position box is set to `peak_price` (the highest price the trailing SL
59
+ ever chased), giving a natural view of the profit zone that was available.
60
+ - **Partial-close grouping** — multiple `ClosedTrade` rows sharing a
61
+ `trade_id` (from `tp_level_close_fractions`) are now collapsed into a
62
+ single position box (one box per unique trade, not one per slice).
63
+ - New `config` parameter on `add_simulation_positions` — pass the
64
+ `SimulateConfig` used for the simulation to enable automatic mode
65
+ detection (`tp_mode`/`sl_mode`) for the correct line style.
66
+
67
+ - **Indicator toolbar** — a new 📈 button in the chart toolbar opens an
68
+ "Add Indicator" panel where the user can add technical indicators to the
69
+ live candle chart without writing any code:
70
+ - **Moving averages**: EMA, SMA, WMA, SMMA, DEMA, TEMA, HMA, VWMA, VWAP
71
+ - **Oscillators**: RSI (with optional MA), MACD, ATR (pure Python — no
72
+ third-party TA library)
73
+ - **Trend**: Ichimoku Cloud
74
+ - Each indicator type shows its own parameter form (period, source, MA
75
+ type, fast/slow/signal periods, Tenkan/Kijun/Senkou B, etc.).
76
+ - A colour picker lets the user override the default series colour.
77
+ - The "Add to Chart" button sends a `compute_indicator` WebSocket message
78
+ to the `Chart` server, which computes the indicator from the stored OHLCV
79
+ `DataFrame` and broadcasts the result as one or more `add_indicator`
80
+ messages; the chart renders them immediately.
81
+
82
+ ### Fixed
83
+
84
+ - **Trailing SL false gap-open trigger** — the trailing SL was previously
85
+ advanced in step A (before close checks), which could cause a "gap open
86
+ past SL" close on the very candle that caused the SL to jump, when the
87
+ candle's open price was between the old and new SL levels. The trailing
88
+ SL is now advanced inside `_check_close` *after* the gap-open test, so
89
+ the gap check always uses the SL from the end of the previous candle.
90
+ This fix applies to both the single-pair `Simulate` engine and the
91
+ multi-pair `run_multi` engine (`_runner.py`).
92
+
93
+ - **Chart and report tabs disconnecting immediately** — the
94
+ `ChartServer` and `ReportServer` both run in daemon threads, which Python
95
+ kills the instant the script's main thread exits. Previously,
96
+ `Simulate.run()` started both servers non-blocking and returned, so all
97
+ user code after the call (printing stats, saving CSV history, etc.) ran
98
+ normally — but as soon as `main()` returned the process died, closing both
99
+ browser tabs before the user could interact with them. A one-time `atexit`
100
+ handler is now registered whenever a browser UI is opened; it blocks with
101
+ a `while True: sleep(1)` loop (identical to `chart.show(block=True)`) after
102
+ all user code has finished, keeping the process alive until the user presses
103
+ **Ctrl+C**. The handler registers only once per process invocation, so
104
+ multiple `Simulate.run()` calls (batch/multi-symbol) produce a single
105
+ blocking handler at exit.
106
+
107
+ ### Tests
108
+
109
+ - `tests/test_simulate.py` — new `TestSlHistory` class (13 tests) covering:
110
+ - `sl_history` length and content after 0, 1, and 2 `multi_rr` TP hits
111
+ - timestamp presence and ordering in `sl_history`
112
+ - `final_next_tp` value after partial and final TP consumption
113
+ - `peak_price` captured correctly for trailing-SL trades
114
+ - backward-compatibility: plain `signal` SL mode produces exactly one
115
+ `sl_history` entry (no SL movement)
116
+ - `add_simulation_positions` smoke test: one posbox per unique `trade_id`
117
+ - dynamic SL `TrendLine` drawings added when `sl_history` length > 1
118
+ - posbox `take_profit` uses `final_next_tp` not initial TP
119
+
120
+ ---
121
+
122
+ ## [0.7.3] — 2026-06-21
123
+
124
+ ### Added
125
+
126
+ #### `strategy` module
127
+ - **`Signal.risk_multiplier`** — new optional field (default `1.0`) that scales
128
+ the size/risk a single signal receives relative to the run's global
129
+ `SimulateConfig` sizing (`risk_percent` / `fixed_amount` / `fixed_lot`).
130
+ Lets a strategy vary risk per signal instead of using one fixed sizing
131
+ rule for every trade in a backtest — e.g. half size on a tentative
132
+ "limit" entry vs. full size on a confirmed one, or splitting one trade
133
+ idea into several sub-positions whose sizes sum to one risk unit
134
+ (three signals at `1/3` each, targeting 1R/2R/3R, to emulate scaling out
135
+ of a position). Default `1.0` is fully backward compatible.
136
+
137
+ #### `simulate` module
138
+ - **`SimulateConfig.tp_level_close_fractions`** — optional list, parallel to
139
+ `tp_levels`, that turns on **true partial closes** for `tp_mode="multi_rr"`.
140
+ Each entry is the fraction of the position's *original* size to realise
141
+ when the corresponding level is reached, instead of only moving the SL.
142
+ `None` (default) preserves the exact pre-0.7.3 behaviour (SL-only
143
+ advancement, single full close at the final level). When set:
144
+ - fractions summing to `1.0` give a full progressive scale-out
145
+ (e.g. `[1/3, 1/3, 1/3]`) — profit is banked level by level instead of
146
+ all-or-nothing at the end.
147
+ - all-zero fractions give a "let it run" position: the SL just walks
148
+ through every level and the full size stays open, trailing at the
149
+ last level's price, until eventually stopped out.
150
+ - any mix is valid — partially bank some levels, let others purely
151
+ advance the SL.
152
+
153
+ Each partial close produces its own `ClosedTrade` sharing the position's
154
+ `trade_id`, so `SimulateReport` statistics (win rate, profit factor,
155
+ equity curve) count every realised slice on its own line, the same way
156
+ a real scaled-out position would appear on a broker statement.
157
+ Commission (pre-charged once, in full, at entry) is split proportionally
158
+ across every slice so the total charged across one position's full
159
+ lifetime never changes.
160
+ - **`CLOSE_REASON_TP_PARTIAL`** is now exported from `AlgoTradeKit.simulate`.
161
+ It existed on `ClosedTrade` since an earlier release (with `is_tp`
162
+ already wired to recognise it) but no code path had ever produced it
163
+ until `tp_level_close_fractions` above.
164
+ - `run_multi()` gained the identical partial-close handling as the
165
+ single-pair `Simulate` engine, so shared-wallet multi-symbol portfolios
166
+ behave the same way; `run_batch()` needed no changes (it only wraps
167
+ `Simulate(cfg).run()`).
168
+
169
+ ### Tests
170
+ - `tests/test_strategy.py` — `TestSignal` extended with 5 tests for
171
+ `risk_multiplier` (default, custom value, `<= 0` validation, positional-
172
+ construction backward compatibility).
173
+ - `tests/test_simulate.py` — new `TestEngineRiskMultiplier` (4 tests) and
174
+ `TestEngineMultiRRPartialClose` (8 tests, including a hand-derived PnL
175
+ ground truth and an end-to-end wallet-conservation check) and
176
+ `TestSimulateConfigPartialTPValidation` (8 tests). All 171 pre-existing
177
+ tests in these two files continue to pass unmodified.
178
+
179
+ ---
180
+
10
181
  ## [0.7.2] — 2026-06-20
11
182
 
12
183
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AlgoTradeKit
3
- Version: 0.7.2
3
+ Version: 0.7.4
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
@@ -425,10 +461,25 @@ TradingView-style position boxes showing SL/TP zones with R:R labels:
425
461
  from AlgoTradeKit.visual.indicator_renderer import add_simulation_positions
426
462
 
427
463
  chart = Chart.from_csv("data/binance-futures_BTCUSDT_1h.csv")
428
- add_simulation_positions(chart, report, opacity=0.15)
464
+ add_simulation_positions(chart, report, opacity=0.15, config=sim_config)
429
465
  chart.show(block=True)
430
466
  ```
431
467
 
468
+ **Dynamic SL/TP lines *(v0.7.4)*** — when `config` is passed, coloured
469
+ horizontal lines are drawn on the chart for every trade where the SL moved:
470
+
471
+ | Colour | Meaning |
472
+ |--------|---------|
473
+ | 🔴 Red | SL is in the loss zone (below entry for long) |
474
+ | 🟡 Amber | SL is at break-even (entry price) |
475
+ | 🔵 Cyan | SL is in profit territory |
476
+ | 🟢 Green | Next TP target (`multi_rr` only) |
477
+
478
+ The position box's profit-zone boundary is also corrected to show the TP
479
+ target that was active at the *moment of closing* rather than the initial TP
480
+ at entry. For trailing-SL trades the box top is set to `peak_price` (the
481
+ highest price the trailing SL ever chased).
482
+
432
483
  Manual single box:
433
484
  ```python
434
485
  chart.add_position_box(
@@ -439,6 +490,21 @@ chart.add_position_box(
439
490
  )
440
491
  ```
441
492
 
493
+ ### Indicator Toolbar *(v0.7.4)*
494
+
495
+ The chart toolbar now has an **Add Indicator** (📈) button. Clicking it
496
+ opens a panel where indicators can be added interactively at runtime — no
497
+ code required:
498
+
499
+ - **Moving averages**: EMA, SMA, WMA, SMMA, DEMA, TEMA, HMA, VWMA, VWAP
500
+ - **Oscillators**: RSI (with optional MA), MACD, ATR
501
+ - **Trend**: Ichimoku Cloud
502
+
503
+ Each type shows its own parameter form. After clicking *Add to Chart*, the
504
+ server computes the indicator from the OHLCV data and the chart updates
505
+ immediately. Indicators added this way work exactly like those added via
506
+ `add_ma()` / `add_rsi()` / etc. in Python code.
507
+
442
508
  ### Strategy Drawings (v0.7.0)
443
509
 
444
510
  ```python
@@ -562,9 +628,20 @@ Two browser tabs open:
562
628
  |---|---|---|
563
629
  | `TP_MODE_SIGNAL` | `"signal"` | Use Signal's take_profit |
564
630
  | `TP_MODE_FIXED_RR` | `"fixed_rr"` | entry ± `tp_rr` × SL distance |
565
- | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails |
631
+ | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails (and optionally partially closes — see `tp_level_close_fractions` *(v0.7.3)*) |
566
632
  | `TP_MODE_NONE` | `"none"` | No TP |
567
633
 
634
+ ### Close Reason Constants
635
+
636
+ | Constant | Value | Meaning |
637
+ |---|---|---|
638
+ | `CLOSE_REASON_SL` | `"sl"` | Stop loss hit |
639
+ | `CLOSE_REASON_TP` | `"tp"` | Take profit hit — nothing left open afterwards |
640
+ | `CLOSE_REASON_TP_PARTIAL` | `"tp_rr"` | **v0.7.3** Intermediate `multi_rr` level partially realised — position still open with reduced size |
641
+ | `CLOSE_REASON_RF` | `"rf"` | Risk-free / trailed SL hit |
642
+ | `CLOSE_REASON_FC` | `"force_close"` | Closed by an `ExitSignal` |
643
+ | `CLOSE_REASON_EOD` | `"end_of_data"` | Still open when data ran out |
644
+
568
645
  ### SL Mode Constants
569
646
 
570
647
  | 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
@@ -369,10 +405,25 @@ TradingView-style position boxes showing SL/TP zones with R:R labels:
369
405
  from AlgoTradeKit.visual.indicator_renderer import add_simulation_positions
370
406
 
371
407
  chart = Chart.from_csv("data/binance-futures_BTCUSDT_1h.csv")
372
- add_simulation_positions(chart, report, opacity=0.15)
408
+ add_simulation_positions(chart, report, opacity=0.15, config=sim_config)
373
409
  chart.show(block=True)
374
410
  ```
375
411
 
412
+ **Dynamic SL/TP lines *(v0.7.4)*** — when `config` is passed, coloured
413
+ horizontal lines are drawn on the chart for every trade where the SL moved:
414
+
415
+ | Colour | Meaning |
416
+ |--------|---------|
417
+ | 🔴 Red | SL is in the loss zone (below entry for long) |
418
+ | 🟡 Amber | SL is at break-even (entry price) |
419
+ | 🔵 Cyan | SL is in profit territory |
420
+ | 🟢 Green | Next TP target (`multi_rr` only) |
421
+
422
+ The position box's profit-zone boundary is also corrected to show the TP
423
+ target that was active at the *moment of closing* rather than the initial TP
424
+ at entry. For trailing-SL trades the box top is set to `peak_price` (the
425
+ highest price the trailing SL ever chased).
426
+
376
427
  Manual single box:
377
428
  ```python
378
429
  chart.add_position_box(
@@ -383,6 +434,21 @@ chart.add_position_box(
383
434
  )
384
435
  ```
385
436
 
437
+ ### Indicator Toolbar *(v0.7.4)*
438
+
439
+ The chart toolbar now has an **Add Indicator** (📈) button. Clicking it
440
+ opens a panel where indicators can be added interactively at runtime — no
441
+ code required:
442
+
443
+ - **Moving averages**: EMA, SMA, WMA, SMMA, DEMA, TEMA, HMA, VWMA, VWAP
444
+ - **Oscillators**: RSI (with optional MA), MACD, ATR
445
+ - **Trend**: Ichimoku Cloud
446
+
447
+ Each type shows its own parameter form. After clicking *Add to Chart*, the
448
+ server computes the indicator from the OHLCV data and the chart updates
449
+ immediately. Indicators added this way work exactly like those added via
450
+ `add_ma()` / `add_rsi()` / etc. in Python code.
451
+
386
452
  ### Strategy Drawings (v0.7.0)
387
453
 
388
454
  ```python
@@ -506,9 +572,20 @@ Two browser tabs open:
506
572
  |---|---|---|
507
573
  | `TP_MODE_SIGNAL` | `"signal"` | Use Signal's take_profit |
508
574
  | `TP_MODE_FIXED_RR` | `"fixed_rr"` | entry ± `tp_rr` × SL distance |
509
- | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails |
575
+ | `TP_MODE_MULTI_RR` | `"multi_rr"` | Multiple levels, SL trails (and optionally partially closes — see `tp_level_close_fractions` *(v0.7.3)*) |
510
576
  | `TP_MODE_NONE` | `"none"` | No TP |
511
577
 
578
+ ### Close Reason Constants
579
+
580
+ | Constant | Value | Meaning |
581
+ |---|---|---|
582
+ | `CLOSE_REASON_SL` | `"sl"` | Stop loss hit |
583
+ | `CLOSE_REASON_TP` | `"tp"` | Take profit hit — nothing left open afterwards |
584
+ | `CLOSE_REASON_TP_PARTIAL` | `"tp_rr"` | **v0.7.3** Intermediate `multi_rr` level partially realised — position still open with reduced size |
585
+ | `CLOSE_REASON_RF` | `"rf"` | Risk-free / trailed SL hit |
586
+ | `CLOSE_REASON_FC` | `"force_close"` | Closed by an `ExitSignal` |
587
+ | `CLOSE_REASON_EOD` | `"end_of_data"` | Still open when data ran out |
588
+
512
589
  ### SL Mode Constants
513
590
 
514
591
  | 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.4"
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.4"
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 (