opencode-pyneruntime 6.6.4__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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. pynecore/utils/stdlib_checker.py +17 -0
@@ -0,0 +1,669 @@
1
+ """
2
+ Strategy statistics calculation module for PyneCore.
3
+ Calculates comprehensive trading statistics similar to TradingView's Strategy Tester.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from dataclasses import dataclass
10
+
11
+ from ..types.na import NA
12
+ from ..lib.strategy import Trade
13
+ from .csv_file import CSVWriter
14
+ from ..lib.strategy import PositionBase
15
+
16
+
17
+ @dataclass
18
+ class StrategyStatistics:
19
+ """Complete strategy statistics matching TradingView's output"""
20
+
21
+ # Overview metrics
22
+ net_profit: float = 0.0
23
+ net_profit_percent: float = 0.0
24
+ gross_profit: float = 0.0
25
+ gross_profit_percent: float = 0.0
26
+ gross_loss: float = 0.0
27
+ gross_loss_percent: float = 0.0
28
+ max_equity_runup: float = 0.0
29
+ max_equity_runup_percent: float = 0.0
30
+ max_equity_drawdown: float = 0.0
31
+ max_equity_drawdown_percent: float = 0.0
32
+ unrealized_max_drawdown: float = 0.0
33
+ unrealized_max_drawdown_percent: float = 0.0
34
+ real_max_drawdown: float = 0.0
35
+ real_max_drawdown_percent: float = 0.0
36
+ buy_and_hold_return: float = 0.0
37
+ buy_and_hold_return_percent: float = 0.0
38
+ sharpe_ratio: float = 0.0
39
+ sortino_ratio: float = 0.0
40
+ profit_factor: float = 0.0
41
+
42
+ # Trade statistics
43
+ total_trades: int = 0
44
+ winning_trades: int = 0
45
+ losing_trades: int = 0
46
+ percent_profitable: float = 0.0
47
+ avg_trade: float = 0.0
48
+ avg_trade_percent: float = 0.0
49
+ avg_winning_trade: float = 0.0
50
+ avg_winning_trade_percent: float = 0.0
51
+ avg_losing_trade: float = 0.0
52
+ avg_losing_trade_percent: float = 0.0
53
+ largest_winning_trade: float = 0.0
54
+ largest_winning_trade_percent: float = 0.0
55
+ largest_losing_trade: float = 0.0
56
+ largest_losing_trade_percent: float = 0.0
57
+ avg_bars_in_trades: float = 0.0
58
+ avg_bars_in_winning_trades: float = 0.0
59
+ avg_bars_in_losing_trades: float = 0.0
60
+
61
+ # Long/Short breakdown
62
+ long_trades: int = 0
63
+ long_winning_trades: int = 0
64
+ long_net_profit: float = 0.0
65
+ long_net_profit_percent: float = 0.0
66
+ long_gross_profit: float = 0.0
67
+ long_gross_profit_percent: float = 0.0
68
+ long_gross_loss: float = 0.0
69
+ long_gross_loss_percent: float = 0.0
70
+ long_avg_trade: float = 0.0
71
+ long_avg_trade_percent: float = 0.0
72
+ long_largest_winning_trade: float = 0.0
73
+ long_largest_winning_trade_percent: float = 0.0
74
+ long_largest_losing_trade: float = 0.0
75
+ long_largest_losing_trade_percent: float = 0.0
76
+ long_avg_bars: float = 0.0
77
+
78
+ short_trades: int = 0
79
+ short_winning_trades: int = 0
80
+ short_net_profit: float = 0.0
81
+ short_net_profit_percent: float = 0.0
82
+ short_gross_profit: float = 0.0
83
+ short_gross_profit_percent: float = 0.0
84
+ short_gross_loss: float = 0.0
85
+ short_gross_loss_percent: float = 0.0
86
+ short_avg_trade: float = 0.0
87
+ short_avg_trade_percent: float = 0.0
88
+ short_largest_winning_trade: float = 0.0
89
+ short_largest_winning_trade_percent: float = 0.0
90
+ short_largest_losing_trade: float = 0.0
91
+ short_largest_losing_trade_percent: float = 0.0
92
+ short_avg_bars: float = 0.0
93
+
94
+ # P&L breakdown (realized / unrealized / total)
95
+ total_pnl: float = 0.0
96
+ total_pnl_percent: float = 0.0
97
+ realized_pnl: float = 0.0
98
+ realized_pnl_percent: float = 0.0
99
+ unrealized_pnl: float = 0.0
100
+ unrealized_pnl_percent: float = 0.0
101
+
102
+ # Other metrics
103
+ margin_calls: int = 0
104
+ max_contracts_held: float = 0.0
105
+ commission_paid: float = 0.0
106
+ total_open_trades: int = 0
107
+
108
+ # Drawdown metrics
109
+ max_cons_winning_trades: int = 0
110
+ max_cons_losing_trades: int = 0
111
+
112
+ # Additional ratio calculations
113
+ ratio_avg_win_loss: float = 0.0
114
+
115
+ def to_dict(self) -> dict[str, float | int]:
116
+ """Convert statistics to dictionary for CSV export"""
117
+ return {
118
+ # Overview
119
+ "Net Profit": self.net_profit,
120
+ "Net Profit %": self.net_profit_percent,
121
+ "Gross Profit": self.gross_profit,
122
+ "Gross Profit %": self.gross_profit_percent,
123
+ "Gross Loss": self.gross_loss,
124
+ "Gross Loss %": self.gross_loss_percent,
125
+ "Max Equity Run-up": self.max_equity_runup,
126
+ "Max Equity Run-up %": self.max_equity_runup_percent,
127
+ "Max Equity Drawdown": self.max_equity_drawdown,
128
+ "Max Equity Drawdown %": self.max_equity_drawdown_percent,
129
+ "Unrealized Max Drawdown": self.unrealized_max_drawdown,
130
+ "Unrealized Max Drawdown %": self.unrealized_max_drawdown_percent,
131
+ "Real Max Drawdown": self.real_max_drawdown,
132
+ "Real Max Drawdown %": self.real_max_drawdown_percent,
133
+ "Buy & Hold Return": self.buy_and_hold_return,
134
+ "Buy & Hold Return %": self.buy_and_hold_return_percent,
135
+ "Sharpe Ratio": self.sharpe_ratio,
136
+ "Sortino Ratio": self.sortino_ratio,
137
+ "Profit Factor": self.profit_factor,
138
+
139
+ # Trades
140
+ "Total Trades": self.total_trades,
141
+ "Winning Trades": self.winning_trades,
142
+ "Losing Trades": self.losing_trades,
143
+ "Percent Profitable": self.percent_profitable,
144
+ "Avg Trade": self.avg_trade,
145
+ "Avg Trade %": self.avg_trade_percent,
146
+ "Avg Winning Trade": self.avg_winning_trade,
147
+ "Avg Winning Trade %": self.avg_winning_trade_percent,
148
+ "Avg Losing Trade": self.avg_losing_trade,
149
+ "Avg Losing Trade %": self.avg_losing_trade_percent,
150
+ "Ratio Avg Win/Loss": self.ratio_avg_win_loss,
151
+ "Largest Winning Trade": self.largest_winning_trade,
152
+ "Largest Winning Trade %": self.largest_winning_trade_percent,
153
+ "Largest Losing Trade": self.largest_losing_trade,
154
+ "Largest Losing Trade %": self.largest_losing_trade_percent,
155
+ "Avg # Bars in Trades": self.avg_bars_in_trades,
156
+ "Avg # Bars in Winning Trades": self.avg_bars_in_winning_trades,
157
+ "Avg # Bars in Losing Trades": self.avg_bars_in_losing_trades,
158
+ "Max Consecutive Wins": self.max_cons_winning_trades,
159
+ "Max Consecutive Losses": self.max_cons_losing_trades,
160
+
161
+ # Long trades
162
+ "Long Trades": self.long_trades,
163
+ "Long Winning Trades": self.long_winning_trades,
164
+ "Long Net Profit": self.long_net_profit,
165
+ "Long Net Profit %": self.long_net_profit_percent,
166
+ "Long Gross Profit": self.long_gross_profit,
167
+ "Long Gross Profit %": self.long_gross_profit_percent,
168
+ "Long Gross Loss": self.long_gross_loss,
169
+ "Long Gross Loss %": self.long_gross_loss_percent,
170
+ "Long Avg Trade": self.long_avg_trade,
171
+ "Long Avg Trade %": self.long_avg_trade_percent,
172
+ "Long Largest Winning Trade": self.long_largest_winning_trade,
173
+ "Long Largest Winning Trade %": self.long_largest_winning_trade_percent,
174
+ "Long Largest Losing Trade": self.long_largest_losing_trade,
175
+ "Long Largest Losing Trade %": self.long_largest_losing_trade_percent,
176
+ "Long Avg # Bars": self.long_avg_bars,
177
+
178
+ # Short trades
179
+ "Short Trades": self.short_trades,
180
+ "Short Winning Trades": self.short_winning_trades,
181
+ "Short Net Profit": self.short_net_profit,
182
+ "Short Net Profit %": self.short_net_profit_percent,
183
+ "Short Gross Profit": self.short_gross_profit,
184
+ "Short Gross Profit %": self.short_gross_profit_percent,
185
+ "Short Gross Loss": self.short_gross_loss,
186
+ "Short Gross Loss %": self.short_gross_loss_percent,
187
+ "Short Avg Trade": self.short_avg_trade,
188
+ "Short Avg Trade %": self.short_avg_trade_percent,
189
+ "Short Largest Winning Trade": self.short_largest_winning_trade,
190
+ "Short Largest Winning Trade %": self.short_largest_winning_trade_percent,
191
+ "Short Largest Losing Trade": self.short_largest_losing_trade,
192
+ "Short Largest Losing Trade %": self.short_largest_losing_trade_percent,
193
+ "Short Avg # Bars": self.short_avg_bars,
194
+
195
+ # P&L breakdown
196
+ "Total P&L": self.total_pnl,
197
+ "Total P&L %": self.total_pnl_percent,
198
+ "Realized P&L": self.realized_pnl,
199
+ "Realized P&L %": self.realized_pnl_percent,
200
+ "Unrealized P&L": self.unrealized_pnl,
201
+ "Unrealized P&L %": self.unrealized_pnl_percent,
202
+
203
+ # Other
204
+ "Margin Calls": self.margin_calls,
205
+ "Max Contracts Held": self.max_contracts_held,
206
+ "Commission Paid": self.commission_paid,
207
+ "Total Open Trades": self.total_open_trades,
208
+ }
209
+
210
+
211
+ def calculate_strategy_statistics(
212
+ position: PositionBase,
213
+ initial_capital: float,
214
+ equity_curve: list[float] | None = None,
215
+ first_price: float | None = None,
216
+ last_price: float | None = None
217
+ ) -> StrategyStatistics:
218
+ """
219
+ Calculate comprehensive strategy statistics from position data.
220
+
221
+ :param position: PositionBase object containing all trade data
222
+ :param initial_capital: Initial capital for percentage calculations
223
+ :param equity_curve: List of equity values for Sharpe/Sortino calculations
224
+ :param first_price: First price for buy & hold calculation
225
+ :param last_price: Last price for buy & hold calculation
226
+ :return: StrategyStatistics object with all calculated metrics
227
+ """
228
+ stats = StrategyStatistics()
229
+
230
+ # Basic metrics from position
231
+ stats.net_profit = float(position.netprofit) if not isinstance(position.netprofit, NA) else 0.0
232
+ stats.gross_profit = float(position.grossprofit) if not isinstance(position.grossprofit, NA) else 0.0
233
+ stats.gross_loss = float(position.grossloss) if not isinstance(position.grossloss, NA) else 0.0
234
+ stats.max_equity_drawdown = float(position.max_drawdown) if not isinstance(position.max_drawdown, NA) else 0.0
235
+ stats.max_equity_runup = float(position.max_runup) if not isinstance(position.max_runup, NA) else 0.0
236
+ # Fork-parity drawdown family (percents already computed per-bar in Position)
237
+ stats.unrealized_max_drawdown = float(position.unrealized_max_drawdown)
238
+ stats.unrealized_max_drawdown_percent = float(position.unrealized_max_drawdown_percent)
239
+ stats.real_max_drawdown = float(position.real_max_drawdown)
240
+ stats.real_max_drawdown_percent = float(position.real_max_drawdown_percent)
241
+
242
+ # P&L breakdown: realized (closed) + unrealized (open) = total
243
+ stats.realized_pnl = stats.net_profit
244
+ stats.unrealized_pnl = float(position.openprofit) if not isinstance(position.openprofit, NA) else 0.0
245
+ stats.total_pnl = stats.realized_pnl + stats.unrealized_pnl
246
+
247
+ # Calculate percentages
248
+ if initial_capital > 0:
249
+ stats.net_profit_percent = (stats.net_profit / initial_capital) * 100
250
+ stats.gross_profit_percent = (stats.gross_profit / initial_capital) * 100
251
+ stats.gross_loss_percent = (stats.gross_loss / initial_capital) * 100
252
+ stats.max_equity_drawdown_percent = (stats.max_equity_drawdown / initial_capital) * 100
253
+ stats.max_equity_runup_percent = (stats.max_equity_runup / initial_capital) * 100
254
+ stats.realized_pnl_percent = (stats.realized_pnl / initial_capital) * 100
255
+ stats.unrealized_pnl_percent = (stats.unrealized_pnl / initial_capital) * 100
256
+ stats.total_pnl_percent = (stats.total_pnl / initial_capital) * 100
257
+
258
+ # Buy & Hold calculation
259
+ if first_price and last_price and first_price > 0:
260
+ buy_hold_shares = initial_capital / first_price
261
+ buy_hold_value = buy_hold_shares * last_price
262
+ stats.buy_and_hold_return = buy_hold_value - initial_capital
263
+ stats.buy_and_hold_return_percent = (stats.buy_and_hold_return / initial_capital) * 100
264
+
265
+ # Get all trades (closed + open)
266
+ all_trades: list[Trade] = list(position.closed_trades) + position.open_trades
267
+ closed_trades = list(position.closed_trades)
268
+
269
+ stats.total_trades = position.closed_trades_count
270
+ stats.winning_trades = position.wintrades
271
+ stats.losing_trades = position.losstrades
272
+ stats.total_open_trades = len(position.open_trades)
273
+
274
+ # Percent profitable
275
+ if stats.total_trades > 0:
276
+ stats.percent_profitable = (stats.winning_trades / stats.total_trades) * 100
277
+
278
+ # Profit factor
279
+ if stats.gross_loss != 0:
280
+ stats.profit_factor = abs(stats.gross_profit / stats.gross_loss)
281
+
282
+ # Calculate trade statistics
283
+ if closed_trades:
284
+ # Commission
285
+ stats.commission_paid = float(sum(trade.commission for trade in closed_trades))
286
+
287
+ # Average calculations
288
+ stats.avg_trade = stats.net_profit / len(closed_trades)
289
+ stats.avg_trade_percent = stats.net_profit_percent / len(closed_trades)
290
+
291
+ # Separate winning and losing trades
292
+ winning_trades = [t for t in closed_trades if float(t.profit) > 0]
293
+ losing_trades = [t for t in closed_trades if float(t.profit) < 0]
294
+
295
+ # Winning trades statistics
296
+ if winning_trades:
297
+ total_win_profit = sum(float(t.profit) for t in winning_trades)
298
+ stats.avg_winning_trade = total_win_profit / len(winning_trades)
299
+ stats.avg_winning_trade_percent = stats.avg_winning_trade / initial_capital * 100
300
+
301
+ # Largest winning trade
302
+ max_win = max(winning_trades, key=lambda t: float(t.profit))
303
+ stats.largest_winning_trade = float(max_win.profit)
304
+ stats.largest_winning_trade_percent = float(max_win.profit_percent)
305
+
306
+ # Average bars in winning trades
307
+ bars_in_wins = [t.exit_bar_index - t.entry_bar_index for t in winning_trades if t.exit_bar_index >= 0]
308
+ if bars_in_wins:
309
+ stats.avg_bars_in_winning_trades = sum(bars_in_wins) / len(bars_in_wins)
310
+
311
+ # Losing trades statistics
312
+ if losing_trades:
313
+ total_loss_profit = sum(float(t.profit) for t in losing_trades)
314
+ stats.avg_losing_trade = total_loss_profit / len(losing_trades)
315
+ stats.avg_losing_trade_percent = stats.avg_losing_trade / initial_capital * 100
316
+
317
+ # Largest losing trade
318
+ max_loss = min(losing_trades, key=lambda t: float(t.profit))
319
+ stats.largest_losing_trade = float(max_loss.profit)
320
+ stats.largest_losing_trade_percent = float(max_loss.profit_percent)
321
+
322
+ # Average bars in losing trades
323
+ bars_in_losses = [t.exit_bar_index - t.entry_bar_index for t in losing_trades if t.exit_bar_index >= 0]
324
+ if bars_in_losses:
325
+ stats.avg_bars_in_losing_trades = sum(bars_in_losses) / len(bars_in_losses)
326
+
327
+ # Ratio of average win to average loss
328
+ if stats.avg_losing_trade != 0:
329
+ stats.ratio_avg_win_loss = abs(stats.avg_winning_trade / stats.avg_losing_trade)
330
+
331
+ # Average bars in all trades
332
+ bars_in_trades = [t.exit_bar_index - t.entry_bar_index for t in closed_trades if t.exit_bar_index >= 0]
333
+ if bars_in_trades:
334
+ stats.avg_bars_in_trades = sum(bars_in_trades) / len(bars_in_trades)
335
+
336
+ # Long/Short breakdown
337
+ long_trades = [t for t in closed_trades if t.sign > 0]
338
+ short_trades = [t for t in closed_trades if t.sign < 0]
339
+
340
+ # Long statistics
341
+ if long_trades:
342
+ stats.long_trades = len(long_trades)
343
+ long_winning = [t for t in long_trades if float(t.profit) > 0]
344
+ long_losing = [t for t in long_trades if float(t.profit) < 0]
345
+
346
+ stats.long_winning_trades = len(long_winning)
347
+ stats.long_net_profit = sum(float(t.profit) for t in long_trades)
348
+ stats.long_net_profit_percent = (stats.long_net_profit / initial_capital) * 100
349
+
350
+ if long_winning:
351
+ stats.long_gross_profit = sum(float(t.profit) for t in long_winning)
352
+ stats.long_gross_profit_percent = (stats.long_gross_profit / initial_capital) * 100
353
+ max_long_win = max(long_winning, key=lambda t: float(t.profit))
354
+ stats.long_largest_winning_trade = float(max_long_win.profit)
355
+ stats.long_largest_winning_trade_percent = float(max_long_win.profit_percent)
356
+
357
+ if long_losing:
358
+ stats.long_gross_loss = sum(float(t.profit) for t in long_losing)
359
+ stats.long_gross_loss_percent = (stats.long_gross_loss / initial_capital) * 100
360
+ max_long_loss = min(long_losing, key=lambda t: float(t.profit))
361
+ stats.long_largest_losing_trade = float(max_long_loss.profit)
362
+ stats.long_largest_losing_trade_percent = float(max_long_loss.profit_percent)
363
+
364
+ stats.long_avg_trade = stats.long_net_profit / len(long_trades)
365
+ stats.long_avg_trade_percent = stats.long_net_profit_percent / len(long_trades)
366
+
367
+ long_bars = [t.exit_bar_index - t.entry_bar_index for t in long_trades if t.exit_bar_index >= 0]
368
+ if long_bars:
369
+ stats.long_avg_bars = sum(long_bars) / len(long_bars)
370
+
371
+ # Short statistics
372
+ if short_trades:
373
+ stats.short_trades = len(short_trades)
374
+ short_winning = [t for t in short_trades if float(t.profit) > 0]
375
+ short_losing = [t for t in short_trades if float(t.profit) < 0]
376
+
377
+ stats.short_winning_trades = len(short_winning)
378
+ stats.short_net_profit = sum(float(t.profit) for t in short_trades)
379
+ stats.short_net_profit_percent = (stats.short_net_profit / initial_capital) * 100
380
+
381
+ if short_winning:
382
+ stats.short_gross_profit = sum(float(t.profit) for t in short_winning)
383
+ stats.short_gross_profit_percent = (stats.short_gross_profit / initial_capital) * 100
384
+ max_short_win = max(short_winning, key=lambda t: float(t.profit))
385
+ stats.short_largest_winning_trade = float(max_short_win.profit)
386
+ stats.short_largest_winning_trade_percent = float(max_short_win.profit_percent)
387
+
388
+ if short_losing:
389
+ stats.short_gross_loss = sum(float(t.profit) for t in short_losing)
390
+ stats.short_gross_loss_percent = (stats.short_gross_loss / initial_capital) * 100
391
+ max_short_loss = min(short_losing, key=lambda t: float(t.profit))
392
+ stats.short_largest_losing_trade = float(max_short_loss.profit)
393
+ stats.short_largest_losing_trade_percent = float(max_short_loss.profit_percent)
394
+
395
+ stats.short_avg_trade = stats.short_net_profit / len(short_trades)
396
+ stats.short_avg_trade_percent = stats.short_net_profit_percent / len(short_trades)
397
+
398
+ short_bars = [t.exit_bar_index - t.entry_bar_index for t in short_trades if t.exit_bar_index >= 0]
399
+ if short_bars:
400
+ stats.short_avg_bars = sum(short_bars) / len(short_bars)
401
+
402
+ # Max consecutive wins/losses
403
+ if closed_trades:
404
+ current_wins = 0
405
+ current_losses = 0
406
+ max_wins = 0
407
+ max_losses = 0
408
+
409
+ for trade in closed_trades:
410
+ profit = float(trade.profit)
411
+ if profit > 0:
412
+ current_wins += 1
413
+ current_losses = 0
414
+ max_wins = max(max_wins, current_wins)
415
+ elif profit < 0:
416
+ current_losses += 1
417
+ current_wins = 0
418
+ max_losses = max(max_losses, current_losses)
419
+ else:
420
+ current_wins = 0
421
+ current_losses = 0
422
+
423
+ stats.max_cons_winning_trades = max_wins
424
+ stats.max_cons_losing_trades = max_losses
425
+
426
+ # Max contracts held
427
+ if all_trades:
428
+ max_size = 0.0
429
+ current_positions: list[Trade] = []
430
+
431
+ # Sort all trades by entry time
432
+ sorted_trades = sorted(all_trades, key=lambda t: t.entry_time)
433
+
434
+ for trade in sorted_trades:
435
+ # Add to current positions
436
+ current_positions.append(trade)
437
+
438
+ # Remove closed positions that exit before this entry
439
+ current_positions = [t for t in current_positions
440
+ if t.exit_time < 0 or t.exit_time > trade.entry_time]
441
+
442
+ # Calculate current size
443
+ current_size = sum(abs(t.size) for t in current_positions)
444
+ max_size = max(max_size, current_size)
445
+
446
+ stats.max_contracts_held = max_size
447
+
448
+ # Sharpe and Sortino ratios (if equity curve provided)
449
+ if equity_curve and len(equity_curve) > 1:
450
+ returns = []
451
+ for i in range(1, len(equity_curve)):
452
+ if equity_curve[i - 1] != 0:
453
+ ret = (equity_curve[i] - equity_curve[i - 1]) / equity_curve[i - 1]
454
+ returns.append(ret)
455
+
456
+ if returns:
457
+ avg_return = sum(returns) / len(returns)
458
+
459
+ # Sharpe ratio calculation
460
+ if len(returns) > 1:
461
+ variance = sum((r - avg_return) ** 2 for r in returns) / (len(returns) - 1)
462
+ std_dev = math.sqrt(variance)
463
+ if std_dev > 0:
464
+ # Annualized Sharpe ratio (assuming daily returns and 252 trading days)
465
+ stats.sharpe_ratio = (avg_return * 252) / (std_dev * math.sqrt(252))
466
+
467
+ # Sortino ratio calculation
468
+ downside_returns = [r for r in returns if r < 0]
469
+ if len(downside_returns) > 1:
470
+ downside_variance = sum(r ** 2 for r in downside_returns) / len(downside_returns)
471
+ downside_std = math.sqrt(downside_variance)
472
+ if downside_std > 0:
473
+ # Annualized Sortino ratio
474
+ stats.sortino_ratio = (avg_return * 252) / (downside_std * math.sqrt(252))
475
+
476
+ return stats
477
+
478
+
479
+ def write_strategy_statistics_csv(
480
+ stats: StrategyStatistics,
481
+ csv_writer: CSVWriter
482
+ ) -> None:
483
+ """
484
+ Write strategy statistics to CSV file in TradingView format.
485
+
486
+ :param stats: Calculated strategy statistics
487
+ :param csv_writer: CSV writer instance (already opened)
488
+ """
489
+ # P&L breakdown: Total / Realized / Unrealized (fork-parity)
490
+ csv_writer.write("Total P&L",
491
+ stats.total_pnl, stats.total_pnl_percent,
492
+ "", "", "", ""
493
+ )
494
+ csv_writer.write("Realized P&L",
495
+ stats.realized_pnl, stats.realized_pnl_percent,
496
+ "", "", "", ""
497
+ )
498
+ csv_writer.write("Unrealized P&L",
499
+ stats.unrealized_pnl, stats.unrealized_pnl_percent,
500
+ "", "", "", ""
501
+ )
502
+ # Row 1: Net profit
503
+ csv_writer.write("Net profit",
504
+ stats.net_profit, stats.net_profit_percent,
505
+ stats.long_net_profit, stats.long_net_profit_percent,
506
+ stats.short_net_profit, stats.short_net_profit_percent
507
+ )
508
+ # Row 2: Gross profit
509
+ csv_writer.write("Gross profit",
510
+ stats.gross_profit, stats.gross_profit_percent,
511
+ stats.long_gross_profit, stats.long_gross_profit_percent,
512
+ stats.short_gross_profit, stats.short_gross_profit_percent
513
+ )
514
+ # Row 3: Gross loss
515
+ csv_writer.write("Gross loss",
516
+ stats.gross_loss, stats.gross_loss_percent,
517
+ stats.long_gross_loss, stats.long_gross_loss_percent,
518
+ stats.short_gross_loss, stats.short_gross_loss_percent
519
+ )
520
+ # Row 4: Commission paid
521
+ csv_writer.write("Commission paid",
522
+ stats.commission_paid, "",
523
+ stats.commission_paid, "",
524
+ 0, ""
525
+ )
526
+ # Row 5: Buy & hold return
527
+ csv_writer.write("Buy & hold return",
528
+ stats.buy_and_hold_return, stats.buy_and_hold_return_percent,
529
+ "", "", "", ""
530
+ )
531
+ # Row 6: Max equity run-up
532
+ csv_writer.write("Max equity run-up",
533
+ stats.max_equity_runup, stats.max_equity_runup_percent,
534
+ "", "", "", ""
535
+ )
536
+ # Row 7: Max equity drawdown
537
+ csv_writer.write("Max equity drawdown",
538
+ stats.max_equity_drawdown, stats.max_equity_drawdown_percent,
539
+ "", "", "", ""
540
+ )
541
+ # Unrealized (intrabar) + Real max drawdown (fork-parity)
542
+ csv_writer.write("Unrealized max drawdown",
543
+ stats.unrealized_max_drawdown, stats.unrealized_max_drawdown_percent,
544
+ "", "", "", ""
545
+ )
546
+ csv_writer.write("Real max drawdown",
547
+ stats.real_max_drawdown, stats.real_max_drawdown_percent,
548
+ "", "", "", ""
549
+ )
550
+ # Row 8: Max contracts held
551
+ csv_writer.write("Max contracts held",
552
+ stats.max_contracts_held, "",
553
+ stats.max_contracts_held, "",
554
+ 0, ""
555
+ )
556
+
557
+ # Empty row
558
+ csv_writer.write("", "", "", "", "", "", "")
559
+
560
+ # Trade statistics section
561
+ csv_writer.write("Total trades",
562
+ stats.total_trades, "",
563
+ stats.long_trades, "",
564
+ stats.short_trades, ""
565
+ )
566
+ csv_writer.write("Total open trades",
567
+ stats.total_open_trades, "",
568
+ stats.total_open_trades, "",
569
+ 0, ""
570
+ )
571
+ csv_writer.write("Winning trades",
572
+ stats.winning_trades, "",
573
+ stats.long_winning_trades, "",
574
+ stats.short_winning_trades, ""
575
+ )
576
+ csv_writer.write("Losing trades",
577
+ stats.losing_trades, "",
578
+ stats.long_trades - stats.long_winning_trades, "",
579
+ stats.short_trades - stats.short_winning_trades, ""
580
+ )
581
+
582
+ # Calculate percentages with safe division
583
+ all_percent = stats.percent_profitable
584
+ long_percent = (stats.long_winning_trades / stats.long_trades * 100) if stats.long_trades > 0 else 0
585
+ short_percent = (stats.short_winning_trades / stats.short_trades * 100) if stats.short_trades > 0 else 0
586
+
587
+ csv_writer.write("Percent profitable",
588
+ "", all_percent,
589
+ "", long_percent,
590
+ "", short_percent
591
+ )
592
+ csv_writer.write("Avg P&L",
593
+ stats.avg_trade, stats.avg_trade_percent,
594
+ stats.long_avg_trade, stats.long_avg_trade_percent,
595
+ stats.short_avg_trade, stats.short_avg_trade_percent
596
+ )
597
+ csv_writer.write("Avg winning trade",
598
+ stats.avg_winning_trade, stats.avg_winning_trade_percent,
599
+ stats.avg_winning_trade, stats.avg_winning_trade_percent,
600
+ 0, ""
601
+ )
602
+ csv_writer.write("Avg losing trade",
603
+ stats.avg_losing_trade, stats.avg_losing_trade_percent,
604
+ stats.avg_losing_trade, stats.avg_losing_trade_percent,
605
+ 0, ""
606
+ )
607
+ csv_writer.write("Ratio avg win / avg loss",
608
+ stats.ratio_avg_win_loss, "",
609
+ stats.ratio_avg_win_loss, "",
610
+ 0, ""
611
+ )
612
+ csv_writer.write("Largest winning trade",
613
+ stats.largest_winning_trade, "",
614
+ stats.long_largest_winning_trade, "",
615
+ stats.short_largest_winning_trade, ""
616
+ )
617
+ csv_writer.write("Largest winning trade percent",
618
+ "", stats.largest_winning_trade_percent,
619
+ "", stats.long_largest_winning_trade_percent,
620
+ "", stats.short_largest_winning_trade_percent
621
+ )
622
+ csv_writer.write("Largest losing trade",
623
+ stats.largest_losing_trade, "",
624
+ stats.long_largest_losing_trade, "",
625
+ stats.short_largest_losing_trade, ""
626
+ )
627
+ csv_writer.write("Largest losing trade percent",
628
+ "", stats.largest_losing_trade_percent,
629
+ "", stats.long_largest_losing_trade_percent,
630
+ "", stats.short_largest_losing_trade_percent
631
+ )
632
+ csv_writer.write("Avg # bars in trades",
633
+ stats.avg_bars_in_trades, "",
634
+ stats.long_avg_bars, "",
635
+ stats.short_avg_bars, ""
636
+ )
637
+ csv_writer.write("Avg # bars in winning trades",
638
+ stats.avg_bars_in_winning_trades, "",
639
+ stats.avg_bars_in_winning_trades, "",
640
+ 0, ""
641
+ )
642
+ csv_writer.write("Avg # bars in losing trades",
643
+ stats.avg_bars_in_losing_trades, "",
644
+ stats.avg_bars_in_losing_trades, "",
645
+ 0, ""
646
+ )
647
+
648
+ # Empty row
649
+ csv_writer.write("", "", "", "", "", "", "")
650
+
651
+ # Additional statistics
652
+ csv_writer.write("Sharpe ratio",
653
+ stats.sharpe_ratio, "",
654
+ "", "", "", ""
655
+ )
656
+ csv_writer.write("Sortino ratio",
657
+ stats.sortino_ratio, "",
658
+ "", "", "", ""
659
+ )
660
+ csv_writer.write("Profit factor",
661
+ stats.profit_factor, "",
662
+ stats.profit_factor, "",
663
+ 0, ""
664
+ )
665
+ csv_writer.write("Margin calls",
666
+ stats.margin_calls, "",
667
+ stats.margin_calls, "",
668
+ stats.margin_calls, ""
669
+ )