@talocode/tradia 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # Tradia
2
+
3
+ Local-first trading intelligence for prop firm traders.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npx @talocode/tradia
9
+ ```
10
+
11
+ Global install:
12
+
13
+ ```bash
14
+ npm install -g @talocode/tradia
15
+ tradia
16
+ ```
17
+
18
+ ## First run
19
+
20
+ ```bash
21
+ tradia init
22
+ tradia import ./trades.csv
23
+ tradia stats
24
+ tradia review week
25
+ ```
26
+
27
+ ## Storage
28
+
29
+ Tradia stores data locally in `~/.tradia/`:
30
+
31
+ - `config.json`
32
+ - `trades.json`
33
+ - `rules.json`
34
+ - `reports/`
35
+
36
+ No secrets, telemetry, broker connections, or paid APIs are required.
37
+
38
+ ## CSV import
39
+
40
+ Tradia imports common broker CSV exports and normalizes them into a local trade journal. See `docs/CSV_IMPORT.md` for the supported column mapping and a sample file.
41
+
42
+ ## Commands
43
+
44
+ ```bash
45
+ tradia
46
+ tradia --version
47
+ tradia --help
48
+ tradia init
49
+ tradia import ./trades.csv
50
+ tradia trades list
51
+ tradia stats
52
+ tradia mistakes
53
+ tradia risk
54
+ tradia review week
55
+ tradia plan tomorrow
56
+ tradia rules set ftmo
57
+ tradia rules status
58
+ tradia doctor
59
+ ```
60
+
61
+ ## Limitations
62
+
63
+ - CSV import first
64
+ - No broker connection yet
65
+ - No auto-trading
66
+ - No financial advice
67
+ - No paid APIs required
68
+ - No cloud sync yet
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/lib/version.ts
7
+ var PACKAGE_VERSION = "0.1.0";
8
+
9
+ // src/lib/terminal.ts
10
+ import readline from "readline";
11
+ async function promptMenu(title, items) {
12
+ console.clear();
13
+ console.log(title);
14
+ console.log("");
15
+ items.forEach((item, index) => console.log(`${index + 1}. ${item}`));
16
+ const answer = await new Promise((resolve) => {
17
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
18
+ rl.question("\nSelect an option: ", (value) => {
19
+ rl.close();
20
+ resolve(value);
21
+ });
22
+ });
23
+ const choice = Number(answer);
24
+ return Number.isInteger(choice) && choice >= 1 && choice <= items.length ? choice - 1 : items.length - 1;
25
+ }
26
+
27
+ // src/lib/storage.ts
28
+ import { mkdir, readFile, writeFile, stat } from "fs/promises";
29
+ import { homedir } from "os";
30
+ import { join } from "path";
31
+ function getStoragePaths(baseDir = join(homedir(), ".tradia")) {
32
+ return {
33
+ baseDir,
34
+ configFile: join(baseDir, "config.json"),
35
+ tradesFile: join(baseDir, "trades.json"),
36
+ rulesFile: join(baseDir, "rules.json"),
37
+ reportsDir: join(baseDir, "reports")
38
+ };
39
+ }
40
+ async function ensureStorage(baseDir = getStoragePaths().baseDir) {
41
+ const paths = getStoragePaths(baseDir);
42
+ await mkdir(paths.baseDir, { recursive: true });
43
+ await mkdir(paths.reportsDir, { recursive: true });
44
+ return paths;
45
+ }
46
+ async function readJsonFile(path, fallback) {
47
+ try {
48
+ const raw = await readFile(path, "utf8");
49
+ return JSON.parse(raw);
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ }
54
+ async function writeJsonFile(path, value) {
55
+ await mkdir(join(path, ".."), { recursive: true });
56
+ await writeFile(path, `${JSON.stringify(value, null, 2)}
57
+ `, "utf8");
58
+ }
59
+ async function fileExists(path) {
60
+ try {
61
+ await stat(path);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // src/lib/csv-importer.ts
69
+ import { readFile as readFile2 } from "fs/promises";
70
+
71
+ // src/lib/trade-normalizer.ts
72
+ import crypto from "crypto";
73
+ function firstValue(row, keys) {
74
+ for (const key of keys) {
75
+ const value = row[key];
76
+ if (value != null && String(value).trim() !== "") return String(value).trim();
77
+ }
78
+ return void 0;
79
+ }
80
+ function parseNumber(value) {
81
+ if (value == null) return void 0;
82
+ const normalized = value.replace(/[$,\s]/g, "");
83
+ if (normalized === "") return void 0;
84
+ const num = Number(normalized);
85
+ return Number.isFinite(num) ? num : void 0;
86
+ }
87
+ function parseDate(value) {
88
+ if (!value) return void 0;
89
+ const normalized = value.trim();
90
+ const time = new Date(normalized).getTime();
91
+ return Number.isFinite(time) ? new Date(time).toISOString() : void 0;
92
+ }
93
+ function normalizeSide(value) {
94
+ const lower = value?.toLowerCase() ?? "";
95
+ if (["buy", "long", "b"].includes(lower)) return "buy";
96
+ if (["sell", "short", "s"].includes(lower)) return "sell";
97
+ return "unknown";
98
+ }
99
+ function normalizeTrade(row, fallbackIndex) {
100
+ const symbol = (firstValue(row, ["symbol", "pair", "instrument", "ticker"]) ?? "").toUpperCase();
101
+ const openedAt = parseDate(firstValue(row, ["date/time", "datetime", "date", "open time", "opened at"])) ?? parseDate(firstValue(row, ["close time", "closed at"])) ?? "";
102
+ const pnl = parseNumber(firstValue(row, ["profit/loss", "profit", "p/l", "pnl", "net pnl"])) ?? 0;
103
+ const commission = parseNumber(firstValue(row, ["commission"]));
104
+ const swap = parseNumber(firstValue(row, ["swap"]));
105
+ const netPnl = pnl + (commission ?? 0) + (swap ?? 0);
106
+ const id = firstValue(row, ["magic", "order id", "order_id", "ticket", "id"]) ?? crypto.createHash("sha1").update(JSON.stringify(row)).digest("hex");
107
+ if (!symbol || !openedAt || !Number.isFinite(netPnl)) return null;
108
+ const closedAt = parseDate(firstValue(row, ["close time", "closed at"]));
109
+ const entryPrice = parseNumber(firstValue(row, ["entry", "entry price", "open price"]));
110
+ const exitPrice = parseNumber(firstValue(row, ["exit", "exit price", "close price"]));
111
+ const lotSize = parseNumber(firstValue(row, ["size", "lot", "lot size", "volume"]));
112
+ const accountBalance = parseNumber(firstValue(row, ["balance", "account balance"]));
113
+ const durationMinutes = parseNumber(firstValue(row, ["duration", "duration minutes"]));
114
+ return {
115
+ id: `${id}-${fallbackIndex}`,
116
+ openedAt,
117
+ closedAt,
118
+ symbol,
119
+ side: normalizeSide(firstValue(row, ["side/type", "side", "type"])),
120
+ lotSize,
121
+ entryPrice,
122
+ exitPrice,
123
+ pnl,
124
+ commission,
125
+ swap,
126
+ netPnl,
127
+ accountBalance,
128
+ durationMinutes,
129
+ source: "csv",
130
+ raw: row
131
+ };
132
+ }
133
+
134
+ // src/lib/csv-importer.ts
135
+ function splitCsvLine(line) {
136
+ const cells = [];
137
+ let current = "";
138
+ let quoted = false;
139
+ for (let i = 0; i < line.length; i += 1) {
140
+ const char = line[i];
141
+ if (char === '"') {
142
+ if (quoted && line[i + 1] === '"') {
143
+ current += '"';
144
+ i += 1;
145
+ } else {
146
+ quoted = !quoted;
147
+ }
148
+ continue;
149
+ }
150
+ if (char === "," && !quoted) {
151
+ cells.push(current.trim());
152
+ current = "";
153
+ continue;
154
+ }
155
+ current += char;
156
+ }
157
+ cells.push(current.trim());
158
+ return cells;
159
+ }
160
+ function parseCsv(content) {
161
+ const lines = content.split(/\r?\n/).filter((line) => line.trim() !== "");
162
+ if (lines.length === 0) return [];
163
+ const headers = splitCsvLine(lines[0]).map((header) => header.trim().toLowerCase());
164
+ return lines.slice(1).map((line) => {
165
+ const values = splitCsvLine(line);
166
+ const row = {};
167
+ headers.forEach((header, index) => {
168
+ row[header] = values[index] ?? "";
169
+ });
170
+ return row;
171
+ });
172
+ }
173
+ async function importTradesFromCsv(path) {
174
+ const content = await readFile2(path, "utf8");
175
+ const rows = parseCsv(content);
176
+ const warnings = [];
177
+ const trades = [];
178
+ rows.forEach((row, index) => {
179
+ const trade = normalizeTrade(row, index);
180
+ if (!trade) {
181
+ warnings.push({ row: index + 2, message: "Invalid row skipped" });
182
+ return;
183
+ }
184
+ trades.push(trade);
185
+ });
186
+ const seen = /* @__PURE__ */ new Set();
187
+ const deduped = [];
188
+ for (const trade of trades) {
189
+ if (seen.has(trade.id)) continue;
190
+ seen.add(trade.id);
191
+ deduped.push(trade);
192
+ }
193
+ return { trades: deduped, warnings };
194
+ }
195
+
196
+ // src/commands/import.ts
197
+ async function runImport(csvPath) {
198
+ const { tradesFile } = await ensureStorage();
199
+ const existing = await readJsonFile(tradesFile, []);
200
+ const { trades, warnings } = await importTradesFromCsv(csvPath);
201
+ const merged = [...existing];
202
+ const seen = new Set(existing.map((trade) => trade.id));
203
+ for (const trade of trades) {
204
+ if (seen.has(trade.id)) continue;
205
+ seen.add(trade.id);
206
+ merged.push(trade);
207
+ }
208
+ await writeJsonFile(tradesFile, merged);
209
+ return [
210
+ `Imported ${trades.length} trades from ${csvPath}.`,
211
+ `Stored ${merged.length} total trades.`,
212
+ warnings.length ? `${warnings.length} invalid rows skipped.` : "No invalid rows skipped."
213
+ ].join("\n");
214
+ }
215
+
216
+ // src/lib/analytics.ts
217
+ function mean(values) {
218
+ if (!values.length) return void 0;
219
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
220
+ }
221
+ function streak(values, positive) {
222
+ let best = 0;
223
+ let current = 0;
224
+ for (const value of values) {
225
+ const hit = positive ? value > 0 : value < 0;
226
+ current = hit ? current + 1 : 0;
227
+ best = Math.max(best, current);
228
+ }
229
+ return best;
230
+ }
231
+ function groupBy(trades, fn) {
232
+ const map = /* @__PURE__ */ new Map();
233
+ for (const trade of trades) {
234
+ const key = fn(trade);
235
+ const group = map.get(key) ?? { key, pnl: 0, count: 0 };
236
+ group.pnl += trade.netPnl;
237
+ group.count += 1;
238
+ map.set(key, group);
239
+ }
240
+ return [...map.values()].sort((a, b) => b.pnl - a.pnl);
241
+ }
242
+ function sessionOf(dateIso) {
243
+ const hour = new Date(dateIso).getUTCHours();
244
+ if (hour >= 12 && hour < 17) return "london";
245
+ if (hour >= 17 && hour < 22) return "new-york";
246
+ if (hour >= 22 || hour < 3) return "asian";
247
+ return "off-session";
248
+ }
249
+ function calculateStats(trades) {
250
+ const pnlValues = trades.map((t) => t.netPnl);
251
+ const wins = trades.filter((t) => t.netPnl > 0);
252
+ const losses = trades.filter((t) => t.netPnl < 0);
253
+ const grossProfit = wins.reduce((sum, t) => sum + t.netPnl, 0);
254
+ const grossLoss = Math.abs(losses.reduce((sum, t) => sum + t.netPnl, 0));
255
+ const cumulative = [];
256
+ let running = 0;
257
+ let peak = 0;
258
+ let maxDrawdown = 0;
259
+ for (const trade of trades) {
260
+ running += trade.netPnl;
261
+ peak = Math.max(peak, running);
262
+ maxDrawdown = Math.max(maxDrawdown, peak - running);
263
+ cumulative.push(running);
264
+ }
265
+ return {
266
+ totalTrades: trades.length,
267
+ winRate: trades.length ? wins.length / trades.length : 0,
268
+ netPnL: pnlValues.reduce((sum, value) => sum + value, 0),
269
+ averageWin: mean(wins.map((t) => t.netPnl)),
270
+ averageLoss: mean(losses.map((t) => t.netPnl)),
271
+ profitFactor: grossLoss === 0 ? void 0 : grossProfit / grossLoss,
272
+ bestTrade: trades.slice().sort((a, b) => b.netPnl - a.netPnl)[0],
273
+ worstTrade: trades.slice().sort((a, b) => a.netPnl - b.netPnl)[0],
274
+ maxLosingStreak: streak(pnlValues, false),
275
+ maxWinningStreak: streak(pnlValues, true),
276
+ mostTradedSymbol: groupBy(trades, (t) => t.symbol)[0],
277
+ bestSymbol: groupBy(trades, (t) => t.symbol).sort((a, b) => b.pnl - a.pnl)[0],
278
+ worstSymbol: groupBy(trades, (t) => t.symbol).sort((a, b) => a.pnl - b.pnl)[0],
279
+ bestDay: groupBy(trades, (t) => t.openedAt.slice(0, 10))[0],
280
+ worstDay: groupBy(trades, (t) => t.openedAt.slice(0, 10)).sort((a, b) => a.pnl - b.pnl)[0],
281
+ bestSession: groupBy(trades, (t) => sessionOf(t.openedAt))[0],
282
+ worstSession: groupBy(trades, (t) => sessionOf(t.openedAt)).sort((a, b) => a.pnl - b.pnl)[0],
283
+ drawdownEstimate: maxDrawdown,
284
+ cumulative
285
+ };
286
+ }
287
+ function analyzeBySymbol(trades) {
288
+ return groupBy(trades, (t) => t.symbol);
289
+ }
290
+ function analyzeByDay(trades) {
291
+ return groupBy(trades, (t) => t.openedAt.slice(0, 10));
292
+ }
293
+ function analyzeBySession(trades) {
294
+ return groupBy(trades, (t) => sessionOf(t.openedAt));
295
+ }
296
+ function getSession(dateIso) {
297
+ return sessionOf(dateIso);
298
+ }
299
+
300
+ // src/lib/mistake-detector.ts
301
+ function detectMistakes(trades) {
302
+ const stats = calculateStats(trades);
303
+ const daily = analyzeByDay(trades);
304
+ const bySymbol = analyzeBySymbol(trades);
305
+ const bySession = analyzeBySession(trades);
306
+ const losses = trades.filter((t) => t.netPnl < 0);
307
+ const averageLot = trades.reduce((sum, trade) => sum + (trade.lotSize ?? 0), 0) / Math.max(trades.length, 1);
308
+ const overtrading = daily.some((day) => day.count > Math.max(5, Math.ceil(trades.length / 10)));
309
+ const increasingSizeAfterLoss = trades.some((trade, index) => {
310
+ const prev = trades[index - 1];
311
+ return Boolean(prev && prev.netPnl < 0 && (trade.lotSize ?? 0) > (prev.lotSize ?? 0));
312
+ });
313
+ const badSessionTrading = bySession.some((session) => session.key !== "off-session" && session.pnl < 0);
314
+ const repeatedLosingSymbol = bySymbol.some((symbol) => symbol.count >= 2 && symbol.pnl < 0);
315
+ const losersLonger = trades.some(
316
+ (trade) => trade.netPnl < 0 && trade.durationMinutes != null && trade.durationMinutes > 2 * (stats.bestTrade?.durationMinutes ?? trade.durationMinutes)
317
+ );
318
+ const tooManyTradesPerDay = daily.some((day) => day.count >= 6);
319
+ const outlierLoss = losses.some((trade) => trade.netPnl <= (stats.averageLoss ?? 0) * 2);
320
+ const inconsistentSizing = trades.filter((t) => t.lotSize != null).some((trade) => Math.abs((trade.lotSize ?? 0) - averageLot) > averageLot * 0.5);
321
+ const revengeTrading = trades.some((trade, index) => {
322
+ const prev = trades[index - 1];
323
+ return Boolean(prev && prev.netPnl < 0 && trade.openedAt > prev.openedAt && getSession(prev.openedAt) === getSession(trade.openedAt));
324
+ });
325
+ return {
326
+ overtrading,
327
+ increasingSizeAfterLoss,
328
+ badSessionTrading,
329
+ repeatedLosingSymbol,
330
+ losersLonger,
331
+ tooManyTradesPerDay,
332
+ outlierLoss,
333
+ inconsistentSizing,
334
+ revengeTrading
335
+ };
336
+ }
337
+
338
+ // src/lib/risk-engine.ts
339
+ function buildRiskReport(trades) {
340
+ const stats = calculateStats(trades);
341
+ const bySymbol = analyzeBySymbol(trades).sort((a, b) => a.pnl - b.pnl);
342
+ const byDay = analyzeByDay(trades).sort((a, b) => a.pnl - b.pnl);
343
+ const bySession = analyzeBySession(trades).sort((a, b) => a.pnl - b.pnl);
344
+ const biggestLoss = stats.worstTrade?.netPnl;
345
+ const averageLoss = stats.averageLoss;
346
+ const lossStreaks = stats.maxLosingStreak;
347
+ const overtradingSignals = trades.length > 0 && stats.totalTrades / Math.max(1, new Set(trades.map((t) => t.openedAt.slice(0, 10))).size) > 4;
348
+ const revengeTradingSignals = trades.some((trade, index) => Boolean(trades[index - 1] && trades[index - 1].netPnl < 0 && trade.netPnl < 0));
349
+ return {
350
+ biggestLoss,
351
+ averageLoss,
352
+ lossStreaks,
353
+ drawdownEstimate: stats.drawdownEstimate,
354
+ worstSymbols: bySymbol.slice(0, 3),
355
+ worstDays: byDay.slice(0, 3),
356
+ worstSessions: bySession.slice(0, 3),
357
+ overtradingSignals,
358
+ revengeTradingSignals
359
+ };
360
+ }
361
+
362
+ // src/lib/report-formatters.ts
363
+ var money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
364
+ function fmt(value) {
365
+ if (value == null || Number.isNaN(value)) return "n/a";
366
+ return money.format(value);
367
+ }
368
+ function formatStats(trades) {
369
+ const s = calculateStats(trades);
370
+ return [
371
+ `Total trades: ${s.totalTrades}`,
372
+ `Win rate: ${(s.winRate * 100).toFixed(1)}%`,
373
+ `Net PnL: ${fmt(s.netPnL)}`,
374
+ `Average win: ${fmt(s.averageWin)}`,
375
+ `Average loss: ${fmt(s.averageLoss)}`,
376
+ `Profit factor: ${s.profitFactor?.toFixed(2) ?? "n/a"}`,
377
+ `Best trade: ${fmt(s.bestTrade?.netPnl)}`,
378
+ `Worst trade: ${fmt(s.worstTrade?.netPnl)}`,
379
+ `Max losing streak: ${s.maxLosingStreak}`,
380
+ `Max winning streak: ${s.maxWinningStreak}`,
381
+ `Most traded symbol: ${s.mostTradedSymbol?.key ?? "n/a"}`,
382
+ `Best symbol: ${s.bestSymbol?.key ?? "n/a"}`,
383
+ `Worst symbol: ${s.worstSymbol?.key ?? "n/a"}`,
384
+ `Best day: ${s.bestDay?.key ?? "n/a"}`,
385
+ `Worst day: ${s.worstDay?.key ?? "n/a"}`,
386
+ `Best session: ${s.bestSession?.key ?? "n/a"}`,
387
+ `Worst session: ${s.worstSession?.key ?? "n/a"}`
388
+ ].join("\n");
389
+ }
390
+ function formatRisk(trades) {
391
+ const r = buildRiskReport(trades);
392
+ return [
393
+ `Biggest loss: ${fmt(r.biggestLoss)}`,
394
+ `Average loss: ${fmt(r.averageLoss)}`,
395
+ `Loss streaks: ${r.lossStreaks}`,
396
+ `Drawdown estimate: ${fmt(r.drawdownEstimate)}`,
397
+ `Worst symbols: ${r.worstSymbols.map((x) => `${x.key} (${fmt(x.pnl)})`).join(", ") || "n/a"}`,
398
+ `Worst days: ${r.worstDays.map((x) => `${x.key} (${fmt(x.pnl)})`).join(", ") || "n/a"}`,
399
+ `Worst sessions: ${r.worstSessions.map((x) => `${x.key} (${fmt(x.pnl)})`).join(", ") || "n/a"}`,
400
+ `Overtrading signals: ${r.overtradingSignals ? "yes" : "no"}`,
401
+ `Revenge trading signals: ${r.revengeTradingSignals ? "yes" : "no"}`
402
+ ].join("\n");
403
+ }
404
+ function formatMistakes(trades) {
405
+ const m = detectMistakes(trades);
406
+ return [
407
+ `Overtrading: ${m.overtrading ? "yes" : "no"}`,
408
+ `Increasing size after loss: ${m.increasingSizeAfterLoss ? "yes" : "no"}`,
409
+ `Trading during bad session: ${m.badSessionTrading ? "yes" : "no"}`,
410
+ `Repeating same losing symbol: ${m.repeatedLosingSymbol ? "yes" : "no"}`,
411
+ `Holding losers longer than winners: ${m.losersLonger ? "yes" : "no"}`,
412
+ `Too many trades per day: ${m.tooManyTradesPerDay ? "yes" : "no"}`,
413
+ `Large outlier loss: ${m.outlierLoss ? "yes" : "no"}`,
414
+ `Inconsistent lot sizing: ${m.inconsistentSizing ? "yes" : "no"}`
415
+ ].join("\n");
416
+ }
417
+ function formatWeeklyReview(trades) {
418
+ const s = calculateStats(trades);
419
+ const m = detectMistakes(trades);
420
+ return [
421
+ `What went well: ${s.bestSymbol?.key ?? "n/a"} and ${s.bestSession?.key ?? "n/a"} were the strongest areas.`,
422
+ `What went wrong: ${s.worstSymbol?.key ?? "n/a"} and ${s.worstSession?.key ?? "n/a"} were the weakest areas.`,
423
+ `Biggest mistake: ${m.overtrading ? "Overtrading showed up." : "No dominant mistake was detected."}`,
424
+ `Biggest risk leak: ${m.increasingSizeAfterLoss ? "Sizing increased after losses." : "No clear size escalation leak found."}`,
425
+ `Best symbol/session: ${s.bestSymbol?.key ?? "n/a"} / ${s.bestSession?.key ?? "n/a"}`,
426
+ `Worst symbol/session: ${s.worstSymbol?.key ?? "n/a"} / ${s.worstSession?.key ?? "n/a"}`,
427
+ `Rule for next week: Keep trade count controlled and stop after the first rule break.`
428
+ ].join("\n");
429
+ }
430
+ function formatTomorrowPlan(trades) {
431
+ const s = calculateStats(trades);
432
+ const badSymbols = [s.worstSymbol?.key, s.bestSymbol && s.bestSymbol.pnl < 0 ? s.bestSymbol.key : void 0].filter(Boolean);
433
+ return [
434
+ `Max trades suggestion: ${Math.max(1, Math.min(5, Math.ceil(trades.length / 3)))}`,
435
+ `Risk cap suggestion: ${trades.length ? "Keep per-trade risk at or below 1% until consistency improves." : "Import trades first."}`,
436
+ `Symbols to avoid: ${badSymbols.length ? badSymbols.join(", ") : "n/a"}`,
437
+ `Session warnings: avoid sessions that have been net negative in your history.`,
438
+ `One discipline rule: do not add size after a loss.`,
439
+ `Not financial advice.`
440
+ ].join("\n");
441
+ }
442
+ function formatRulesStatus(trades, rules) {
443
+ const lines = [
444
+ `Ruleset: ${rules.name}`,
445
+ `Max daily loss percent: ${rules.maxDailyLossPercent ?? "n/a"}`,
446
+ `Max total drawdown percent: ${rules.maxTotalDrawdownPercent ?? "n/a"}`,
447
+ `Max trades per day: ${rules.maxTradesPerDay ?? "n/a"}`,
448
+ `Max risk per trade percent: ${rules.maxRiskPerTradePercent ?? "n/a"}`
449
+ ];
450
+ if (!trades.length) {
451
+ lines.push("No trades imported yet, so limits cannot be evaluated.");
452
+ }
453
+ return lines.join("\n");
454
+ }
455
+
456
+ // src/commands/stats.ts
457
+ async function runStats() {
458
+ const { tradesFile } = await ensureStorage();
459
+ const trades = await readJsonFile(tradesFile, []);
460
+ return formatStats(trades);
461
+ }
462
+
463
+ // src/commands/risk.ts
464
+ async function runRisk() {
465
+ const { tradesFile } = await ensureStorage();
466
+ const trades = await readJsonFile(tradesFile, []);
467
+ return formatRisk(trades);
468
+ }
469
+
470
+ // src/commands/mistakes.ts
471
+ async function runMistakes() {
472
+ const { tradesFile } = await ensureStorage();
473
+ const trades = await readJsonFile(tradesFile, []);
474
+ return formatMistakes(trades);
475
+ }
476
+
477
+ // src/commands/review.ts
478
+ async function runReview(_range) {
479
+ const { tradesFile } = await ensureStorage();
480
+ const trades = await readJsonFile(tradesFile, []);
481
+ return `${formatWeeklyReview(trades)}
482
+ Not financial advice.`;
483
+ }
484
+
485
+ // src/commands/plan.ts
486
+ async function runPlan(_range) {
487
+ const { tradesFile } = await ensureStorage();
488
+ const trades = await readJsonFile(tradesFile, []);
489
+ return formatTomorrowPlan(trades);
490
+ }
491
+
492
+ // src/lib/rules-engine.ts
493
+ function defaultFtmoRules() {
494
+ return {
495
+ name: "ftmo",
496
+ maxDailyLossPercent: 5,
497
+ maxTotalDrawdownPercent: 10,
498
+ maxTradesPerDay: 5,
499
+ maxRiskPerTradePercent: 1
500
+ };
501
+ }
502
+
503
+ // src/commands/rules.ts
504
+ async function runRulesSet(name) {
505
+ const { rulesFile } = await ensureStorage();
506
+ const rules = name.toLowerCase() === "ftmo" ? defaultFtmoRules() : { name };
507
+ await writeJsonFile(rulesFile, rules);
508
+ return `Saved ruleset ${rules.name}.`;
509
+ }
510
+ async function runRulesStatus() {
511
+ const { rulesFile, tradesFile } = await ensureStorage();
512
+ const rules = await readJsonFile(rulesFile, defaultFtmoRules());
513
+ const trades = await readJsonFile(tradesFile, []);
514
+ return formatRulesStatus(trades, rules);
515
+ }
516
+
517
+ // src/commands/init.ts
518
+ async function runInit() {
519
+ const paths = await ensureStorage();
520
+ return [`Initialized Tradia storage at ${paths.baseDir}.`, "No secrets stored.", "No network calls required."].join("\n");
521
+ }
522
+
523
+ // src/tui/app.ts
524
+ async function runInteractiveHome() {
525
+ const choice = await promptMenu("Tradia\n\nLocal-first trading intelligence for prop firm traders.\n", [
526
+ "Import trades",
527
+ "Account stats",
528
+ "Risk report",
529
+ "Mistake report",
530
+ "Weekly review",
531
+ "Tomorrow plan",
532
+ "Prop firm rules",
533
+ "Settings",
534
+ "Exit"
535
+ ]);
536
+ const actions = [
537
+ async () => console.log(await runImport("./trades.csv")),
538
+ async () => console.log(await runStats()),
539
+ async () => console.log(await runRisk()),
540
+ async () => console.log(await runMistakes()),
541
+ async () => console.log(await runReview("week")),
542
+ async () => console.log(await runPlan("tomorrow")),
543
+ async () => console.log(await runRulesStatus()),
544
+ async () => console.log(await runInit()),
545
+ async () => process.exit(0)
546
+ ];
547
+ await actions[choice]();
548
+ }
549
+
550
+ // src/commands/trades.ts
551
+ async function runTradesList() {
552
+ const { tradesFile } = await ensureStorage();
553
+ const trades = await readJsonFile(tradesFile, []);
554
+ if (!trades.length) return "No trades imported yet.";
555
+ return trades.map((trade) => `${trade.openedAt.slice(0, 10)} ${trade.symbol} ${trade.side} pnl=${trade.netPnl.toFixed(2)} id=${trade.id}`).join("\n");
556
+ }
557
+
558
+ // src/commands/doctor.ts
559
+ async function runDoctor() {
560
+ const paths = await ensureStorage();
561
+ const nodeVersion = process.version;
562
+ const checks = [
563
+ `Node version: ${nodeVersion}`,
564
+ `Config dir: ${paths.baseDir}`,
565
+ `Trades file exists: ${await fileExists(paths.tradesFile) ? "yes" : "no"}`,
566
+ `Rules file exists: ${await fileExists(paths.rulesFile) ? "yes" : "no"}`,
567
+ `Package version: ${PACKAGE_VERSION}`,
568
+ "No network/API required: yes",
569
+ "No secrets printed: yes"
570
+ ];
571
+ return checks.join("\n");
572
+ }
573
+
574
+ // src/commands/index.ts
575
+ function registerCommands(program) {
576
+ program.command("init").description("Create local Tradia storage").action(async () => console.log(await runInit()));
577
+ program.command("import <csv>").description("Import a CSV trade history export").action(async (csv) => console.log(await runImport(csv)));
578
+ program.command("trades").description("Trade commands").command("list").description("List imported trades").action(async () => console.log(await runTradesList()));
579
+ program.command("stats").description("Show trade stats").action(async () => console.log(await runStats()));
580
+ program.command("mistakes").description("Show mistake report").action(async () => console.log(await runMistakes()));
581
+ program.command("risk").description("Show risk report").action(async () => console.log(await runRisk()));
582
+ program.command("review <range>").description("Generate a deterministic review").action(async (range) => console.log(await runReview(range)));
583
+ program.command("plan <range>").description("Generate a deterministic plan").action(async (range) => console.log(await runPlan(range)));
584
+ const rules = program.command("rules").description("Prop firm rules");
585
+ rules.command("set <name>").description("Set prop firm rules").action(async (name) => console.log(await runRulesSet(name)));
586
+ rules.command("status").description("Show rules status").action(async () => console.log(await runRulesStatus()));
587
+ program.command("doctor").description("Run environment checks").action(async () => console.log(await runDoctor()));
588
+ }
589
+
590
+ // src/cli.ts
591
+ async function main() {
592
+ const program = new Command();
593
+ program.name("tradia").description("Local-first trading intelligence for prop firm traders.").version(PACKAGE_VERSION);
594
+ registerCommands(program);
595
+ program.action(async () => {
596
+ await runInteractiveHome();
597
+ });
598
+ await program.parseAsync(process.argv);
599
+ }
600
+
601
+ // src/index.ts
602
+ void main();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@talocode/tradia",
3
+ "version": "0.1.0",
4
+ "description": "Local-first terminal trading intelligence for prop firm traders.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Talocode",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/talocode/tradia.git",
11
+ "directory": "packages/tradia-tui"
12
+ },
13
+ "homepage": "https://github.com/talocode/tradia#tradia-tui",
14
+ "bugs": {
15
+ "url": "https://github.com/talocode/tradia/issues"
16
+ },
17
+ "keywords": ["trading", "terminal", "tui", "journal", "risk", "prop-firm"],
18
+ "bin": {
19
+ "tradia": "dist/index.js"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "files": ["dist"],
25
+ "engines": {
26
+ "node": ">=18"
27
+ },
28
+ "scripts": {
29
+ "dev": "tsx src/index.ts",
30
+ "build": "tsup && chmod +x dist/index.js",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "vitest run",
33
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
34
+ },
35
+ "dependencies": {
36
+ "chalk": "^5.4.1",
37
+ "commander": "^13.1.0",
38
+ "ora": "^8.2.0"
39
+ },
40
+ "devDependencies": {
41
+ "tsup": "^8.5.0",
42
+ "tsx": "^4.19.4",
43
+ "typescript": "^5.8.3",
44
+ "vitest": "^3.2.4"
45
+ }
46
+ }