Skip to content
SlashClone
Made by@qtrader

Trade journal

@qtrader/trade-journal

Keep a trading journal that actually teaches you something. Log each trade with its setup, thesis, emotions, plan adherence, and mistakes in a CSV, then get a weekly review that finds what is working and what is leaking money by setup, time of day, holding period, emotion, and plan adherence. Not financial advice.

SkillTradingv1.0.0 (latest)Tested
Activity
New: be the first to try it
  1. 1#!/usr/bin/env node
  2. 2// Self-test for the trade journal. Usage: node scripts/selftest.mjs (exits 1 on any failure)
  3. 3import { readFileSync } from "node:fs";
  4. 4import { dirname, join } from "node:path";
  5. 5import { fileURLToPath } from "node:url";
  6. 6import { addTrade, closeTrade, csvField, enrich, holdingBucket, parseCsv, review, timeOfDay } from "./journal.mjs";
  7. 7
  8. 8const here = dirname(fileURLToPath(import.meta.url));
  9. 9let passed = 0;
  10. 10let failed = 0;
  11. 11function close(a, e, label, tol = 1e-9) {
  12. 12 if (typeof a === "number" && Math.abs(a - e) <= tol) passed++;
  13. 13 else {
  14. 14 failed++;
  15. 15 console.error(`FAIL ${label}: expected ${e}, got ${a}`);
  16. 16 }
  17. 18function eq(a, e, label) {
  18. 19 if (JSON.stringify(a) === JSON.stringify(e)) passed++;
  19. 20 else {
  20. 21 failed++;
  21. 22 console.error(`FAIL ${label}: expected ${JSON.stringify(e)}, got ${JSON.stringify(a)}`);
  22. 23 }
  23. 25function throws(fn, text, label) {
  24. 26 try {
  25. 27 fn();
  26. 28 failed++;
  27. 29 console.error(`FAIL ${label}: no error`);
  28. 30 } catch (e) {
  29. 31 if (e.message.includes(text)) passed++;
  30. 32 else {
  31. 33 failed++;
  32. 34 console.error(`FAIL ${label}: ${e.message}`);
  33. 35 }
  34. 36 }
  35. 39const rows = parseCsv(readFileSync(join(here, "../examples/trades.csv"), "utf8"));
  36. 41// P&L and R by hand
  37. 43 const t1 = enrich(rows[0], 0); // long 100 @ 50.00 -> 51.20, stop 49.40, fees 1
  38. 44 close(t1.pnl, 119, "long pnl = 1.20 x 100 - 1");
  39. 45 close(t1.r, 119 / 60, "R = 119 / (0.60 x 100)");
  40. 46 eq([t1.time_of_day, t1.holding, t1.weekday], ["2 open (9:30-10:30)", "1 scalp (<30m)", "1 Mon"], "buckets for trade 1");
  41. 47 const t2 = enrich(rows[1], 1); // short 200 @ 30.00 -> 30.35, stop 30.50
  42. 48 close(t2.pnl, -71, "short pnl = -0.35 x 200 - 1");
  43. 49 close(t2.r, -0.71, "short R = -71 / 100");
  44. 50 const t5 = enrich(rows[4], 4); // held 2 days
  45. 51 eq(t5.holding, "3 swing (1-5 days)", "multi-day hold");
  46. 52 eq(enrich(rows[17], 17).closed, false, "blank closed_at is an open trade");
  47. 55// Buckets
  48. 56eq(timeOfDay(9 * 60 + 29), "1 pre-market (before 9:30)", "9:29 is pre-market");
  49. 57eq(timeOfDay(10 * 60 + 30), "3 midday (10:30-14:00)", "10:30 is midday");
  50. 58eq(timeOfDay(16 * 60), "5 after-hours (16:00+)", "16:00 is after-hours");
  51. 59eq(holdingBucket(29, true), "1 scalp (<30m)", "29 minutes is a scalp");
  52. 60eq(holdingBucket(6 * 1440, false), "4 position (>5 days)", "6 days is a position");
  53. 62// Review totals and patterns on the example journal
  54. 64 const r = review(rows, { minN: 3 });
  55. 65 eq(r.overall.n, 17, "17 closed trades");
  56. 66 close(r.overall.total_pnl, 763.4, "net P&L", 1e-6);
  57. 67 eq(r.open_trades.map((t) => t.id), ["18"], "trade 18 is open");
  58. 68 const setup = Object.fromEntries(r.groups.setup.map((g) => [g.key, g]));
  59. 69 eq([setup["opening range breakout"].n, setup["opening range breakout"].wins], [8, 6], "ORB 6 of 8");
  60. 70 close(setup["vwap fade"].total_pnl, -355, "vwap fade total", 1e-6);
  61. 71 const kinds = r.patterns.map((p) => `${p.kind}:${p.dimension}:${p.key}`);
  62. 72 eq(kinds.includes("edge:Setup:opening range breakout"), true, "finds the ORB edge");
  63. 73 eq(kinds.includes("leak:Setup:vwap fade"), true, "finds the vwap fade leak");
  64. 74 eq(kinds.includes("leak:Followed plan:no"), true, "finds the off-plan leak");
  65. 75 eq(kinds.includes("mistake:Mistakes tagged:chased"), true, "lists the chasing mistake");
  66. 76 eq(r.patterns.some((p) => p.key === "power hour trend"), false, "groups under min-n are not patterns");
  67. 77 const week = review(rows, { from: "2026-10-26", to: "2026-10-30" });
  68. 78 eq(week.overall.n, 8, "date filter keeps the last week's 8 closed trades");
  69. 81// Adding trades
  70. 83 const first = addTrade("", { symbol: "xyz", side: "Long", qty: "10", entry: "5", opened_at: "2026-11-02 09:31", setup: "gap fill", thesis: 'Said "fill", then, filled' });
  71. 84 eq(first.id, 1, "first id is 1");
  72. 85 eq(first.header.startsWith("id,opened_at,closed_at,symbol"), true, "header on a new file");
  73. 86 eq(first.line.includes('"Said ""fill"", then, filled"'), true, "quotes commas and quotes");
  74. 87 const next = addTrade(readFileSync(join(here, "../examples/trades.csv"), "utf8"), { symbol: "XYZ", side: "short", qty: "1", entry: "5", exit: "4", opened_at: "2026-11-02 09:31", closed_at: "2026-11-02 10:00", setup: "x" });
  75. 88 eq([next.id, next.header], [19, null], "next id after 18, no header");
  76. 89 throws(() => addTrade("", { symbol: "X", side: "long", qty: "1", entry: "5", opened_at: "2026-11-02 9:31", setup: "x" }), "must look like", "time format");
  77. 90 throws(() => addTrade("", { symbol: "X", side: "long", qty: "1", entry: "5", opened_at: "2026-11-02 09:31", closed_at: "2026-11-02 09:00", exit: "5", setup: "x" }), "before opened_at", "close before open");
  78. 91 throws(() => addTrade("", { symbol: "X", side: "long", qty: "1", entry: "5", opened_at: "2026-11-02 09:31" }), "setup is required", "setup required");
  79. 92 throws(() => addTrade("", { symbol: "X", side: "long", qty: "1", entry: "5", opened_at: "2026-11-02 09:31", closed_at: "2026-11-02 10:00", setup: "x" }), "exit price", "closed needs exit");
  80. 93 eq(csvField("=HYPERLINK(1)"), "'=HYPERLINK(1)", "neutralizes spreadsheet formulas");
  81. 94 eq(csvField("-0.5"), "-0.5", "keeps negative numbers");
  82. 97// Closing an open trade rewrites only that row
  83. 99 const text = readFileSync(join(here, "../examples/trades.csv"), "utf8");
  84. 100 const updated = closeTrade(text, "18", { exit: "30.90", closed_at: "2026-11-02 10:05", emotion_exit: "relieved" });
  85. 101 const t18 = enrich(parseCsv(updated).find((r) => r.id === "18"), 17);
  86. 102 close(t18.pnl, 60, "short 100 from 31.50 to 30.90 = +60");
  87. 103 eq(t18.holding, "3 swing (1-5 days)", "Friday to Monday is a swing");
  88. 104 eq(parseCsv(updated).length, 18, "row count unchanged");
  89. 105 throws(() => closeTrade(updated, "18", { exit: "1", closed_at: "2026-11-02 11:00" }), "already closed", "cannot close twice");
  90. 106 throws(() => closeTrade(text, "99", {}), "no trade with id 99", "unknown id");
  91. 109console.log(`${passed} passed, ${failed} failed`);
  92. 110process.exit(failed ? 1 : 0);