Skip to content

Commit 461e338

Browse files
committed
feat: rename equity_path to trade_path and fix strategy statistics
Breaking changes: - Rename equity_path parameter to trade_path in ScriptRunner and CLI - Change CLI flags from --equity/-ep to --trade/-tp - Update default filename from _equity.csv to _trade.csv - Fix strategy statistics CSV export with proper headers and numeric values - Add comprehensive TradingView-compatible statistics calculation - Version bump to 6.1.0 for breaking API changes
1 parent d78e01f commit 461e338

File tree

5 files changed

+734
-61
lines changed

5 files changed

+734
-61
lines changed

docs/cli/run.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: "Running Scripts"
55
description: "Running PyneCore scripts from the command line"
66
icon: "play_circle"
77
date: "2025-03-31"
8-
lastmod: "2025-03-31"
8+
lastmod: "2025-07-24"
99
draft: false
1010
toc: true
1111
categories: ["Usage", "CLI", "Scripting"]
@@ -73,12 +73,12 @@ pyne run my_strategy.py eurusd_data.ohlcv --from "2023-01-01" --to "2023-12-31"
7373

7474
- `--plot`, `-pp`: Path to save the plot data (CSV format). If not specified, it will be saved as `<script_name>.csv` in the `workdir/output/` directory.
7575
- `--strat`, `-sp`: Path to save the strategy statistics (CSV format). If not specified, it will be saved as `<script_name>_strat.csv` in the `workdir/output/` directory.
76-
- `--equity`, `-ep`: Path to save the equity curve (CSV format). If not specified, it will be saved as `<script_name>_equity.csv` in the `workdir/output/` directory.
76+
- `--trade`, `-tp`: Path to save the trade data (CSV format). If not specified, it will be saved as `<script_name>_trade.csv` in the `workdir/output/` directory.
7777

7878
Example:
7979
```bash
8080
# Specify custom output paths
81-
pyne run my_strategy.py eurusd_data.ohlcv --plot custom_plot.csv --strat custom_stats.csv
81+
pyne run my_strategy.py eurusd_data.ohlcv --plot custom_plot.csv --strat custom_stats.csv --trade custom_trades.csv
8282
```
8383

8484
## Symbol Information
@@ -120,18 +120,34 @@ After the script execution completes, several output files are created:
120120

121121
Contains the values plotted by the script for each bar. This includes all values passed to `plot()` functions in your script.
122122

123+
**Default filename**: `<script_name>.csv`
124+
123125
### Strategy Statistics (CSV)
124126

125-
If your script is a strategy, this file contains detailed statistics about the trading performance, including:
126-
- Total profit/loss
127-
- Win rate
128-
- Maximum drawdown
129-
- Sharpe ratio
130-
- Trade details
127+
If your script is a strategy, this file contains comprehensive TradingView-compatible statistics about the trading performance, including:
128+
- **Overview metrics**: Net profit, gross profit/loss, max equity runup/drawdown, buy & hold return
129+
- **Performance ratios**: Sharpe ratio, Sortino ratio, profit factor
130+
- **Trade statistics**: Total/winning/losing trades, percent profitable, average trade metrics
131+
- **Position analysis**: Largest winning/losing trades, average bars in trades
132+
- **Long/Short breakdown**: Separate statistics for long and short positions
133+
- **Risk metrics**: Max contracts held, commission paid, margin calls
134+
135+
**Default filename**: `<script_name>_strat.csv`
136+
137+
### Trade Data (CSV)
138+
139+
If your script is a strategy, this file contains detailed trade-by-trade data with entry and exit records:
140+
- **Trade information**: Trade number, bar index, entry/exit type, signal ID
141+
- **Timing data**: Date/time of entry and exit
142+
- **Price data**: Entry/exit prices in the symbol's currency
143+
- **Position data**: Number of contracts traded
144+
- **Performance metrics**: Profit/loss in currency and percentage
145+
- **Cumulative tracking**: Running totals of profit and profit percentage
146+
- **Risk analysis**: Maximum run-up and drawdown for each trade
131147

132-
### Equity Curve (CSV)
148+
**Default filename**: `<script_name>_trade.csv`
133149

