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.

A trader and analyst workstation: a code editor with Python journal-analysis scripts and a chart with Pine Script indicators on the monitor
Key points in 60 seconds
  • 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 the time function). 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 pandas and matplotlib (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.

Analysis pipeline: a CSV journal export goes into the Python scripts, metrics and an equity-curve chart come out, and the conclusions feed into the weekly review and playbook CSV journalformat from add-on F Python: S1–S3metrics · curve · drawdown EOW reviewsample, dependencies, uncertainty Playbookversion rules and tests there is no “code → order” arrow in this workflow — and that is no oversight

S1 · Loading a CSV journal

Python 3 · pandas

The 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 · pandas

The 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 + matplotlib

Plots 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.

Test file and expected output (S1 + S2 + S3) CSV · PL format

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 library

Volume 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 · indicator

A 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 · indicator

Section 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.

Pine Script v6 indicators on a TradingView EUR/USD chart: highlighted plan session windows (S6) and the bottom daily range vs ATR panel with an 80% alert level (S5)
Pine Script v6 indicators on a TradingView EUR/USD chart: highlighted plan session windows (S6) and the bottom daily range vs ATR panel with an 80% alert level (S5)
The entire kit in one file generated locally in your browser

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.

Verified S3 test calculation: five-trade cumulative net R from zero and drawdown, with Python calculation excerpt and journal metrics.
Verified S3 test calculation using the supplied CSV: cumulative net R is 0, −1.00, −2.08, −0.25, −0.25 and +2.15. Maximum drawdown is 2.08 R. This explanatory reconstruction is not an application screenshot or account performance record.

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.

Illustrative scenario: rounding “to a neater number”. The sizer calculated 0.07 lots; the trader clicked 0.1 — “a cosmetic difference, and a round number”. That cosmetic adjustment in numbers: +43% risk on every trade, systematically, throughout the quarter; the daily −2R limit was effectively −2.9R, and the equity curve's drawdown exceeded the plan, even though the trader regarded each adjustment as cosmetic. Hence math.floor in S4 instead of round: the machine rounds down without argument; the human rounds up with a justification.
Illustrative scenario: a cluster of losses visible only on the curve. The quarter's metrics looked reasonable: positive expectancy, drawdown within the expected range. Only the S3 chart revealed that the entire drawdown came from one week — and an entry-time filter added to S2 made it more precise: every loss between 21:00 and 23:00 UTC (the journal in add-on F records times in UTC), on wide spreads, “because I will stay a little longer”. Whether those hours coincide with rollover depends on the broker and time zone — check its specification rather than assuming. An aggregate number averages things out; the curve shows WHEN the system is bleeding. One line in the plan (no new entries after 21:00 UTC, or 23:00 CEST / 22:00 CET — record this in Europe/Warsaw time in the plan and the S6 windows) is, in this scenario, a conclusion to test over subsequent weeks, not a ready-made improvement in results — and it is exactly the kind of conclusion this kit is designed to support.
Disclaimer: this page is educational and does not constitute investment advice. The code snippets are analytical tools that you run at your own responsibility: read the code before using it, verify the results (for example, with the calculator in add-on C), and make trading decisions according to your own plan. The Python snippets (S1–S4) do not connect to the internet or your account — they read local files and plot charts; the Pine indicators (S5–S6) run on your charting platform using its data. None of the snippets generates buy/sell signals or places orders.

Sources

  1. 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.
  2. 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.
  3. 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

Hypothetical journal; each R uses its own recorded monetary reference
TradeReference risk USDNet outcome USDNet R
A100+100+1
B200−200−1
C100+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.

  1. A. +300 USD
  2. B. +100 USD
  3. C. 0 USD
  4. D. −200 USD
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.

  1. A. Approximately +0.3333R.
  2. B. 0R because cash sums to zero.
  3. C. +1R per trade.
  4. D. −1R per trade.
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.

  1. A. Yes; all positive R means imply a cash gain.
  2. B. Yes; the losing trade can be removed.
  3. C. Yes; use the smallest reference for every trade.
  4. D. No; the cash total must retain each trade’s actual monetary scale.
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.

Return to the tool

Jarosław Wasiński

Editor-in-chief, ForexMechanics.com • Macro & FX analyst

Independent analyst and practitioner with over 20 years of experience in financial markets. Actively involved in Forex since 2007, with a focus on fundamental analysis, OTC market structure, and disciplined capital risk management.

  • Creator and editor-in-chief of MyBank.pl, providing reliable financial education since 2004.
  • Author of hundreds of in-depth market commentaries, structural analyses, and educational materials for traders and investors.
  • Advocate for market transparency, promoting education grounded in hard data and institutional reports (BIS, ESMA, CFTC).

All content is educational and informational only – it does not constitute investment advice or a recommendation. Remember: leveraged trading (Forex/CFD) carries a high risk of rapid capital loss.