Trader code kit — Python and Pine Script
Six code snippets to copy: four in Python (loading a CSV journal, metrics, an equity curve with drawdown, and an ATR position sizer) and two in Pine Script (the daily range relative to ATR with an alert, and trading-plan session windows on the chart). These are all ANALYTICAL tools — they calculate, plot and remind you; none of the snippets generates buy/sell signals or makes decisions, because that is the playbook's job. The code and comments are in English — this is the convention used in this kit, consistent with API names, the pandas and Pine Script documentation, and error messages (the same approach that the AI workshop takes for prompts involving code); the surrounding explanations use the language of the course and guide you step by step. Environment: Python installation and first runs are covered in 11.8; the journal column headers are in add-on F.
- Tools, not strategies: the code calculates metrics from YOUR data and keeps track of YOUR plan — the boundary is the same as in the “what AI does not do” section of add-on H, and for the same reasons
- A shared data format: the Python snippets read the headers from the journal spreadsheet (add-on F) — win rate includes break-even trades in the denominator, and expectancy uses the net result; these are the same definitions as in prompt P5
- Pine Script without lookahead: S5 retrieves daily data with
lookahead=barmerge.lookahead_off; S6 does not access a higher timeframe at all (only thetimefunction). The absence of lookahead does not prove the absence of repainting — the S5 description explains what you see on historical bars and in real time. These are the pitfalls that a code review with prompt P3 looks for - The sizer rounds volume DOWN: when converting risk into lots, rounding up systematically increases risk beyond the plan — that one line of code is the difference between 1% and “about 1%”
Before you run the code: three rules
- Read before you run. Every snippet is deliberately short — you should understand every line. Modify code you do not understand with the help of a P3 review, not by guesswork; this rule applies doubly to snippets from forums and chats.
- Data comes in from files; decisions do not come out of the code. The scripts read a journal export (the format from add-on F) and parameters you supply explicitly. The output consists of numbers and charts for the weekly review — you draw the conclusions, following the audit process in 15.10 and taking account of sample size, dependencies between trades, and uncertainty in the result.
- The environment from 11.8: Python 3.11 or later with
pandasandmatplotlib(installation and the first script are covered there); snippets S1–S4 were rerun on 17 September 2026 using Python 3.12.10, pandas 3.0.5 and matplotlib 3.11.1. Other versions were not tested. Paste Pine Script into the indicator editor of your charting platform; both indicators use version 6.
Minimum steps to run the code: in your Python 3.11+ environment, check the version with python --version, then install the packages with python -m pip install pandas==3.0.5 matplotlib==3.11.1, save S1+S2+S3 as analiza_dziennika.py and the test CSV as dziennik.csv, then run python analiza_dziennika.py. S4 requires only the standard library. Paste S5 and S6 into Pine Editor separately. The two Pine snippets were checked against the v6 documentation; they were not compiled, and their alerts were not tested in TradingView.
S1 · Loading a CSV journal
Python 3 · pandasThe entry point for S2 and S3: reads the spreadsheet export from add-on F, checks the headers and number of fields, and then cleans the r_net column. Duplicate column names, an empty header, and too many or too few fields stop the import; the script does not choose between two result columns or move data into the index. Represent an empty field with a separator, for example ;A+;;0; a truncated row is a different case. Set the separator and decimal character for the CSV variant (PL: semicolon and comma; EN: comma and point), along with the encoding: files from the add-on F generator use UTF-8, and the “CSV UTF-8” variant saved in Excel can also be read as UTF-8; an older export using the Polish code page may require cp1250. Check the save format, because the interface language alone does not determine the encoding. An empty r_net and an empty close_time_utc mean the trade has not yet closed (and is skipped); a close time without a result means an incomplete record and stops the calculations. A non-empty value that cannot be read as a number (including “NA” or a dash) stops the script and reports the record number — as does a closed trade without a close time in the add-on F format (2026-07-08 13:15), because otherwise the curve's sequence would have to be guessed.
import pandas as pd
CSV_PATH = "dziennik.csv" # journal export (add-on F structure)
CSV_SEP = ";" # ";" for PL-locale CSV, "," for EN
DECIMAL = "," # "," for PL-locale CSV, "." for EN
ENCODING = "utf-8-sig" # add-on F generator writes UTF-8 (BOM handled); legacy Polish-code-page CSV may need "cp1250"
REQUIRED = ["r_net", "setup", "setup_class", "error_code", "close_time_utc"]
# Read a local file and validate its shape BEFORE building a DataFrame.
# pandas.read_csv can rename duplicate headers or infer an index from extra fields.
import csv
try:
with open(CSV_PATH, encoding=ENCODING, newline="") as source:
reader = csv.reader(source, delimiter=CSV_SEP, strict=True)
header = next(reader, None)
if not header:
raise SystemExit("Cannot read CSV: the header is empty.")
if any(not name or name != name.strip() for name in header):
raise SystemExit("Empty or space-padded column name — fix the CSV header.")
duplicates = sorted({name for name in header if header.count(name) > 1})
if duplicates:
raise SystemExit(f"Duplicate columns: {duplicates} — resolve them in the journal.")
rows, record_indexes = [], []
for index, row in enumerate(reader):
if not row: # a completely empty CSV record
continue
if len(row) != len(header):
raise SystemExit(f"Record {index + 1}: expected {len(header)} fields, got {len(row)} "
f"— check separators and quotes; no fields were discarded.")
rows.append(row)
record_indexes.append(index)
df = pd.DataFrame(rows, columns=header, index=record_indexes, dtype="string").replace("", pd.NA)
except UnicodeDecodeError:
raise SystemExit(f"Cannot decode {CSV_PATH} as {ENCODING} — check UTF-8 or legacy cp1250.") from None
except (OSError, csv.Error) as exc:
raise SystemExit(f"Cannot read {CSV_PATH}: {exc}") from None
missing = [c for c in REQUIRED if c not in df.columns]
if missing:
raise SystemExit(f"Missing columns: {missing} — check headers against add-on F and the CSV_SEP setting.")
raw = df["r_net"] # an empty result is allowed only when the close time is also empty
txt = raw.astype("string").str.strip().str.replace(DECIMAL, ".", regex=False) # parse the column ourselves: one bad cell must not hide the readable ones
r = pd.to_numeric(txt, errors="coerce").astype("Float64") # avoid integer wraparound in later sums
r = r.where(r.abs() != float("inf")) # inf is not a valid R-multiple either
invalid = raw.notna() & r.isna() # filled in, but not readable as a number
if invalid.any():
raise SystemExit(f"Invalid r_net in records {(df.index[invalid] + 1).tolist()} "
f"(record 1 = first row under the header) — check values and DECIMAL; "
f"NA, dashes and infinity are not numbers.")
blank = df.isna().all(axis=1) # whole row empty = unused sheet row (whole-sheet XLSX export), not a trade
df, r = df[~blank], r[~blank]
has_close = df["close_time_utc"].fillna("").str.strip().ne("")
incomplete = r.isna() & has_close
if incomplete.any():
raise SystemExit(f"Missing r_net for closed records {(df.index[incomplete] + 1).tolist()} "
f"— complete the net result before calculating metrics.")
open_trades = int(r.isna().sum())
df = df.assign(r_net=r).dropna(subset=["r_net"]) # index kept = record numbers for the message below
# equity curve and drawdown need chronological order (add-on F: by close time) — a closed trade without a readable close time stops the script
close_text = df["close_time_utc"].fillna("").str.strip()
has_time = close_text.str.fullmatch(r"\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})?")
closed = pd.to_datetime(close_text.where(has_time), errors="coerce", format="ISO8601", utc=True) # add-on F writes 2026-07-08 13:15; a fixed format keeps the check independent of row order, utc=True keeps a stray "Z"/"+00:00" suffix from crashing it
if closed.isna().any():
raise SystemExit(f"close_time_utc not readable in records {(df.index[closed.isna()] + 1).tolist()} "
f"— every closed trade needs a close time in the add-on F format (2026-07-08 13:15); fix the journal, not the script.")
df = df.assign(_t=closed).sort_values("_t", kind="stable").drop(columns="_t").reset_index(drop=True)
print(f"Loaded {len(df)} closed trades ({open_trades} row(s) skipped: no close time and no r_net).")
You get: a DataFrame, df, ready for the next snippets, sorted by close time (the curve and drawdown are calculated in that order), plus the number of open trades skipped (a blank row means an entirely empty record, as in an export of a whole XLSX sheet — the script does not count these rows; a row containing data but no result or close time is skipped as not yet closed; a closed entry without a result stops the script). If it stops with a list of records, fix the indicated rows in the journal, not the script; if the whole column is affected, also check the DECIMAL constant. Record numbering starts after the header and includes empty records; a quoted field may contain a separator or a newline and still belongs to the same record. Each trade should appear once: check the trade_id identifiers before combining exports — S1 does not deduplicate the history. A date without a time is insufficient to reconstruct the sequence. A time without a time-zone marker is treated as UTC; an explicit ISO 8601 offset is converted to UTC. For identical times, the script preserves record order — establish that order in the journal, because it affects the drawdown path.
S2 · Journal metrics
Python 3 · pandasThe same definitions as in prompt P5 and the spreadsheet dashboard: win rate includes break-even trades in the denominator; profit factor returns a message instead of dividing by zero; expectancy is the mean of net R (r_net); and drawdown is measured from the cumulative curve starting at 0 R (the first loss is already a drawdown — as in the formula in spreadsheet F). Run after S1. The metric definitions are shared, but the error policy differs from P5: S1 stops the analysis when a closed trade has an invalid result, whereas P5 instructs you to skip that row and count it. Compare results only after establishing which trades were included in both samples.
import math
def metrics(r):
"""Core journal metrics on a series of net R-multiples.
Definitions (same as add-on F dashboard):
- win rate: trades with r > 0 over ALL valid trades (break-even stays in denominator)
- profit factor: sum of positive r over abs(sum of negative r)
- expectancy: arithmetic mean of r (net of costs)
- max drawdown: max drop of the cumulative R curve from its running peak,
the curve starting at 0 R (so a first loss of -1 R is already a 1 R drawdown)"""
n = len(r)
if n == 0:
raise SystemExit("No valid trades — nothing to measure.")
try:
r = r.astype(float)
except (TypeError, ValueError):
raise SystemExit("Metrics require numeric net R values — run S1 first.") from None
if not r.map(math.isfinite).all():
raise SystemExit("Metrics require finite net R values — run S1 first.")
wins = r[r > 0]
losses = r[r < 0]
equity = r.cumsum()
peak = equity.cummax().clip(lower=0) # add-on F: MAX(0; running peak)
drawdown = peak - equity
try:
total = math.fsum(r)
pf_value = None if losses.empty else math.fsum(wins) / abs(math.fsum(losses))
except (OverflowError, ValueError, ZeroDivisionError):
raise SystemExit("Numeric range exceeded — check the R values; no metrics returned.") from None
if (not equity.map(math.isfinite).all() or not drawdown.map(math.isfinite).all()
or (pf_value is not None and not math.isfinite(pf_value))):
raise SystemExit("Numeric range exceeded — check the R values; no metrics returned.")
pf = "no losses in sample" if pf_value is None else round(pf_value, 2)
return {
"trades": n,
"win_rate": round(len(wins) / n, 3),
"profit_factor": pf,
"expectancy_R": round(total / n, 3),
"sum_R": round(total, 2),
"max_drawdown_R": round(drawdown.max(), 2),
}
for key, value in metrics(df["r_net"]).items():
print(f"{key:>16}: {value}")
You get: six metrics for the weekly review; if there are no losses, PF is a message rather than a number. If values exceed the calculation range, the script stops the analysis instead of returning inf, NaN or an apparently zero drawdown. WR = 0.4 means 40%. PF is calculated in R, not PLN; with varying monetary risk, these results may differ. Costs are already included in r_net — S2 does not subtract them again. Do not expect an assessment of your edge from the mean alone, or simply from passing 20 or 50 trades. You will find the dashboard definitions in add-on F, and the plan for sample selection, costs and validation in add-on N.
S3 · Equity curve and drawdown
Python 3 · pandas + matplotlibPlots the cumulative R curve with drawdown from the peak below it, and saves a PNG next to the CSV file specified by CSV_PATH (S1), regardless of the directory from which you run the script. The curve reveals the sequence of results and clusters of losses that disappear in aggregate numbers. It is a sum of R, not the account balance in PLN or percentage equity: with varying monetary risk, it will not reproduce balance changes. This chart does not include deposits, withdrawals or the results of open positions. Run after S1.
import math
import matplotlib.pyplot as plt
from pathlib import Path
if len(df) == 0:
raise SystemExit("No closed trades — nothing to plot.")
equity = df["r_net"].astype(float).cumsum()
drawdown = equity.cummax().clip(lower=0) - equity # same definition as S2 and add-on F (curve starts at 0 R)
if not equity.map(math.isfinite).all() or not drawdown.map(math.isfinite).all():
raise SystemExit("Numeric range exceeded — check the R values; no chart saved.")
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 6), sharex=True,
gridspec_kw={"height_ratios": [3, 1]})
x = [0, *(equity.index + 1)] # trade 0 = the 0 R starting point the drawdown is measured from
ax1.plot(x, [0.0, *equity.values])
ax1.set_ylabel("equity (R)")
ax1.set_title("Equity curve — cumulative net R")
ax1.grid(True, alpha=0.3)
ax2.fill_between(x, [0.0, *(-drawdown.values)], 0, alpha=0.5)
ax2.set_ylabel("drawdown (R)")
ax2.set_xlabel("trade #")
ax2.grid(True, alpha=0.3)
fig.tight_layout()
PNG_PATH = Path(CSV_PATH).with_name("equity_curve.png") # next to the journal file, whatever the working directory
fig.savefig(PNG_PATH, dpi=150)
plt.close(fig)
print(f"Saved: {PNG_PATH}")
You get: an equity_curve.png file next to the journal; the curve starts at 0 R (trade 0), from which drawdown is measured. The lower panel shows the drawdown of cumulative R from its peak (starting at 0 R, as in S2 and spreadsheet F) over the entire history — it is not the weekly loss. Check the weekly limit from the plan (add-on E) separately, week by week, using the unit and reset rule specified in the plan.
Save as dziennik.csv (UTF-8) next to the combined S1+S2+S3 script and run it. The file contains only the columns S1 needs — the full export from add-on F (26 columns) is loaded in the same way. The last row is an open trade (empty r_net); the first two closed trades are losses, so drawdown is measured from 0 R.
trade_id;close_time_utc;setup;setup_class;r_net;error_code 2026-041;2026-07-08 13:15;wybicie-azja-londyn;A+;-1,00;5 2026-042;2026-07-09 16:40;powrot-do-sredniej;B;-1,08;3 2026-043;2026-07-10 09:05;wybicie-azja-londyn;A+;1,83;0 2026-044;2026-07-13 12:20;wybicie-azja-londyn;B;0,00;0 2026-045;2026-07-14 14:30;powrot-do-sredniej;C;2,40;0 2026-046;;wybicie-azja-londyn;B;;
Expected output (Python 3.12.10, pandas 3.0.5, matplotlib 3.11.1; run on 17 September 2026):
Loaded 5 closed trades (1 row(s) skipped: no close time and no r_net).
trades: 5
win_rate: 0.4
profit_factor: 2.03
expectancy_R: 0.43
sum_R: 2.15
max_drawdown_R: 2.08
Saved: equity_curve.png
Calculation to check by hand: wins of 1.83 + 2.40 = 4.23 R, losses of 1.00 + 1.08 = 2.08 R → PF 2.03; total 2.15 R / 5 = 0.43 R; the curve −1.00 → −2.08 → −0.25 → −0.25 → 2.15, with a starting peak of 0 R → drawdown of 2.08 R (without the 0 R point, it would be 1.08 R — this is the distinction behind the definition in add-on F). The output refers to this exact file and the stated versions; if it differs, check the data (separator, decimal character, encoding, row order), the constants in S1, the code, and the package versions, in that order.
S4 · ATR position sizer
Python 3 · standard libraryVolume in lots for a stop distance of k × ATR: the risk amount comes from capital and percentage, the distance from ATR, and the volume is rounded DOWN to your account's lot step (the lot_step and min_lot parameters, both defaulting to 0.01) — so that PLANNED risk does not exceed the budget. The actual loss can be greater: this calculation excludes slippage, gaps through the stop, spread, commission and swap. Capital and pip value must be in the same account currency; separately check the maximum volume and margin requirement in the specification. Enter ATR in pips (EURUSD 0.0080 → 80; USDJPY 0.80 → 80; pip size is covered in the pair profile, add-on B); the pip value for 1 lot is in the instrument specification (or you can calculate it using the calculator in add-on C).
import math
from decimal import Decimal
from fractions import Fraction
def atr_position_size(capital, risk_pct, atr_pips, atr_mult, pip_value_per_lot,
lot_step=0.01, min_lot=0.01):
"""Volume in lots for a stop of atr_mult × ATR.
atr_pips: ATR in PIPS, not in price units (EURUSD ATR 0.0080 -> 80; USDJPY ATR 0.80 -> 80 — pip size of the pair).
pip_value_per_lot: value of 1 pip for 1.00 lot, in account currency
(e.g. ~10 USD for EURUSD on a USD account — check instrument specs).
lot_step / min_lot: volume step and minimum of YOUR account (0.01 is common, not universal).
Rounds DOWN to multiples of lot_step, measured from zero. Fraction arithmetic
preserves the decimal inputs without depending on the global Decimal precision.
Planned risk is not the maximum loss: slippage, gaps, spread and swap are not in this model."""
inputs = (capital, risk_pct, atr_pips, atr_mult, pip_value_per_lot, lot_step, min_lot)
if any(isinstance(x, bool) or not isinstance(x, (int, float, Decimal)) for x in inputs):
raise ValueError("Use numeric values, not text or booleans (Python decimals use a dot).")
decimals = tuple(Decimal(str(x)) for x in inputs)
if not all(x.is_finite() and x > 0 for x in decimals):
raise ValueError("All inputs must be finite, positive numbers.")
c, p, a, k, v, step, minimum = map(Fraction, decimals)
if p > 100:
raise ValueError("risk_pct is a percentage of capital (1.0 = 1%).")
risk_amount = c * p / 100
sl_pips = a * k
lots = math.floor(risk_amount / (sl_pips * v * step)) * step
if lots < minimum:
return {"warning": "No admissible volume for this lot_step and min_lot within the risk budget."}
exact = {"risk_amount": risk_amount, "sl_pips": sl_pips,
"lots": lots, "planned_risk": lots * sl_pips * v}
try:
result = {key: float(value) for key, value in exact.items()}
except OverflowError:
raise ValueError("Inputs exceed the output numeric range.") from None
if not all(math.isfinite(value) and value > 0 for value in result.values()):
raise ValueError("Inputs exceed the output numeric range.")
return result # floats for display; no extra rounding of the stop or money values
print(atr_position_size(capital=10_000, risk_pct=1.0,
atr_pips=80, atr_mult=1.5, pip_value_per_lot=10.0))
# {'risk_amount': 100.0, 'sl_pips': 120.0, 'lots': 0.08, 'planned_risk': 96.0}
You get: lots rounded down to the step and the resulting PLANNED risk amount (in the model without costs, ≤ the budget; the actual loss may be greater). The step is measured from zero; check that your account uses this volume grid. The stop and monetary amounts are not rounded further: for example, ATR of 0.26 pips × 1.25 gives 0.325 pips, not 0.3. The output is returned as float values for reading, while the number of lot steps is calculated using exact fractions; the instrument's minimum price increment must still be checked separately. If you adjust the stop to that increment, recalculate the volume. Do not expect: a decision on whether to enter — the sizer calculates “how much”, never “whether”.
S5 · Daily range vs ATR with an alert
Pine Script v6 · indicatorA volatility-awareness tool (an intraday indicator): on live bars, it shows what percentage of the daily ATR the market has already “used up”, and alerts you when a threshold is crossed — a moment to consult the plan, not to chase. Daily data is retrieved without lookahead (without future values); on historical bars, request.security holds the last confirmed daily value until the end of the next D period, with that period's new result appearing on its final intraday bar. In real time, it retrieves the developing range and ATR of the current D candle, so earlier readings may change after a reload (repainting). A day means the data provider's D candle, not a calendar day in Europe/Warsaw. The ATR in the denominator also changes during the day; 100% is not a limit on how far price can move. This method cannot reconstruct today's successive threshold crossings on historical bars.
//@version=6
indicator("Daily range vs ATR (awareness tool)", overlay=false)
if not timeframe.isintraday
runtime.error("Use a time-based intraday chart below 1D.")
atrLen = input.int(14, "ATR length", minval=2)
warnPct = input.int(80, "Alert when day range reaches % of ATR", minval=10, maxval=200)
// Historical: last confirmed D value, updated at the END of each D period.
// Realtime: developing D values; elapsed realtime readings can repaint after reload.
dayHigh = request.security(syminfo.tickerid, "D", high, lookahead=barmerge.lookahead_off)
dayLow = request.security(syminfo.tickerid, "D", low, lookahead=barmerge.lookahead_off)
dayAtr = request.security(syminfo.tickerid, "D", ta.atr(atrLen), lookahead=barmerge.lookahead_off)
rangePct = dayAtr > 0 ? 100.0 * (dayHigh - dayLow) / dayAtr : na
plot(rangePct, "Day range as % of ATR", linewidth=2)
hline(100, "100% of ATR")
hline(warnPct, "Alert level", linestyle=hline.style_dotted)
alertcondition(ta.crossover(rangePct, warnPct),
title="Range reached % of ATR",
message="Daily range reached your % of ATR — re-read the plan before any new entry.")
You get: a panel below the chart and an alert condition — create the alert yourself in the platform after adding the indicator, selecting “Range reached % of ATR”. Use standard time-based intraday candles; Heikin Ashi/Renko data may have different characteristics. A missing reading (na) means the required ATR or data is unavailable, not 0%. The condition requires a move from a value ≤ the threshold to a value > the threshold: simply staying above it does not trigger another alert. “Once Per Bar Close” restricts alerts to the intraday candle close, but does not confirm the entire D candle. Recreate the alert after changing the parameters. Do not expect: a directional signal — a high reading says “the move has already happened”; the playbook tells you what to do about it.
S6 · Trading-plan session windows on the chart
Pine Script v6 · indicatorSection 3 of the plan (add-on E), drawn on the chart: highlights the hours when you ARE ALLOWED to open positions, and alerts you when the window closes. Enter the times in the Europe/Warsaw time zone — the IANA identifier accounts for CET/CEST. By default, the windows apply Monday to Friday; an option extends them to all days. The highlighting confirms only the time specified in the plan, not the other entry conditions. Use standard time-based intraday candles.
//@version=6
indicator("Trading-plan session windows", overlay=true)
if not timeframe.isintraday
runtime.error("Use a time-based intraday chart below 1D.")
tz = input.string("Europe/Warsaw", "Time zone of the windows")
win1 = input.session("0900-1200", "Window 1")
win2 = input.session("1400-1700", "Window 2")
weekdaysOnly = input.bool(true, "Monday to Friday only")
days = weekdaysOnly ? ":23456" : ":1234567"
inWin = not na(time(timeframe.period, win1 + days, tz)) or not na(time(timeframe.period, win2 + days, tz))
bgcolor(inWin ? color.new(color.teal, 88) : na, title="Inside a plan window")
windowJustClosed = inWin[1] and not inWin
alertcondition(windowJustClosed,
title="Plan window closed",
message="Your trading window just closed — manage open positions only, no new entries (plan, section 3).")
You get: background shading during trading hours and an alert condition for the window's close (“Plan window closed” — create the alert in the platform after adding the indicator). The alert detects the first candle outside the window following a candle inside it, so it does not guarantee a notification at the exact closing time: without a new candle and a data update, it performs no check. Use a timeframe whose opens and closes align with the window boundaries. For example, on M15, with a 09:00–12:00 window and a 12:00 candle available, the shading ends at 12:00 and the condition is active on that first candle outside the window. “Once Per Bar” may notify you on its first update; “Once Per Bar Close” only at its close. Overlapping windows form a union: the end of the first does not close the second. Recreate the alert after changing the times. Do not expect: order blocking — the indicator reminds you of the plan; your pre-trade checklist enforces it.
Six snippets with headings in one text file, the setup instructions and library versions, followed by the test CSV and expected output — to archive alongside your journal or keep in your own repository. The TXT combines different languages: do not run the whole file as Python. Choose S1+S2+S3, the standalone S4, or a single Pine indicator.
FAQ
Why are the code and comments in English in the Polish course?
This is the convention used in this kit: English comments are consistent with API names, the pandas and Pine Script documentation, and the error messages you already read in English; the AI workshop takes the same approach for prompts involving code (add-on H, P3). The Polish descriptions around the snippets explain WHAT the code does and when to use it; the English comments inside explain HOW. The exception in the Polish version of prompt P5 (Polish comments in code generated specifically for you) applies to one-off code, not a library.
How do I run the Python snippets?
11.8 takes you step by step through installing Python, the environment and packages — pandas and matplotlib are sufficient for this kit. Combine snippets S1–S3 into one file, analiza_dziennika.py (S1 at the top), and run python analiza_dziennika.py in the directory containing the journal export; S4 runs independently. The relative CSV_PATH refers to the terminal's working directory. Start with the test CSV, and analyse a copy of your real journal; equity_curve.png is overwritten on the next run. If the terminal returns an error you do not understand, paste the code and the full message into a P3 review from add-on H, instead of guessing.
Can I modify and extend these snippets?
That is what they are for — this is a STARTER kit. Natural extensions include metrics per setup (group by the setup column before calling the S2 function), filtering by error_code to calculate expectancy for clean execution, or separate curves for A+/B classes. Two rules for modifications: put every change through a P3 review (especially anything involving request.security in Pine), and version both metric definitions and strategy rules. Fix a calculation error immediately and recalculate the periods being compared using the same method; document a change to strategy rules in the testing protocol (add-on N) and check it on new data. The end of a quarter does not in itself justify a change, and being partway through one does not justify retaining an error.
Why does the kit contain no strategies or EAs?
For the same reason that the AI workshop does not generate signals: a ready-made strategy from the internet has an unknown edge, unknown assumptions and no connection to your plan — and an EA executing someone else's logic on your account means delegating decisions for which you alone pay. The code in this course strengthens the process (measurement, visualisation and keeping to the plan), rather than replacing it. If you want to automate your own tested strategy, the route runs through the backtest in 15.4, the testing protocol in add-on N, and a P3 code review, not through copying “profitable” scripts.
math.floor in S4 instead of round: the machine rounds down without argument; the human rounds up with a justification.Related course material
- 11.8 Python in trading — setting up the environment, your first script and working with packages
- Add-on F — journal spreadsheet — the CSV format read by S1–S3, and the same metrics expressed as spreadsheet formulas
- Add-on H, prompt P3 — reviewing every modification to this code (lookahead, repainting, logic errors)
- Tools section — calculators — browser-based pip value and position sizing, for checking the results of S4
- Add-on E — plan template — the trading windows and limits that S6 draws on the chart
- 15.10 Strategy audit — disciplined interpretation of the numbers this kit produces
Sources
- Documentation: pandas 3.0.5 and matplotlib 3.11.1 (functions used in S1–S3); Python: CSV reading and Fraction arithmetic (format validation and exact calculation of the number of lot steps) — snippets S1–S4 were run on synthetic data on 17 September 2026 using Python 3.12.10; TradingView Pine Script v6: other timeframes and data, sessions, time and time zones, alerts (request.security with the lookahead parameter and repainting, hline, input.session, alertcondition) — the S5–S6 code was reviewed against the documentation, without compilation.
- Internal: 11.8 Python in trading (environment), 15.9 (metric definitions and journal format), 15.10 (principles for drawing conclusions) — this kit is their practical condensed implementation.
- Van K. Tharp, Trade Your Way to Financial Freedom, 2nd ed., McGraw-Hill 2007 — R-multiples and expectancy, the common language of S2, spreadsheet F and prompt P5.
Check what a positive mean R actually measures
A hypothetical journal example with three exercises on monetary results and R-multiples. Compare the hand calculation with your data definitions.
Name the units of the journal columns
The code above calculates from supplied records; it does not establish that those records describe the right quantity. Before interpreting a result, identify the account currency, whether monetary outcomes are net, and the monetary risk reference recorded before each trade.
Our invented USD journal has three completed trades. Their net monetary outcomes include every charge in this exercise. Their predeclared reference risks are positive amounts; they need not be the same dollar size. No deposit, withdrawal or other result enters this small sample.
Reconcile the normalised and monetary results
| Trade | Reference risk USD | Net outcome USD | Net R |
|---|---|---|---|
| A | 100 | +100 | +1 |
| B | 200 | −200 | −1 |
| C | 100 | +100 | +1 |
The monetary sum is 100 − 200 + 100 = 0 USD. The mean normalised result is (1 − 1 + 1) / 3 ≈ +0.3333R, rounded to four decimals. These findings are compatible because the losing trade carried a larger monetary reference.
Two outcomes are positive out of three: approximately 66.67% under the stated win definition. There are no zero trades here. The code’s treatment of break-even observations still matters for other datasets; do not silently change that denominator.
Do not turn the average into a cash forecast
A positive mean R is not the same claim as a positive dollar total when risk amounts vary. Multiplying the sum of R by an arbitrarily selected reference would not reconstruct these cash outcomes. Use each trade’s own reference to reconcile money: 1 × 100 − 1 × 200 + 1 × 100 = 0 USD.
No account opening value is supplied, so we do not calculate an account return or percentage drawdown. Three invented trades provide no estimate of a future edge. The exercise checks units and aggregation, not a sizing policy.
Validate before running or adapting the snippets
Preserve trade identifiers, currency, net-outcome definition, reference-risk convention and missing-value policy with the data file. A zero or unknown risk reference cannot safely be replaced by a convenient positive number just to obtain an R value.
Use a separate test copy when learning, inspect the required columns, and check a small hand-calculated sample before accepting a chart. No additional commission or spread is deducted from these explicitly net outcomes.
Three exercises with explanations
1. What is the sample’s total net monetary result?
USD outcomes: +100, −200, +100.
Show the answer and explanation
C. 100 − 200 + 100 = 0 USD.
- A. This ignores the negative outcome.
- B. That confuses a normalised result with cash.
- C. The two positive amounts offset the larger loss exactly.
- D. This omits both positive outcomes.
2. What is the mean net R?
The three normalised outcomes are +1, −1, +1.
Show the answer and explanation
A. (1 − 1 + 1) / 3 ≈ +0.3333R. Keep the per-trade reference convention.
- A. The R sum is 1 across 3 observations.
- B. Different monetary references allow a zero cash sum and a positive mean R.
- C. That is the sum, not the mean.
- D. Only one of the supplied outcomes is negative.
3. Does the positive mean R establish positive cash profit here?
References are 100, 200, 100 USD; the supplied cash sum is 0 USD.
Show the answer and explanation
D. Report both measures with their definitions. Neither proves future profitability.
- A. That inference ignores the varying monetary references.
- B. Removing a loss changes the sample.
- C. Replacing recorded references changes the accounting.
- D. The mean normalises each trade, whereas cash aggregation retains different scales.