134-
If your script is a strategy, this file contains the equity curve data showing how the account balance changed over time.
150+
**Note**: This file exports individual trade records (entry/exit pairs), not the equity curve. The equity curve is tracked internally for statistics calculation.
135151

136152
## Examples
137153

@@ -156,7 +172,7 @@ pyne run my_strategy.py eurusd_data.ohlcv --from "2023-03-01" --to "2023-03-31"
156172
pyne run my_strategy.py eurusd_data.ohlcv \
157173
--plot ./analysis/my_plot.csv \
158174
--strat ./analysis/my_stats.csv \
159-
--equity ./analysis/my_equity.csv
175+
--trade ./analysis/my_trades.csv
160176
```
161177

162178
## Troubleshooting

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ egg_info.egg_base = "build"
1616

1717
[project]
1818
name = "pynesys-pynecore"
19-
version = "6.0.18" # PineVersion.Major.Minor
19+
version = "6.1.0" # PineVersion.Major.Minor
2020
description = "Python based Pine Script like runtime and API"
2121
authors = [{ name = "PYNESYS LLC", email = "hello@pynesys.com" }]
2222
readme = "README.md"

src/pynecore/cli/commands/run.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ def run(
7171
help="Path to save the strategy statistics",
7272
rich_help_panel="Out Path Options"
7373
),
74-
equity_path: Path | None = Option(None, "--equity", "-ep",
75-
help="Path to save the equity curve",
76-
rich_help_panel="Out Path Options"),
74+
trade_path: Path | None = Option(None, "--trade", "-tp",
75+
help="Path to save the trade data",
76+
rich_help_panel="Out Path Options"),
7777
):
7878
"""
7979
Run a script
@@ -83,7 +83,7 @@ def run(
8383
8484
If [bold]script[/] path is a name without full path, it will be searched in the [italic]"workdir/scripts"[/] directory.
8585
Similarly, if [bold]data[/] path is a name without full path, it will be searched in the [italic]"workdir/data"[/] directory.
86-
The [bold]plot_path[/], [bold]strat_path[/], and [bold]equity_path[/] work the same way - if they are names without full paths,
86+
The [bold]plot_path[/], [bold]strat_path[/], and [bold]trade_path[/] work the same way - if they are names without full paths,
8787
they will be saved in the [italic]"workdir/output"[/] directory.
8888
""" # noqa
8989
# Ensure .py extension
@@ -132,11 +132,11 @@ def run(
132132
if not strat_path:
133133
strat_path = app_state.output_dir / f"{script.stem}_strat.csv"
134134

135-
# Ensure .csv extension for equity path
136-
if equity_path and equity_path.suffix != ".csv":
137-
equity_path = equity_path.with_suffix(".csv")
138-
if not equity_path:
139-
equity_path = app_state.output_dir / f"{script.stem}_equity.csv"
135+
# Ensure .csv extension for trade path
136+
if trade_path and trade_path.suffix != ".csv":
137+
trade_path = trade_path.with_suffix(".csv")
138+
if not trade_path:
139+
trade_path = app_state.output_dir / f"{script.stem}_trade.csv"
140140

141141
# Get symbol info for the data
142142
try:
@@ -177,7 +177,7 @@ def run(
177177
try:
178178
# Create script runner (this is where the import happens)
179179
runner = ScriptRunner(script, ohlcv_iter, syminfo, last_bar_index=size - 1,
180-
plot_path=plot_path, strat_path=strat_path, equity_path=equity_path)
180+
plot_path=plot_path, strat_path=strat_path, trade_path=trade_path)
181181
finally:
182182
# Remove lib directory from Python path
183183
if lib_path_added:

src/pynecore/core/script_runner.py

Lines changed: 85 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pynecore.types.ohlcv import OHLCV
88
from pynecore.core.syminfo import SymInfo
99
from pynecore.core.csv_file import CSVWriter
10+
from pynecore.core.strategy_stats import calculate_strategy_statistics, write_strategy_statistics_csv
1011

1112
from pynecore.types import script_type
1213

@@ -145,11 +146,12 @@ class ScriptRunner:
145146
"""
146147

147148
__slots__ = ('script_module', 'script', 'ohlcv_iter', 'syminfo', 'update_syminfo_every_run',
148-
'bar_index', 'tz', 'plot_writer', 'strat_writer', 'equity_writer', 'last_bar_index')
149+
'bar_index', 'tz', 'plot_writer', 'strat_writer', 'trades_writer', 'last_bar_index',
150+
'equity_curve', 'first_price', 'last_price')
149151

150152
def __init__(self, script_path: Path, ohlcv_iter: Iterable[OHLCV], syminfo: SymInfo, *,
151153
plot_path: Path | None = None, strat_path: Path | None = None,
152-
equity_path: Path | None = None,
154+
trade_path: Path | None = None,
153155
update_syminfo_every_run: bool = False, last_bar_index=0):
154156
"""
155157
Initialize the script runner
@@ -159,7 +161,7 @@ def __init__(self, script_path: Path, ohlcv_iter: Iterable[OHLCV], syminfo: SymI
159161
:param syminfo: Symbol information
160162
:param plot_path: Path to save the plot data
161163
:param strat_path: Path to save the strategy results
162-
:param equity_path: Path to save the equity data of the strategy
164+
:param trade_path: Path to save the trade data of the strategy
163165
:param update_syminfo_every_run: If it is needed to update the syminfo lib in every run,
164166
needed for parallel script executions
165167
:param last_bar_index: Last bar index, the index of the last bar of the historical data
@@ -186,16 +188,26 @@ def __init__(self, script_path: Path, ohlcv_iter: Iterable[OHLCV], syminfo: SymI
186188

187189
self.tz = _parse_timezone(syminfo.timezone)
188190

191+
# Initialize tracking variables for statistics
192+
self.equity_curve: list[float] = []
193+
self.first_price: float | None = None
194+
self.last_price: float | None = None
195+
189196
self.plot_writer = CSVWriter(
190197
plot_path, float_fmt=f".8g"
191198
) if plot_path else None
192-
self.strat_writer = CSVWriter(strat_path) if strat_path else None
193-
self.equity_writer = CSVWriter(equity_path, headers=(
199+
self.strat_writer = CSVWriter(strat_path, headers=(
200+
"Metric",
201+
f"All {syminfo.currency}", "All %",
202+
f"Long {syminfo.currency}", "Long %",
203+
f"Short {syminfo.currency}", "Short %",
204+
)) if strat_path else None
205+
self.trades_writer = CSVWriter(trade_path, headers=(
194206
"Trade #", "Bar Index", "Type", "Signal", "Date/Time", f"Price {syminfo.currency}",
195207
"Contracts", f"Profit {syminfo.currency}", "Profit %", f"Cumulative profit {syminfo.currency}",
196208
"Cumulative profit %", f"Run-up {syminfo.currency}", "Run-up %", f"Drawdown {syminfo.currency}",
197209
"Drawdown %",
198-
)) if equity_path else None
210+
)) if trade_path else None
199211

200212
# noinspection PyProtectedMember
201213
def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
@@ -233,9 +245,9 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
233245

234246
# If the script is a strategy, we open strategy output files too
235247
if is_strat:
236-
# Open equity writer if we have one
237-
if self.equity_writer:
238-
self.equity_writer.open()
248+
# Open trade writer if we have one
249+
if self.trades_writer:
250+
self.trades_writer.open()
239251

240252
# Clear plot data
241253
lib._plot_data.clear()
@@ -259,6 +271,13 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
259271
# Update lib properties
260272
_set_lib_properties(candle, self.bar_index, self.tz, lib)
261273

274+
# Store first price for buy & hold calculation
275+
if self.first_price is None:
276+
self.first_price = lib.close # type: ignore
277+
278+
# Update last price
279+
self.last_price = lib.close # type: ignore
280+
262281
# Reset function increments
263282
function_isolation.reset_step()
264283

@@ -295,11 +314,11 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
295314
elif position:
296315
yield candle, lib._plot_data, position.new_closed_trades
297316

298-
# Save equity data if we have a writer
299-
if is_strat and self.equity_writer and position:
317+
# Save trade data if we have a writer
318+
if is_strat and self.trades_writer and position:
300319
for trade in position.new_closed_trades:
301320
trade_num += 1 # Start from 1
302-
self.equity_writer.write(
321+
self.trades_writer.write(
303322
trade_num,
304323
trade.entry_bar_index,
305324
"Entry long" if trade.size > 0 else "Entry short",
@@ -316,7 +335,7 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
316335
trade.max_drawdown,
317336
f"{trade.max_drawdown_percent:.2f}",
318337
)
319-
self.equity_writer.write(
338+
self.trades_writer.write(
320339
trade_num,
321340
trade.exit_bar_index,
322341
"Exit long" if trade.size > 0 else "Exit short",
@@ -337,6 +356,11 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
337356
# Clear plot data
338357
lib._plot_data.clear()
339358

359+
# Track equity curve for strategies
360+
if is_strat and position:
361+
current_equity = float(position.equity) if position.equity else self.script.initial_capital
362+
self.equity_curve.append(current_equity)
363+
340364
# Call the progress callback
341365
if on_progress and lib._datetime is not None:
342366
on_progress(lib._datetime.replace(tzinfo=None))
@@ -352,35 +376,58 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
352376
except GeneratorExit:
353377
pass
354378
finally: # Python reference counter will close this even if the iterator is not exhausted
355-
# Export remaining open trades before closing
356-
if is_strat and self.equity_writer and position and position.open_trades:
357-
for trade in position.open_trades:
358-
trade_num += 1 # Continue numbering from closed trades
359-
# Export only the entry part for open trades
360-
self.equity_writer.write(
361-
trade_num,
362-
trade.entry_bar_index,
363-
"Entry long" if trade.size > 0 else "Entry short",
364-
trade.entry_id,
365-
string.format_time(trade.entry_time), # type: ignore
366-
trade.entry_price,
367-
abs(trade.size),
368-
0.0, # No profit yet for open trades
369-
"0.00", # No profit percent yet
370-
0.0, # No cumulative profit change
371-
"0.00", # No cumulative profit percent change
372-
0.0, # No max runup yet
373-
"0.00", # No max runup percent yet
374-
0.0, # No max drawdown yet
375-
"0.00", # No max drawdown percent yet
376-
)
379+
if is_strat and position:
380+
# Export remaining open trades before closing
381+
if self.trades_writer and position.open_trades:
382+
for trade in position.open_trades:
383+
trade_num += 1 # Continue numbering from closed trades
384+
# Export only the entry part for open trades
385+
self.trades_writer.write(
386+
trade_num,
387+
trade.entry_bar_index,
388+
"Entry long" if trade.size > 0 else "Entry short",
389+
trade.entry_id,
390+
string.format_time(trade.entry_time), # type: ignore
391+
trade.entry_price,
392+
abs(trade.size),
393+
0.0, # No profit yet for open trades
394+
"0.00", # No profit percent yet
395+
0.0, # No cumulative profit change
396+
"0.00", # No cumulative profit percent change
397+
0.0, # No max runup yet
398+
"0.00", # No max runup percent yet
399+
0.0, # No max drawdown yet
400+
"0.00", # No max drawdown percent yet
401+
)
402+
403+
# Write strategy statistics
404+
if self.strat_writer and position:
405+
try:
406+
# Open strat writer and write statistics
407+
self.strat_writer.open()
408+
409+
# Calculate comprehensive statistics
410+
stats = calculate_strategy_statistics(
411+
position,
412+
self.script.initial_capital,
413+
self.equity_curve if self.equity_curve else None,
414+
self.first_price,
415+
self.last_price
416+
)
417+
418+
write_strategy_statistics_csv(stats, self.strat_writer)
419+
self.strat_writer.close()
420+
421+
finally:
422+
# Close strat writer
423+
self.strat_writer.close()
377424

378425
# Close the plot writer
379426
if self.plot_writer:
380427
self.plot_writer.close()
381-
# Close the equity writer
382-
if self.equity_writer:
383-
self.equity_writer.close()
428+
# Close the trade writer
429+
if self.trades_writer:
430+
self.trades_writer.close()
384431

385432
# Reset library variables
386433
_reset_lib_vars(lib)

0 commit comments

Comments
 (0)