tradeblocks-mcp 0.1.0 → 0.1.2

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 CHANGED
@@ -189,26 +189,28 @@ backtests/
189
189
  ### Core Tools
190
190
  | Tool | Description |
191
191
  |------|-------------|
192
- | `list_backtests` | List all available blocks |
192
+ | `list_backtests` | List all available blocks with summary stats |
193
+ | `get_block_info` | Detailed info for a specific block |
193
194
  | `get_statistics` | Performance metrics (Sharpe, Sortino, drawdown, etc.) |
194
- | `get_trades` | Raw trade data with optional filters |
195
- | `reprocess_block` | Re-parse CSVs and recalculate statistics |
195
+ | `get_strategy_comparison` | Compare strategies within a block |
196
+ | `compare_blocks` | Compare statistics across multiple blocks |
197
+ | `get_trades` | Raw trade data with filtering, sorting, pagination |
196
198
 
197
199
  ### Analysis Tools
198
200
  | Tool | Description |
199
201
  |------|-------------|
200
202
  | `run_walk_forward` | Walk-forward analysis with configurable windows |
201
203
  | `run_monte_carlo` | Monte Carlo simulation with worst-case scenarios |
202
- | `calculate_correlation` | Strategy correlation matrix (Kendall's tau) |
203
- | `get_tail_risk` | VaR, CVaR, and max drawdown analysis |
204
- | `calculate_position_sizing` | Kelly criterion position sizing |
204
+ | `get_correlation_matrix` | Strategy correlation matrix (Kendall, Spearman, Pearson) |
205
+ | `get_tail_risk` | Tail dependence and copula-based risk analysis |
206
+ | `get_position_sizing` | Kelly criterion position sizing |
205
207
 
206
208
  ### Performance Tools
207
209
  | Tool | Description |
208
210
  |------|-------------|
209
211
  | `get_performance_charts` | 16 chart types (equity, drawdown, distribution) |
210
212
  | `get_period_returns` | Returns aggregated by time period |
211
- | `compare_backtest_vs_actual` | Backtest vs live performance comparison |
213
+ | `compare_backtest_to_actual` | Backtest vs live performance comparison |
212
214
 
213
215
  ### Report Builder Tools
214
216
  | Tool | Description |
@@ -174,6 +174,48 @@ async function discoverCsvFiles(folderPath) {
174
174
  }
175
175
  return { mappings, unrecognized };
176
176
  }
177
+ async function getCsvFileMtimes(blockPath, mappings) {
178
+ const mtimes = {};
179
+ for (const [type, filename] of Object.entries(mappings)) {
180
+ if (!filename) continue;
181
+ try {
182
+ const filePath = path.join(blockPath, filename);
183
+ const stat2 = await fs.stat(filePath);
184
+ mtimes[type] = stat2.mtimeMs;
185
+ } catch {
186
+ }
187
+ }
188
+ return Object.keys(mtimes).length > 0 ? mtimes : void 0;
189
+ }
190
+ async function isCacheValid(blockPath, metadata) {
191
+ if (!metadata.csvFileMtimes) {
192
+ return false;
193
+ }
194
+ const mappings = metadata.csvMappings || { tradelog: "tradelog.csv" };
195
+ const tradelogFilename = mappings.tradelog || "tradelog.csv";
196
+ try {
197
+ const tradelogPath = path.join(blockPath, tradelogFilename);
198
+ const stat2 = await fs.stat(tradelogPath);
199
+ if (stat2.mtimeMs !== metadata.csvFileMtimes.tradelog) {
200
+ return false;
201
+ }
202
+ } catch {
203
+ return false;
204
+ }
205
+ if (metadata.csvFileMtimes.dailylog) {
206
+ const dailylogFilename = mappings.dailylog || "dailylog.csv";
207
+ try {
208
+ const dailylogPath = path.join(blockPath, dailylogFilename);
209
+ const stat2 = await fs.stat(dailylogPath);
210
+ if (stat2.mtimeMs !== metadata.csvFileMtimes.dailylog) {
211
+ return false;
212
+ }
213
+ } catch {
214
+ return false;
215
+ }
216
+ }
217
+ return true;
218
+ }
177
219
  function logCsvDiscoveryWarning(folderName, csvFiles) {
178
220
  console.error(`Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`);
179
221
  console.error(` Found: ${csvFiles.join(", ")}`);
@@ -296,6 +338,36 @@ async function saveMetadata(blockPath, metadata) {
296
338
  const metadataPath = path.join(blockPath, ".block.json");
297
339
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
298
340
  }
341
+ async function buildBlockMetadata(options) {
342
+ const {
343
+ blockId,
344
+ blockPath,
345
+ trades,
346
+ dailyLogs,
347
+ existingMetadata,
348
+ csvMappings,
349
+ cachedStats
350
+ } = options;
351
+ const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
352
+ const dates = trades.map((t) => new Date(t.dateOpened).getTime());
353
+ const csvFileMtimes = await getCsvFileMtimes(blockPath, csvMappings);
354
+ return {
355
+ blockId,
356
+ name: existingMetadata?.name || blockId,
357
+ createdAt: existingMetadata?.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
358
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
359
+ tradeCount: trades.length,
360
+ dailyLogCount: dailyLogs?.length ?? 0,
361
+ dateRange: {
362
+ start: dates.length > 0 ? new Date(Math.min(...dates)).toISOString() : null,
363
+ end: dates.length > 0 ? new Date(Math.max(...dates)).toISOString() : null
364
+ },
365
+ strategies,
366
+ csvMappings,
367
+ csvFileMtimes,
368
+ cachedStats
369
+ };
370
+ }
299
371
  async function loadBlock(baseDir, blockId) {
300
372
  const blockPath = path.join(baseDir, blockId);
301
373
  const metadata = await loadMetadata(blockPath);
@@ -378,7 +450,8 @@ async function listBlocks(baseDir) {
378
450
  continue;
379
451
  }
380
452
  const hasDailyLog = !!dailylogFilename;
381
- if (metadata?.cachedStats && !needsMetadataUpdate) {
453
+ const cacheValid = metadata?.cachedStats && !needsMetadataUpdate && await isCacheValid(blockPath, metadata);
454
+ if (cacheValid && metadata?.cachedStats) {
382
455
  blocks.push({
383
456
  blockId: entry.name,
384
457
  name: metadata.name || entry.name,
@@ -395,50 +468,36 @@ async function listBlocks(baseDir) {
395
468
  } else {
396
469
  try {
397
470
  const trades = await loadTrades(blockPath, tradelogFilename);
398
- const strategies = Array.from(
399
- new Set(trades.map((t) => t.strategy))
400
- ).sort();
401
- const dates = trades.map((t) => new Date(t.dateOpened).getTime());
402
471
  const totalPl = trades.reduce((sum, t) => sum + t.pl, 0);
403
472
  const totalCommissions = trades.reduce(
404
473
  (sum, t) => sum + t.openingCommissionsFees + t.closingCommissionsFees,
405
474
  0
406
475
  );
476
+ const csvMappings = { tradelog: tradelogFilename };
477
+ if (dailylogFilename) {
478
+ csvMappings.dailylog = dailylogFilename;
479
+ }
480
+ const updatedMetadata = await buildBlockMetadata({
481
+ blockId: entry.name,
482
+ blockPath,
483
+ trades,
484
+ existingMetadata: metadata,
485
+ csvMappings
486
+ });
407
487
  blocks.push({
408
488
  blockId: entry.name,
409
- name: entry.name,
489
+ name: updatedMetadata.name,
410
490
  tradeCount: trades.length,
411
491
  hasDailyLog,
412
492
  dateRange: {
413
- start: dates.length > 0 ? new Date(Math.min(...dates)) : null,
414
- end: dates.length > 0 ? new Date(Math.max(...dates)) : null
493
+ start: updatedMetadata.dateRange.start ? new Date(updatedMetadata.dateRange.start) : null,
494
+ end: updatedMetadata.dateRange.end ? new Date(updatedMetadata.dateRange.end) : null
415
495
  },
416
- strategies,
496
+ strategies: updatedMetadata.strategies,
417
497
  totalPl,
418
498
  netPl: totalPl - totalCommissions
419
499
  });
420
- if (needsMetadataUpdate) {
421
- const csvMappings = { tradelog: tradelogFilename };
422
- if (dailylogFilename) {
423
- csvMappings.dailylog = dailylogFilename;
424
- }
425
- const updatedMetadata = {
426
- blockId: entry.name,
427
- name: entry.name,
428
- createdAt: metadata?.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
429
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
430
- tradeCount: trades.length,
431
- dailyLogCount: 0,
432
- // Will be updated if daily logs loaded
433
- dateRange: {
434
- start: dates.length > 0 ? new Date(Math.min(...dates)).toISOString() : null,
435
- end: dates.length > 0 ? new Date(Math.max(...dates)).toISOString() : null
436
- },
437
- strategies,
438
- csvMappings
439
- };
440
- await saveMetadata(blockPath, updatedMetadata);
441
- }
500
+ await saveMetadata(blockPath, updatedMetadata);
442
501
  } catch (error) {
443
502
  console.error(`Error loading block ${entry.name}:`, error);
444
503
  }
@@ -628,23 +687,17 @@ async function importCsv(baseDir, options) {
628
687
  if (trade) trades.push(trade);
629
688
  }
630
689
  if (trades.length > 0) {
631
- const dates = trades.map((t) => new Date(t.dateOpened).getTime());
632
- dateRange = {
633
- start: new Date(Math.min(...dates)).toISOString(),
634
- end: new Date(Math.max(...dates)).toISOString()
635
- };
636
- strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();
637
- const metadata = {
690
+ const csvMappings = { tradelog: targetFilename };
691
+ const metadata = await buildBlockMetadata({
638
692
  blockId,
639
- name,
640
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
641
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
642
- tradeCount: trades.length,
643
- dailyLogCount: 0,
644
- dateRange,
645
- strategies
646
- };
693
+ blockPath,
694
+ trades,
695
+ csvMappings
696
+ });
697
+ metadata.name = name;
647
698
  await saveMetadata(blockPath, metadata);
699
+ dateRange = metadata.dateRange;
700
+ strategies = metadata.strategies;
648
701
  }
649
702
  } else if (csvType === "dailylog") {
650
703
  const entries = [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utils/block-loader.ts","../../../lib/models/reporting-trade.ts"],"sourcesContent":["/**\n * Block Data Loader\n *\n * Utilities for loading and managing block data from folder-based structure.\n * Blocks are directories containing tradelog.csv (required) and optional dailylog.csv.\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport type { Trade } from \"@lib/models/trade\";\nimport type { DailyLogEntry } from \"@lib/models/daily-log\";\nimport type { ReportingTrade } from \"@lib/models/reporting-trade\";\nimport { REPORTING_TRADE_COLUMN_ALIASES } from \"@lib/models/reporting-trade\";\n\n/**\n * CSV file mappings for flexible discovery\n */\nexport interface CsvMappings {\n tradelog?: string;\n dailylog?: string;\n reportinglog?: string;\n}\n\n/**\n * Block metadata stored in .block.json\n */\nexport interface BlockMetadata {\n blockId: string;\n name: string;\n createdAt: string;\n updatedAt: string;\n tradeCount: number;\n dailyLogCount: number;\n dateRange: {\n start: string | null;\n end: string | null;\n };\n strategies: string[];\n /** CSV filename mappings when files don't have standard names */\n csvMappings?: CsvMappings;\n cachedStats?: {\n totalPl: number;\n netPl: number;\n winRate: number;\n sharpeRatio?: number;\n maxDrawdown: number;\n calculatedAt: string;\n };\n}\n\n/**\n * Block info summary for listing\n */\nexport interface BlockInfo {\n blockId: string;\n name: string;\n tradeCount: number;\n hasDailyLog: boolean;\n dateRange: {\n start: Date | null;\n end: Date | null;\n };\n strategies: string[];\n totalPl: number;\n netPl: number;\n}\n\n/**\n * Loaded block data\n */\nexport interface LoadedBlock {\n blockId: string;\n trades: Trade[];\n dailyLogs?: DailyLogEntry[];\n metadata?: BlockMetadata;\n}\n\n/**\n * Parse a YYYY-MM-DD date string preserving the calendar date.\n * Same approach as lib/processing for consistency.\n */\nfunction parseDatePreservingCalendarDay(dateStr: string): Date {\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (match) {\n const [, year, month, day] = match;\n return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));\n }\n return new Date(dateStr);\n}\n\n/**\n * Parse numeric value from CSV string\n */\nfunction parseNumber(\n value: string | undefined,\n defaultValue?: number\n): number {\n if (!value || value.trim() === \"\" || value.toLowerCase() === \"nan\") {\n if (defaultValue !== undefined) return defaultValue;\n return 0;\n }\n const cleaned = value.replace(/[$,%]/g, \"\").trim();\n const parsed = parseFloat(cleaned);\n return isNaN(parsed) ? defaultValue ?? 0 : parsed;\n}\n\n/**\n * Parse CSV content into array of record objects\n */\nfunction parseCSV(content: string): Record<string, string>[] {\n const lines = content.trim().split(\"\\n\");\n if (lines.length < 2) return [];\n\n const headers = parseCSVLine(lines[0]);\n const records: Record<string, string>[] = [];\n\n for (let i = 1; i < lines.length; i++) {\n const values = parseCSVLine(lines[i]);\n if (values.length === 0) continue;\n\n const record: Record<string, string> = {};\n headers.forEach((header, idx) => {\n record[header] = values[idx] || \"\";\n });\n records.push(record);\n }\n\n return records;\n}\n\n/**\n * Parse a single CSV line handling quoted fields\n */\nfunction parseCSVLine(line: string): string[] {\n const result: string[] = [];\n let current = \"\";\n let inQuotes = false;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n\n if (char === '\"') {\n if (inQuotes && line[i + 1] === '\"') {\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n } else if (char === \",\" && !inQuotes) {\n result.push(current.trim());\n current = \"\";\n } else {\n current += char;\n }\n }\n\n result.push(current.trim());\n return result;\n}\n\n/**\n * CSV type detection result\n */\ntype CsvType = \"tradelog\" | \"dailylog\" | \"reportinglog\" | null;\n\n/**\n * Read just the header line from a CSV file (for detection)\n */\nasync function readCsvHeaders(filePath: string): Promise<string[]> {\n const content = await fs.readFile(filePath, \"utf-8\");\n const firstLine = content.split(\"\\n\")[0] || \"\";\n return parseCSVLine(firstLine).map((h) => h.toLowerCase().trim());\n}\n\n/**\n * Detect CSV type by examining column headers.\n * Returns the detected type or null if unrecognized.\n */\nasync function detectCsvType(filePath: string): Promise<CsvType> {\n try {\n const headers = await readCsvHeaders(filePath);\n\n // Trade log detection:\n // Required: \"P/L\" or \"P&L\" or \"Profit/Loss\"\n // Plus at least 2 of: \"Date Opened\", \"Date Closed\", \"Symbol\", \"Strategy\", \"Contracts\", \"Premium\"\n const plColumnAliases = [\"p/l\", \"p&l\", \"profit/loss\", \"pl\"];\n const tradeOptionalColumns = [\n \"date opened\",\n \"date closed\",\n \"symbol\",\n \"strategy\",\n \"contracts\",\n \"no. of contracts\",\n \"premium\",\n \"legs\",\n ];\n\n const hasPl = plColumnAliases.some((alias) => headers.includes(alias));\n const matchedTradeColumns = tradeOptionalColumns.filter((col) =>\n headers.some((h) => h.includes(col) || col.includes(h))\n );\n\n if (hasPl && matchedTradeColumns.length >= 2) {\n return \"tradelog\";\n }\n\n // Daily log detection:\n // Required: \"Date\", and (\"Portfolio Value\" or \"Value\" or \"Equity\" or \"Net Liquidity\")\n const hasDate = headers.some(\n (h) => h === \"date\" || h.includes(\"date\") && !h.includes(\"opened\") && !h.includes(\"closed\")\n );\n const valueColumnAliases = [\n \"portfolio value\",\n \"value\",\n \"equity\",\n \"net liquidity\",\n \"netliquidity\",\n ];\n const hasValue = valueColumnAliases.some((alias) =>\n headers.some((h) => h.includes(alias) || alias.includes(h))\n );\n\n if (hasDate && hasValue && !hasPl) {\n // Daily log has date + value but typically no P/L column (or if it has P/L, it's daily P/L)\n // To distinguish, check if it lacks trade-specific columns\n if (matchedTradeColumns.length < 2) {\n return \"dailylog\";\n }\n }\n\n // Reporting log detection:\n // Has \"Actual P/L\" or columns from REPORTING_TRADE_COLUMN_ALIASES\n // Or has \"Trade ID\" + \"Reported\" style columns\n const reportingAliases = Object.keys(REPORTING_TRADE_COLUMN_ALIASES).map(\n (k) => k.toLowerCase()\n );\n const hasReportingColumns = reportingAliases.some((alias) =>\n headers.includes(alias)\n );\n const hasActualPl = headers.some(\n (h) => h.includes(\"actual\") && h.includes(\"p\")\n );\n const hasReportedStyle =\n headers.includes(\"trade id\") ||\n headers.some((h) => h.includes(\"reported\"));\n\n if (hasActualPl || hasReportingColumns || hasReportedStyle) {\n // Double-check it's not a regular tradelog\n if (!hasPl || hasActualPl) {\n return \"reportinglog\";\n }\n }\n\n // If we have P/L and trade columns, fallback to tradelog\n if (hasPl && matchedTradeColumns.length >= 1) {\n return \"tradelog\";\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Discover CSV files in a folder and detect their types.\n * Returns mapping of detected CSV types to filenames.\n */\nasync function discoverCsvFiles(\n folderPath: string\n): Promise<{ mappings: CsvMappings; unrecognized: string[] }> {\n const mappings: CsvMappings = {};\n const unrecognized: string[] = [];\n\n try {\n const entries = await fs.readdir(folderPath, { withFileTypes: true });\n const csvFiles = entries\n .filter((e) => e.isFile() && e.name.toLowerCase().endsWith(\".csv\"))\n .map((e) => e.name);\n\n // First, check for exact standard names\n if (csvFiles.includes(\"tradelog.csv\")) {\n mappings.tradelog = \"tradelog.csv\";\n }\n if (csvFiles.includes(\"dailylog.csv\")) {\n mappings.dailylog = \"dailylog.csv\";\n }\n if (csvFiles.includes(\"reportinglog.csv\")) {\n mappings.reportinglog = \"reportinglog.csv\";\n }\n\n // For any remaining CSVs, detect by content\n for (const csvFile of csvFiles) {\n // Skip if already mapped via exact name\n if (\n csvFile === \"tradelog.csv\" ||\n csvFile === \"dailylog.csv\" ||\n csvFile === \"reportinglog.csv\"\n ) {\n continue;\n }\n\n const csvPath = path.join(folderPath, csvFile);\n const detectedType = await detectCsvType(csvPath);\n\n if (detectedType) {\n // Only assign if we haven't found this type yet\n if (detectedType === \"tradelog\" && !mappings.tradelog) {\n mappings.tradelog = csvFile;\n } else if (detectedType === \"dailylog\" && !mappings.dailylog) {\n mappings.dailylog = csvFile;\n } else if (detectedType === \"reportinglog\" && !mappings.reportinglog) {\n mappings.reportinglog = csvFile;\n } else {\n // Type already found, this is an extra CSV\n unrecognized.push(csvFile);\n }\n } else {\n unrecognized.push(csvFile);\n }\n }\n } catch {\n // Folder read error - return empty\n }\n\n return { mappings, unrecognized };\n}\n\n/**\n * Log warning when folder has CSVs but none match expected patterns\n */\nfunction logCsvDiscoveryWarning(\n folderName: string,\n csvFiles: string[]\n): void {\n console.error(`Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`);\n console.error(` Found: ${csvFiles.join(\", \")}`);\n console.error(` Expected columns: P/L, Date Opened, Date Closed, Symbol, Strategy`);\n}\n\n/**\n * Convert raw CSV record to Trade object\n */\nfunction convertToTrade(raw: Record<string, string>): Trade | null {\n try {\n const dateOpened = parseDatePreservingCalendarDay(raw[\"Date Opened\"]);\n if (isNaN(dateOpened.getTime())) return null;\n\n const dateClosed = raw[\"Date Closed\"]\n ? parseDatePreservingCalendarDay(raw[\"Date Closed\"])\n : undefined;\n\n const strategy = (raw[\"Strategy\"] || \"\").trim() || \"Unknown\";\n\n const rawPremium = (raw[\"Premium\"] || \"\").replace(/[$,]/g, \"\").trim();\n const premiumPrecision: Trade[\"premiumPrecision\"] =\n rawPremium && !rawPremium.includes(\".\") ? \"cents\" : \"dollars\";\n\n return {\n dateOpened,\n timeOpened: raw[\"Time Opened\"] || \"00:00:00\",\n openingPrice: parseNumber(raw[\"Opening Price\"]),\n legs: raw[\"Legs\"] || \"\",\n premium: parseNumber(raw[\"Premium\"]),\n premiumPrecision,\n closingPrice: raw[\"Closing Price\"]\n ? parseNumber(raw[\"Closing Price\"])\n : undefined,\n dateClosed,\n timeClosed: raw[\"Time Closed\"] || undefined,\n avgClosingCost: raw[\"Avg. Closing Cost\"]\n ? parseNumber(raw[\"Avg. Closing Cost\"])\n : undefined,\n reasonForClose: raw[\"Reason For Close\"] || undefined,\n pl: parseNumber(raw[\"P/L\"]),\n numContracts: Math.round(parseNumber(raw[\"No. of Contracts\"], 1)),\n fundsAtClose: parseNumber(raw[\"Funds at Close\"]),\n marginReq: parseNumber(raw[\"Margin Req.\"]),\n strategy,\n openingCommissionsFees: parseNumber(\n raw[\"Opening Commissions + Fees\"] || raw[\"Opening comms & fees\"],\n 0\n ),\n closingCommissionsFees: parseNumber(\n raw[\"Closing Commissions + Fees\"] || raw[\"Closing comms & fees\"],\n 0\n ),\n openingShortLongRatio: parseNumber(raw[\"Opening Short/Long Ratio\"], 0),\n closingShortLongRatio: raw[\"Closing Short/Long Ratio\"]\n ? parseNumber(raw[\"Closing Short/Long Ratio\"])\n : undefined,\n openingVix: raw[\"Opening VIX\"]\n ? parseNumber(raw[\"Opening VIX\"])\n : undefined,\n closingVix: raw[\"Closing VIX\"]\n ? parseNumber(raw[\"Closing VIX\"])\n : undefined,\n gap: raw[\"Gap\"] ? parseNumber(raw[\"Gap\"]) : undefined,\n movement: raw[\"Movement\"] ? parseNumber(raw[\"Movement\"]) : undefined,\n maxProfit: raw[\"Max Profit\"]\n ? parseNumber(raw[\"Max Profit\"])\n : undefined,\n maxLoss: raw[\"Max Loss\"] ? parseNumber(raw[\"Max Loss\"]) : undefined,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Convert raw CSV record to DailyLogEntry object\n */\nfunction convertToDailyLogEntry(\n raw: Record<string, string>,\n blockId?: string\n): DailyLogEntry | null {\n try {\n const date = parseDatePreservingCalendarDay(raw[\"Date\"]);\n if (isNaN(date.getTime())) return null;\n\n return {\n date,\n netLiquidity: parseNumber(raw[\"Net Liquidity\"]),\n currentFunds: parseNumber(raw[\"Current Funds\"]),\n withdrawn: parseNumber(raw[\"Withdrawn\"], 0),\n tradingFunds: parseNumber(raw[\"Trading Funds\"]),\n dailyPl: parseNumber(raw[\"P/L\"]),\n dailyPlPct: parseNumber(raw[\"P/L %\"]),\n drawdownPct: parseNumber(raw[\"Drawdown %\"]),\n blockId,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Load trades from tradelog CSV file\n * @param blockPath - Path to the block directory\n * @param filename - CSV filename (default: \"tradelog.csv\")\n */\nasync function loadTrades(\n blockPath: string,\n filename: string = \"tradelog.csv\"\n): Promise<Trade[]> {\n const tradelogPath = path.join(blockPath, filename);\n const content = await fs.readFile(tradelogPath, \"utf-8\");\n const records = parseCSV(content);\n\n const trades: Trade[] = [];\n for (const record of records) {\n const trade = convertToTrade(record);\n if (trade) {\n trades.push(trade);\n }\n }\n\n // Sort by date and time\n trades.sort((a, b) => {\n const dateCompare =\n new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime();\n if (dateCompare !== 0) return dateCompare;\n return a.timeOpened.localeCompare(b.timeOpened);\n });\n\n return trades;\n}\n\n/**\n * Load daily logs from dailylog CSV file (optional)\n * @param blockPath - Path to the block directory\n * @param blockId - Block identifier\n * @param filename - CSV filename (default: \"dailylog.csv\")\n */\nasync function loadDailyLogs(\n blockPath: string,\n blockId: string,\n filename: string = \"dailylog.csv\"\n): Promise<DailyLogEntry[] | undefined> {\n const dailylogPath = path.join(blockPath, filename);\n\n try {\n await fs.access(dailylogPath);\n const content = await fs.readFile(dailylogPath, \"utf-8\");\n const records = parseCSV(content);\n\n const entries: DailyLogEntry[] = [];\n for (const record of records) {\n const entry = convertToDailyLogEntry(record, blockId);\n if (entry) {\n entries.push(entry);\n }\n }\n\n // Sort by date\n entries.sort(\n (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()\n );\n\n return entries.length > 0 ? entries : undefined;\n } catch {\n // Daily log doesn't exist - that's fine\n return undefined;\n }\n}\n\n/**\n * Load block metadata from .block.json\n */\nexport async function loadMetadata(\n blockPath: string\n): Promise<BlockMetadata | undefined> {\n const metadataPath = path.join(blockPath, \".block.json\");\n\n try {\n const content = await fs.readFile(metadataPath, \"utf-8\");\n return JSON.parse(content) as BlockMetadata;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Save block metadata to .block.json\n */\nexport async function saveMetadata(\n blockPath: string,\n metadata: BlockMetadata\n): Promise<void> {\n const metadataPath = path.join(blockPath, \".block.json\");\n await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), \"utf-8\");\n}\n\n/**\n * Load a complete block (trades + optional daily logs)\n * Uses CSV mappings from metadata if available for flexible filename support.\n */\nexport async function loadBlock(\n baseDir: string,\n blockId: string\n): Promise<LoadedBlock> {\n const blockPath = path.join(baseDir, blockId);\n\n // Load metadata first to get CSV mappings\n const metadata = await loadMetadata(blockPath);\n const mappings = metadata?.csvMappings;\n\n // Determine tradelog filename (from mappings or default)\n const tradelogFilename = mappings?.tradelog || \"tradelog.csv\";\n const tradelogPath = path.join(blockPath, tradelogFilename);\n\n // Verify tradelog exists\n try {\n await fs.access(tradelogPath);\n } catch {\n // If using default name failed, try discovery\n if (!mappings?.tradelog) {\n const discovered = await discoverCsvFiles(blockPath);\n if (discovered.mappings.tradelog) {\n // Found a tradelog via discovery - use it\n const trades = await loadTrades(blockPath, discovered.mappings.tradelog);\n const dailyLogs = discovered.mappings.dailylog\n ? await loadDailyLogs(blockPath, blockId, discovered.mappings.dailylog)\n : undefined;\n return { blockId, trades, dailyLogs, metadata };\n }\n }\n throw new Error(`Block not found or missing tradelog: ${blockId}`);\n }\n\n // Determine dailylog filename\n const dailylogFilename = mappings?.dailylog || \"dailylog.csv\";\n\n const trades = await loadTrades(blockPath, tradelogFilename);\n const dailyLogs = await loadDailyLogs(blockPath, blockId, dailylogFilename);\n\n return {\n blockId,\n trades,\n dailyLogs,\n metadata,\n };\n}\n\n/**\n * List all valid blocks in the base directory.\n * Uses flexible CSV discovery to find blocks with non-standard CSV filenames.\n */\nexport async function listBlocks(baseDir: string): Promise<BlockInfo[]> {\n const blocks: BlockInfo[] = [];\n\n try {\n const entries = await fs.readdir(baseDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith(\".\")) continue; // Skip hidden folders\n\n const blockPath = path.join(baseDir, entry.name);\n\n // Try to load existing metadata first\n const metadata = await loadMetadata(blockPath);\n\n // Determine CSV filenames - prefer metadata mappings, then exact names, then discovery\n let tradelogFilename: string | undefined;\n let dailylogFilename: string | undefined;\n let needsMetadataUpdate = false;\n\n if (metadata?.csvMappings?.tradelog) {\n // Use cached mappings from metadata\n tradelogFilename = metadata.csvMappings.tradelog;\n dailylogFilename = metadata.csvMappings.dailylog;\n } else {\n // Check for exact standard names first\n const standardTradelogPath = path.join(blockPath, \"tradelog.csv\");\n const standardDailylogPath = path.join(blockPath, \"dailylog.csv\");\n\n let hasStandardTradelog = false;\n\n try {\n await fs.access(standardTradelogPath);\n hasStandardTradelog = true;\n tradelogFilename = \"tradelog.csv\";\n } catch {\n // No standard tradelog\n }\n\n try {\n await fs.access(standardDailylogPath);\n dailylogFilename = \"dailylog.csv\";\n } catch {\n // No standard dailylog\n }\n\n // If no standard tradelog, try discovery\n if (!hasStandardTradelog) {\n const discovered = await discoverCsvFiles(blockPath);\n\n if (discovered.mappings.tradelog) {\n tradelogFilename = discovered.mappings.tradelog;\n dailylogFilename = discovered.mappings.dailylog || dailylogFilename;\n needsMetadataUpdate = true;\n\n // Log that we discovered non-standard files\n if (tradelogFilename !== \"tradelog.csv\") {\n console.error(\n `Discovered trade log in '${entry.name}': ${tradelogFilename}`\n );\n }\n } else if (discovered.unrecognized.length > 0) {\n // Has CSV files but none recognized as tradelog\n logCsvDiscoveryWarning(entry.name, discovered.unrecognized);\n continue; // Skip this folder\n } else {\n // No CSV files at all\n continue;\n }\n }\n }\n\n // If still no tradelog, skip this folder\n if (!tradelogFilename) {\n continue;\n }\n\n const hasDailyLog = !!dailylogFilename;\n\n // Use cached stats from metadata if available\n if (metadata?.cachedStats && !needsMetadataUpdate) {\n blocks.push({\n blockId: entry.name,\n name: metadata.name || entry.name,\n tradeCount: metadata.tradeCount,\n hasDailyLog,\n dateRange: {\n start: metadata.dateRange.start\n ? new Date(metadata.dateRange.start)\n : null,\n end: metadata.dateRange.end\n ? new Date(metadata.dateRange.end)\n : null,\n },\n strategies: metadata.strategies,\n totalPl: metadata.cachedStats.totalPl,\n netPl: metadata.cachedStats.netPl,\n });\n } else {\n // Load trades to get basic info\n try {\n const trades = await loadTrades(blockPath, tradelogFilename);\n const strategies = Array.from(\n new Set(trades.map((t) => t.strategy))\n ).sort();\n const dates = trades.map((t) => new Date(t.dateOpened).getTime());\n const totalPl = trades.reduce((sum, t) => sum + t.pl, 0);\n const totalCommissions = trades.reduce(\n (sum, t) =>\n sum + t.openingCommissionsFees + t.closingCommissionsFees,\n 0\n );\n\n blocks.push({\n blockId: entry.name,\n name: entry.name,\n tradeCount: trades.length,\n hasDailyLog,\n dateRange: {\n start: dates.length > 0 ? new Date(Math.min(...dates)) : null,\n end: dates.length > 0 ? new Date(Math.max(...dates)) : null,\n },\n strategies,\n totalPl,\n netPl: totalPl - totalCommissions,\n });\n\n // Update metadata with discovered mappings for faster future loads\n if (needsMetadataUpdate) {\n const csvMappings: CsvMappings = { tradelog: tradelogFilename };\n if (dailylogFilename) {\n csvMappings.dailylog = dailylogFilename;\n }\n\n const updatedMetadata: BlockMetadata = {\n blockId: entry.name,\n name: entry.name,\n createdAt: metadata?.createdAt || new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n tradeCount: trades.length,\n dailyLogCount: 0, // Will be updated if daily logs loaded\n dateRange: {\n start:\n dates.length > 0\n ? new Date(Math.min(...dates)).toISOString()\n : null,\n end:\n dates.length > 0\n ? new Date(Math.max(...dates)).toISOString()\n : null,\n },\n strategies,\n csvMappings,\n };\n\n // Save updated metadata (cache the mappings)\n await saveMetadata(blockPath, updatedMetadata);\n }\n } catch (error) {\n console.error(`Error loading block ${entry.name}:`, error);\n }\n }\n }\n\n // Sort by name\n blocks.sort((a, b) => a.name.localeCompare(b.name));\n\n return blocks;\n } catch (error) {\n throw new Error(`Failed to list blocks: ${(error as Error).message}`);\n }\n}\n\n/**\n * Normalize header names using column aliases\n */\nfunction normalizeRecordHeaders(\n raw: Record<string, string>\n): Record<string, string> {\n const normalized: Record<string, string> = { ...raw };\n Object.entries(REPORTING_TRADE_COLUMN_ALIASES).forEach(\n ([alias, canonical]) => {\n if (normalized[alias] !== undefined) {\n normalized[canonical] = normalized[alias];\n delete normalized[alias];\n }\n }\n );\n return normalized;\n}\n\n/**\n * Convert raw CSV record to ReportingTrade object\n */\nfunction convertToReportingTrade(\n raw: Record<string, string>\n): ReportingTrade | null {\n try {\n const normalized = normalizeRecordHeaders(raw);\n\n const dateOpened = parseDatePreservingCalendarDay(normalized[\"Date Opened\"]);\n if (isNaN(dateOpened.getTime())) return null;\n\n const dateClosed = normalized[\"Date Closed\"]\n ? parseDatePreservingCalendarDay(normalized[\"Date Closed\"])\n : undefined;\n\n const strategy = (normalized[\"Strategy\"] || \"\").trim() || \"Unknown\";\n\n return {\n strategy,\n dateOpened,\n timeOpened: normalized[\"Time Opened\"] || undefined,\n openingPrice: parseNumber(normalized[\"Opening Price\"]),\n legs: normalized[\"Legs\"] || \"\",\n initialPremium: parseNumber(normalized[\"Initial Premium\"]),\n numContracts: parseNumber(normalized[\"No. of Contracts\"], 1),\n pl: parseNumber(normalized[\"P/L\"]),\n closingPrice: normalized[\"Closing Price\"]\n ? parseNumber(normalized[\"Closing Price\"])\n : undefined,\n dateClosed,\n timeClosed: normalized[\"Time Closed\"] || undefined,\n avgClosingCost: normalized[\"Avg. Closing Cost\"]\n ? parseNumber(normalized[\"Avg. Closing Cost\"])\n : undefined,\n reasonForClose: normalized[\"Reason For Close\"] || undefined,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Load reporting log (actual trades) from reportinglog CSV\n * Uses CSV mappings from metadata if available.\n * @throws Error if reportinglog CSV does not exist\n */\nexport async function loadReportingLog(\n baseDir: string,\n blockId: string\n): Promise<ReportingTrade[]> {\n const blockPath = path.join(baseDir, blockId);\n\n // Load metadata to check for CSV mappings\n const metadata = await loadMetadata(blockPath);\n const filename = metadata?.csvMappings?.reportinglog || \"reportinglog.csv\";\n const reportingLogPath = path.join(blockPath, filename);\n\n // Check if file exists - throw if not\n try {\n await fs.access(reportingLogPath);\n } catch {\n // Try discovery as fallback\n if (filename === \"reportinglog.csv\") {\n const discovered = await discoverCsvFiles(blockPath);\n if (discovered.mappings.reportinglog) {\n const altPath = path.join(blockPath, discovered.mappings.reportinglog);\n const content = await fs.readFile(altPath, \"utf-8\");\n const records = parseCSV(content);\n const trades: ReportingTrade[] = [];\n for (const record of records) {\n const trade = convertToReportingTrade(record);\n if (trade) trades.push(trade);\n }\n trades.sort(\n (a, b) =>\n new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()\n );\n return trades;\n }\n }\n throw new Error(`reportinglog.csv not found in block: ${blockId}`);\n }\n\n const content = await fs.readFile(reportingLogPath, \"utf-8\");\n const records = parseCSV(content);\n\n const trades: ReportingTrade[] = [];\n for (const record of records) {\n const trade = convertToReportingTrade(record);\n if (trade) {\n trades.push(trade);\n }\n }\n\n // Sort by date\n trades.sort(\n (a, b) =>\n new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()\n );\n\n return trades;\n}\n\n/**\n * Import CSV result\n */\nexport interface ImportCsvResult {\n blockId: string;\n name: string;\n recordCount: number;\n dateRange: {\n start: string | null;\n end: string | null;\n };\n strategies: string[];\n blockPath: string;\n}\n\n/**\n * Import CSV options\n */\nexport interface ImportCsvOptions {\n /** Absolute path to the CSV file */\n csvPath: string;\n /** Name for the block */\n blockName: string;\n /** Type of CSV data */\n csvType?: \"tradelog\" | \"dailylog\" | \"reportinglog\";\n}\n\n/**\n * Convert a string to kebab-case for blockId\n */\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\") // camelCase to kebab-case\n .replace(/[\\s_]+/g, \"-\") // spaces and underscores to hyphens\n .replace(/[^a-zA-Z0-9-]/g, \"\") // remove special characters\n .toLowerCase()\n .replace(/-+/g, \"-\") // collapse multiple hyphens\n .replace(/^-|-$/g, \"\"); // trim leading/trailing hyphens\n}\n\n/**\n * Validate CSV has required columns for the specified type\n */\nfunction validateCsvColumns(\n records: Record<string, string>[],\n csvType: \"tradelog\" | \"dailylog\" | \"reportinglog\"\n): { valid: boolean; error?: string } {\n if (records.length === 0) {\n return { valid: false, error: \"CSV file is empty or has no data rows\" };\n }\n\n const headers = Object.keys(records[0]);\n\n switch (csvType) {\n case \"tradelog\": {\n // Required columns for trade log\n const required = [\"Date Opened\", \"P/L\"];\n const missing = required.filter((col) => !headers.includes(col));\n if (missing.length > 0) {\n return {\n valid: false,\n error: `Missing required columns for tradelog: ${missing.join(\", \")}. Expected columns include: Date Opened, P/L, Strategy, Legs, etc.`,\n };\n }\n break;\n }\n case \"dailylog\": {\n // Required columns for daily log\n const required = [\"Date\", \"Net Liquidity\"];\n const missing = required.filter((col) => !headers.includes(col));\n if (missing.length > 0) {\n return {\n valid: false,\n error: `Missing required columns for dailylog: ${missing.join(\", \")}. Expected columns include: Date, Net Liquidity, P/L, etc.`,\n };\n }\n break;\n }\n case \"reportinglog\": {\n // Required columns for reporting log (with aliases)\n const dateOpenedAliases = [\"Date Opened\", \"date_opened\"];\n const plAliases = [\"P/L\", \"pl\"];\n const hasDateOpened = dateOpenedAliases.some((col) =>\n headers.includes(col)\n );\n const hasPl = plAliases.some((col) => headers.includes(col));\n const missing: string[] = [];\n if (!hasDateOpened) missing.push(\"Date Opened\");\n if (!hasPl) missing.push(\"P/L\");\n if (missing.length > 0) {\n return {\n valid: false,\n error: `Missing required columns for reportinglog: ${missing.join(\", \")}. Expected columns include: Date Opened (or date_opened), P/L (or pl), Strategy, etc.`,\n };\n }\n break;\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Import a CSV file into the blocks directory\n *\n * Requires local filesystem access. The MCP server must be running locally\n * (via npx tradeblocks-mcp or mcpb desktop extension) to access files.\n *\n * @param baseDir - Base directory for blocks\n * @param options - Import options: csvPath, blockName, csvType\n * @returns Import result with block info\n */\nexport async function importCsv(\n baseDir: string,\n options: ImportCsvOptions\n): Promise<ImportCsvResult> {\n const { csvPath, blockName, csvType = \"tradelog\" } = options;\n\n // Validate source file exists\n try {\n await fs.access(csvPath);\n } catch {\n throw new Error(`CSV file not found: ${csvPath}`);\n }\n\n // Read and parse the CSV\n const content = await fs.readFile(csvPath, \"utf-8\");\n const records = parseCSV(content);\n\n // Validate CSV has required columns\n const validation = validateCsvColumns(records, csvType);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n\n // Convert blockName to kebab-case for blockId\n const name = blockName;\n const blockId = toKebabCase(name);\n\n if (!blockId) {\n throw new Error(\n \"Could not derive a valid block ID from the filename or provided name\"\n );\n }\n\n // Check if block already exists\n const blockPath = path.join(baseDir, blockId);\n try {\n await fs.access(blockPath);\n throw new Error(\n `Block \"${blockId}\" already exists. Use a different blockName or delete the existing block first.`\n );\n } catch (error) {\n // Directory doesn't exist - good, we can create it\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw error; // Re-throw if it's not a \"not found\" error\n }\n }\n\n // Create block directory\n await fs.mkdir(blockPath, { recursive: true });\n\n // Determine target filename\n const targetFilename =\n csvType === \"tradelog\"\n ? \"tradelog.csv\"\n : csvType === \"dailylog\"\n ? \"dailylog.csv\"\n : \"reportinglog.csv\";\n\n // Copy CSV to block directory\n const targetPath = path.join(blockPath, targetFilename);\n await fs.copyFile(csvPath, targetPath);\n\n // Extract metadata based on CSV type\n let dateRange: { start: string | null; end: string | null } = {\n start: null,\n end: null,\n };\n let strategies: string[] = [];\n\n if (csvType === \"tradelog\") {\n // Parse trades to extract metadata\n const trades: Trade[] = [];\n for (const record of records) {\n const trade = convertToTrade(record);\n if (trade) trades.push(trade);\n }\n\n if (trades.length > 0) {\n const dates = trades.map((t) => new Date(t.dateOpened).getTime());\n dateRange = {\n start: new Date(Math.min(...dates)).toISOString(),\n end: new Date(Math.max(...dates)).toISOString(),\n };\n strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();\n\n // Create and save .block.json metadata\n const metadata: BlockMetadata = {\n blockId,\n name,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n tradeCount: trades.length,\n dailyLogCount: 0,\n dateRange,\n strategies,\n };\n await saveMetadata(blockPath, metadata);\n }\n } else if (csvType === \"dailylog\") {\n // Parse daily logs to extract date range\n const entries: DailyLogEntry[] = [];\n for (const record of records) {\n const entry = convertToDailyLogEntry(record, blockId);\n if (entry) entries.push(entry);\n }\n\n if (entries.length > 0) {\n const dates = entries.map((e) => new Date(e.date).getTime());\n dateRange = {\n start: new Date(Math.min(...dates)).toISOString(),\n end: new Date(Math.max(...dates)).toISOString(),\n };\n }\n // Note: dailylog-only blocks won't have a .block.json created here\n // They would typically be added to an existing tradelog block\n } else if (csvType === \"reportinglog\") {\n // Parse reporting trades to extract metadata\n const trades: ReportingTrade[] = [];\n for (const record of records) {\n const trade = convertToReportingTrade(record);\n if (trade) trades.push(trade);\n }\n\n if (trades.length > 0) {\n const dates = trades.map((t) => new Date(t.dateOpened).getTime());\n dateRange = {\n start: new Date(Math.min(...dates)).toISOString(),\n end: new Date(Math.max(...dates)).toISOString(),\n };\n strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();\n }\n }\n\n return {\n blockId,\n name,\n recordCount: records.length,\n dateRange,\n strategies,\n blockPath,\n };\n}\n","/**\n * Reporting trade model represents backtested strategy executions coming from the\n * strategy-trade-log.csv export. These records are used to align theoretical\n * performance with the real trade log for a block.\n */\nexport interface ReportingTrade {\n strategy: string\n dateOpened: Date\n timeOpened?: string\n openingPrice: number\n legs: string\n initialPremium: number\n numContracts: number\n pl: number\n closingPrice?: number\n dateClosed?: Date\n timeClosed?: string\n avgClosingCost?: number\n reasonForClose?: string\n}\n\n/**\n * Raw reporting trade data direct from the CSV prior to conversion.\n */\nexport interface RawReportingTradeData {\n \"Strategy\": string\n \"Date Opened\": string\n \"Time Opened\"?: string\n \"Opening Price\": string\n \"Legs\": string\n \"Initial Premium\": string\n \"No. of Contracts\": string\n \"P/L\": string\n \"Closing Price\"?: string\n \"Date Closed\"?: string\n \"Time Closed\"?: string\n \"Avg. Closing Cost\"?: string\n \"Reason For Close\"?: string\n}\n\n/**\n * Required columns that must be present for a reporting log import to be valid.\n */\nexport const REQUIRED_REPORTING_TRADE_COLUMNS = [\n \"Strategy\",\n \"Date Opened\",\n \"Opening Price\",\n \"Legs\",\n \"Initial Premium\",\n \"No. of Contracts\",\n \"P/L\",\n] as const\n\n/**\n * Column aliases to support slight variations in exports.\n */\nexport const REPORTING_TRADE_COLUMN_ALIASES: Record<string, string> = {\n \"Initial Premium ($)\": \"Initial Premium\",\n \"Initial Credit\": \"Initial Premium\",\n \"Contracts\": \"No. of Contracts\",\n \"Contracts Traded\": \"No. of Contracts\",\n \"PL\": \"P/L\",\n}\n"],"mappings":"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","names":["trades","dailyLogs","content","records"]}
1
+ {"version":3,"sources":["../src/utils/block-loader.ts","../../../lib/models/reporting-trade.ts"],"sourcesContent":["/**\n * Block Data Loader\n *\n * Utilities for loading and managing block data from folder-based structure.\n * Blocks are directories containing tradelog.csv (required) and optional dailylog.csv.\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport type { Trade } from \"@lib/models/trade\";\nimport type { DailyLogEntry } from \"@lib/models/daily-log\";\nimport type { ReportingTrade } from \"@lib/models/reporting-trade\";\nimport { REPORTING_TRADE_COLUMN_ALIASES } from \"@lib/models/reporting-trade\";\n\n/**\n * CSV file mappings for flexible discovery\n */\nexport interface CsvMappings {\n tradelog?: string;\n dailylog?: string;\n reportinglog?: string;\n}\n\n/**\n * Block metadata stored in .block.json\n */\nexport interface BlockMetadata {\n blockId: string;\n name: string;\n createdAt: string;\n updatedAt: string;\n tradeCount: number;\n dailyLogCount: number;\n dateRange: {\n start: string | null;\n end: string | null;\n };\n strategies: string[];\n /** CSV filename mappings when files don't have standard names */\n csvMappings?: CsvMappings;\n /** File modification times for cache invalidation */\n csvFileMtimes?: {\n tradelog?: number;\n dailylog?: number;\n reportinglog?: number;\n };\n cachedStats?: {\n totalPl: number;\n netPl: number;\n winRate: number;\n sharpeRatio?: number;\n maxDrawdown: number;\n calculatedAt: string;\n };\n}\n\n/**\n * Block info summary for listing\n */\nexport interface BlockInfo {\n blockId: string;\n name: string;\n tradeCount: number;\n hasDailyLog: boolean;\n dateRange: {\n start: Date | null;\n end: Date | null;\n };\n strategies: string[];\n totalPl: number;\n netPl: number;\n}\n\n/**\n * Loaded block data\n */\nexport interface LoadedBlock {\n blockId: string;\n trades: Trade[];\n dailyLogs?: DailyLogEntry[];\n metadata?: BlockMetadata;\n}\n\n/**\n * Parse a YYYY-MM-DD date string preserving the calendar date.\n * Same approach as lib/processing for consistency.\n */\nfunction parseDatePreservingCalendarDay(dateStr: string): Date {\n const match = dateStr.match(/^(\\d{4})-(\\d{2})-(\\d{2})$/);\n if (match) {\n const [, year, month, day] = match;\n return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));\n }\n return new Date(dateStr);\n}\n\n/**\n * Parse numeric value from CSV string\n */\nfunction parseNumber(\n value: string | undefined,\n defaultValue?: number\n): number {\n if (!value || value.trim() === \"\" || value.toLowerCase() === \"nan\") {\n if (defaultValue !== undefined) return defaultValue;\n return 0;\n }\n const cleaned = value.replace(/[$,%]/g, \"\").trim();\n const parsed = parseFloat(cleaned);\n return isNaN(parsed) ? defaultValue ?? 0 : parsed;\n}\n\n/**\n * Parse CSV content into array of record objects\n */\nfunction parseCSV(content: string): Record<string, string>[] {\n const lines = content.trim().split(\"\\n\");\n if (lines.length < 2) return [];\n\n const headers = parseCSVLine(lines[0]);\n const records: Record<string, string>[] = [];\n\n for (let i = 1; i < lines.length; i++) {\n const values = parseCSVLine(lines[i]);\n if (values.length === 0) continue;\n\n const record: Record<string, string> = {};\n headers.forEach((header, idx) => {\n record[header] = values[idx] || \"\";\n });\n records.push(record);\n }\n\n return records;\n}\n\n/**\n * Parse a single CSV line handling quoted fields\n */\nfunction parseCSVLine(line: string): string[] {\n const result: string[] = [];\n let current = \"\";\n let inQuotes = false;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i];\n\n if (char === '\"') {\n if (inQuotes && line[i + 1] === '\"') {\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n } else if (char === \",\" && !inQuotes) {\n result.push(current.trim());\n current = \"\";\n } else {\n current += char;\n }\n }\n\n result.push(current.trim());\n return result;\n}\n\n/**\n * CSV type detection result\n */\ntype CsvType = \"tradelog\" | \"dailylog\" | \"reportinglog\" | null;\n\n/**\n * Read just the header line from a CSV file (for detection)\n */\nasync function readCsvHeaders(filePath: string): Promise<string[]> {\n const content = await fs.readFile(filePath, \"utf-8\");\n const firstLine = content.split(\"\\n\")[0] || \"\";\n return parseCSVLine(firstLine).map((h) => h.toLowerCase().trim());\n}\n\n/**\n * Detect CSV type by examining column headers.\n * Returns the detected type or null if unrecognized.\n */\nasync function detectCsvType(filePath: string): Promise<CsvType> {\n try {\n const headers = await readCsvHeaders(filePath);\n\n // Trade log detection:\n // Required: \"P/L\" or \"P&L\" or \"Profit/Loss\"\n // Plus at least 2 of: \"Date Opened\", \"Date Closed\", \"Symbol\", \"Strategy\", \"Contracts\", \"Premium\"\n const plColumnAliases = [\"p/l\", \"p&l\", \"profit/loss\", \"pl\"];\n const tradeOptionalColumns = [\n \"date opened\",\n \"date closed\",\n \"symbol\",\n \"strategy\",\n \"contracts\",\n \"no. of contracts\",\n \"premium\",\n \"legs\",\n ];\n\n const hasPl = plColumnAliases.some((alias) => headers.includes(alias));\n const matchedTradeColumns = tradeOptionalColumns.filter((col) =>\n headers.some((h) => h.includes(col) || col.includes(h))\n );\n\n if (hasPl && matchedTradeColumns.length >= 2) {\n return \"tradelog\";\n }\n\n // Daily log detection:\n // Required: \"Date\", and (\"Portfolio Value\" or \"Value\" or \"Equity\" or \"Net Liquidity\")\n const hasDate = headers.some(\n (h) => h === \"date\" || h.includes(\"date\") && !h.includes(\"opened\") && !h.includes(\"closed\")\n );\n const valueColumnAliases = [\n \"portfolio value\",\n \"value\",\n \"equity\",\n \"net liquidity\",\n \"netliquidity\",\n ];\n const hasValue = valueColumnAliases.some((alias) =>\n headers.some((h) => h.includes(alias) || alias.includes(h))\n );\n\n if (hasDate && hasValue && !hasPl) {\n // Daily log has date + value but typically no P/L column (or if it has P/L, it's daily P/L)\n // To distinguish, check if it lacks trade-specific columns\n if (matchedTradeColumns.length < 2) {\n return \"dailylog\";\n }\n }\n\n // Reporting log detection:\n // Has \"Actual P/L\" or columns from REPORTING_TRADE_COLUMN_ALIASES\n // Or has \"Trade ID\" + \"Reported\" style columns\n const reportingAliases = Object.keys(REPORTING_TRADE_COLUMN_ALIASES).map(\n (k) => k.toLowerCase()\n );\n const hasReportingColumns = reportingAliases.some((alias) =>\n headers.includes(alias)\n );\n const hasActualPl = headers.some(\n (h) => h.includes(\"actual\") && h.includes(\"p\")\n );\n const hasReportedStyle =\n headers.includes(\"trade id\") ||\n headers.some((h) => h.includes(\"reported\"));\n\n if (hasActualPl || hasReportingColumns || hasReportedStyle) {\n // Double-check it's not a regular tradelog\n if (!hasPl || hasActualPl) {\n return \"reportinglog\";\n }\n }\n\n // If we have P/L and trade columns, fallback to tradelog\n if (hasPl && matchedTradeColumns.length >= 1) {\n return \"tradelog\";\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Discover CSV files in a folder and detect their types.\n * Returns mapping of detected CSV types to filenames.\n */\nasync function discoverCsvFiles(\n folderPath: string\n): Promise<{ mappings: CsvMappings; unrecognized: string[] }> {\n const mappings: CsvMappings = {};\n const unrecognized: string[] = [];\n\n try {\n const entries = await fs.readdir(folderPath, { withFileTypes: true });\n const csvFiles = entries\n .filter((e) => e.isFile() && e.name.toLowerCase().endsWith(\".csv\"))\n .map((e) => e.name);\n\n // First, check for exact standard names\n if (csvFiles.includes(\"tradelog.csv\")) {\n mappings.tradelog = \"tradelog.csv\";\n }\n if (csvFiles.includes(\"dailylog.csv\")) {\n mappings.dailylog = \"dailylog.csv\";\n }\n if (csvFiles.includes(\"reportinglog.csv\")) {\n mappings.reportinglog = \"reportinglog.csv\";\n }\n\n // For any remaining CSVs, detect by content\n for (const csvFile of csvFiles) {\n // Skip if already mapped via exact name\n if (\n csvFile === \"tradelog.csv\" ||\n csvFile === \"dailylog.csv\" ||\n csvFile === \"reportinglog.csv\"\n ) {\n continue;\n }\n\n const csvPath = path.join(folderPath, csvFile);\n const detectedType = await detectCsvType(csvPath);\n\n if (detectedType) {\n // Only assign if we haven't found this type yet\n if (detectedType === \"tradelog\" && !mappings.tradelog) {\n mappings.tradelog = csvFile;\n } else if (detectedType === \"dailylog\" && !mappings.dailylog) {\n mappings.dailylog = csvFile;\n } else if (detectedType === \"reportinglog\" && !mappings.reportinglog) {\n mappings.reportinglog = csvFile;\n } else {\n // Type already found, this is an extra CSV\n unrecognized.push(csvFile);\n }\n } else {\n unrecognized.push(csvFile);\n }\n }\n } catch {\n // Folder read error - return empty\n }\n\n return { mappings, unrecognized };\n}\n\n/**\n * Get modification times for CSV files in a block folder.\n * Used for cache invalidation - if mtimes change, cached stats are stale.\n */\nexport async function getCsvFileMtimes(\n blockPath: string,\n mappings: CsvMappings\n): Promise<BlockMetadata[\"csvFileMtimes\"]> {\n const mtimes: BlockMetadata[\"csvFileMtimes\"] = {};\n\n for (const [type, filename] of Object.entries(mappings)) {\n if (!filename) continue;\n try {\n const filePath = path.join(blockPath, filename);\n const stat = await fs.stat(filePath);\n mtimes[type as keyof CsvMappings] = stat.mtimeMs;\n } catch {\n // File doesn't exist or can't be read\n }\n }\n\n return Object.keys(mtimes).length > 0 ? mtimes : undefined;\n}\n\n/**\n * Check if cached metadata is still valid by comparing file mtimes.\n * Returns true if cache is valid (files unchanged), false if stale.\n */\nasync function isCacheValid(\n blockPath: string,\n metadata: BlockMetadata\n): Promise<boolean> {\n // No cached mtimes means old metadata format - invalidate\n if (!metadata.csvFileMtimes) {\n return false;\n }\n\n // Determine which files to check\n const mappings: CsvMappings = metadata.csvMappings || { tradelog: \"tradelog.csv\" };\n\n // Check tradelog mtime (required)\n const tradelogFilename = mappings.tradelog || \"tradelog.csv\";\n try {\n const tradelogPath = path.join(blockPath, tradelogFilename);\n const stat = await fs.stat(tradelogPath);\n if (stat.mtimeMs !== metadata.csvFileMtimes.tradelog) {\n return false;\n }\n } catch {\n // File missing - cache invalid\n return false;\n }\n\n // Check dailylog mtime if it was cached\n if (metadata.csvFileMtimes.dailylog) {\n const dailylogFilename = mappings.dailylog || \"dailylog.csv\";\n try {\n const dailylogPath = path.join(blockPath, dailylogFilename);\n const stat = await fs.stat(dailylogPath);\n if (stat.mtimeMs !== metadata.csvFileMtimes.dailylog) {\n return false;\n }\n } catch {\n // Dailylog was cached but now missing - cache invalid\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Log warning when folder has CSVs but none match expected patterns\n */\nfunction logCsvDiscoveryWarning(\n folderName: string,\n csvFiles: string[]\n): void {\n console.error(`Warning: Folder '${folderName}' has CSV files but none match expected trade log format.`);\n console.error(` Found: ${csvFiles.join(\", \")}`);\n console.error(` Expected columns: P/L, Date Opened, Date Closed, Symbol, Strategy`);\n}\n\n/**\n * Convert raw CSV record to Trade object\n */\nfunction convertToTrade(raw: Record<string, string>): Trade | null {\n try {\n const dateOpened = parseDatePreservingCalendarDay(raw[\"Date Opened\"]);\n if (isNaN(dateOpened.getTime())) return null;\n\n const dateClosed = raw[\"Date Closed\"]\n ? parseDatePreservingCalendarDay(raw[\"Date Closed\"])\n : undefined;\n\n const strategy = (raw[\"Strategy\"] || \"\").trim() || \"Unknown\";\n\n const rawPremium = (raw[\"Premium\"] || \"\").replace(/[$,]/g, \"\").trim();\n const premiumPrecision: Trade[\"premiumPrecision\"] =\n rawPremium && !rawPremium.includes(\".\") ? \"cents\" : \"dollars\";\n\n return {\n dateOpened,\n timeOpened: raw[\"Time Opened\"] || \"00:00:00\",\n openingPrice: parseNumber(raw[\"Opening Price\"]),\n legs: raw[\"Legs\"] || \"\",\n premium: parseNumber(raw[\"Premium\"]),\n premiumPrecision,\n closingPrice: raw[\"Closing Price\"]\n ? parseNumber(raw[\"Closing Price\"])\n : undefined,\n dateClosed,\n timeClosed: raw[\"Time Closed\"] || undefined,\n avgClosingCost: raw[\"Avg. Closing Cost\"]\n ? parseNumber(raw[\"Avg. Closing Cost\"])\n : undefined,\n reasonForClose: raw[\"Reason For Close\"] || undefined,\n pl: parseNumber(raw[\"P/L\"]),\n numContracts: Math.round(parseNumber(raw[\"No. of Contracts\"], 1)),\n fundsAtClose: parseNumber(raw[\"Funds at Close\"]),\n marginReq: parseNumber(raw[\"Margin Req.\"]),\n strategy,\n openingCommissionsFees: parseNumber(\n raw[\"Opening Commissions + Fees\"] || raw[\"Opening comms & fees\"],\n 0\n ),\n closingCommissionsFees: parseNumber(\n raw[\"Closing Commissions + Fees\"] || raw[\"Closing comms & fees\"],\n 0\n ),\n openingShortLongRatio: parseNumber(raw[\"Opening Short/Long Ratio\"], 0),\n closingShortLongRatio: raw[\"Closing Short/Long Ratio\"]\n ? parseNumber(raw[\"Closing Short/Long Ratio\"])\n : undefined,\n openingVix: raw[\"Opening VIX\"]\n ? parseNumber(raw[\"Opening VIX\"])\n : undefined,\n closingVix: raw[\"Closing VIX\"]\n ? parseNumber(raw[\"Closing VIX\"])\n : undefined,\n gap: raw[\"Gap\"] ? parseNumber(raw[\"Gap\"]) : undefined,\n movement: raw[\"Movement\"] ? parseNumber(raw[\"Movement\"]) : undefined,\n maxProfit: raw[\"Max Profit\"]\n ? parseNumber(raw[\"Max Profit\"])\n : undefined,\n maxLoss: raw[\"Max Loss\"] ? parseNumber(raw[\"Max Loss\"]) : undefined,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Convert raw CSV record to DailyLogEntry object\n */\nfunction convertToDailyLogEntry(\n raw: Record<string, string>,\n blockId?: string\n): DailyLogEntry | null {\n try {\n const date = parseDatePreservingCalendarDay(raw[\"Date\"]);\n if (isNaN(date.getTime())) return null;\n\n return {\n date,\n netLiquidity: parseNumber(raw[\"Net Liquidity\"]),\n currentFunds: parseNumber(raw[\"Current Funds\"]),\n withdrawn: parseNumber(raw[\"Withdrawn\"], 0),\n tradingFunds: parseNumber(raw[\"Trading Funds\"]),\n dailyPl: parseNumber(raw[\"P/L\"]),\n dailyPlPct: parseNumber(raw[\"P/L %\"]),\n drawdownPct: parseNumber(raw[\"Drawdown %\"]),\n blockId,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Load trades from tradelog CSV file\n * @param blockPath - Path to the block directory\n * @param filename - CSV filename (default: \"tradelog.csv\")\n */\nasync function loadTrades(\n blockPath: string,\n filename: string = \"tradelog.csv\"\n): Promise<Trade[]> {\n const tradelogPath = path.join(blockPath, filename);\n const content = await fs.readFile(tradelogPath, \"utf-8\");\n const records = parseCSV(content);\n\n const trades: Trade[] = [];\n for (const record of records) {\n const trade = convertToTrade(record);\n if (trade) {\n trades.push(trade);\n }\n }\n\n // Sort by date and time\n trades.sort((a, b) => {\n const dateCompare =\n new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime();\n if (dateCompare !== 0) return dateCompare;\n return a.timeOpened.localeCompare(b.timeOpened);\n });\n\n return trades;\n}\n\n/**\n * Load daily logs from dailylog CSV file (optional)\n * @param blockPath - Path to the block directory\n * @param blockId - Block identifier\n * @param filename - CSV filename (default: \"dailylog.csv\")\n */\nasync function loadDailyLogs(\n blockPath: string,\n blockId: string,\n filename: string = \"dailylog.csv\"\n): Promise<DailyLogEntry[] | undefined> {\n const dailylogPath = path.join(blockPath, filename);\n\n try {\n await fs.access(dailylogPath);\n const content = await fs.readFile(dailylogPath, \"utf-8\");\n const records = parseCSV(content);\n\n const entries: DailyLogEntry[] = [];\n for (const record of records) {\n const entry = convertToDailyLogEntry(record, blockId);\n if (entry) {\n entries.push(entry);\n }\n }\n\n // Sort by date\n entries.sort(\n (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()\n );\n\n return entries.length > 0 ? entries : undefined;\n } catch {\n // Daily log doesn't exist - that's fine\n return undefined;\n }\n}\n\n/**\n * Load block metadata from .block.json\n */\nexport async function loadMetadata(\n blockPath: string\n): Promise<BlockMetadata | undefined> {\n const metadataPath = path.join(blockPath, \".block.json\");\n\n try {\n const content = await fs.readFile(metadataPath, \"utf-8\");\n return JSON.parse(content) as BlockMetadata;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Save block metadata to .block.json\n */\nexport async function saveMetadata(\n blockPath: string,\n metadata: BlockMetadata\n): Promise<void> {\n const metadataPath = path.join(blockPath, \".block.json\");\n await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), \"utf-8\");\n}\n\n/**\n * Options for building block metadata\n */\nexport interface BuildMetadataOptions {\n blockId: string;\n blockPath: string;\n trades: Trade[];\n dailyLogs?: DailyLogEntry[];\n existingMetadata?: BlockMetadata;\n csvMappings: CsvMappings;\n cachedStats?: BlockMetadata[\"cachedStats\"];\n}\n\n/**\n * Build a complete BlockMetadata object with file mtimes for cache invalidation.\n * Centralizes metadata construction to ensure consistency across all code paths.\n */\nexport async function buildBlockMetadata(\n options: BuildMetadataOptions\n): Promise<BlockMetadata> {\n const {\n blockId,\n blockPath,\n trades,\n dailyLogs,\n existingMetadata,\n csvMappings,\n cachedStats,\n } = options;\n\n // Extract info from trades\n const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();\n const dates = trades.map((t) => new Date(t.dateOpened).getTime());\n\n // Get file mtimes for cache invalidation\n const csvFileMtimes = await getCsvFileMtimes(blockPath, csvMappings);\n\n return {\n blockId,\n name: existingMetadata?.name || blockId,\n createdAt: existingMetadata?.createdAt || new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n tradeCount: trades.length,\n dailyLogCount: dailyLogs?.length ?? 0,\n dateRange: {\n start:\n dates.length > 0 ? new Date(Math.min(...dates)).toISOString() : null,\n end: dates.length > 0 ? new Date(Math.max(...dates)).toISOString() : null,\n },\n strategies,\n csvMappings,\n csvFileMtimes,\n cachedStats,\n };\n}\n\n/**\n * Load a complete block (trades + optional daily logs)\n * Uses CSV mappings from metadata if available for flexible filename support.\n */\nexport async function loadBlock(\n baseDir: string,\n blockId: string\n): Promise<LoadedBlock> {\n const blockPath = path.join(baseDir, blockId);\n\n // Load metadata first to get CSV mappings\n const metadata = await loadMetadata(blockPath);\n const mappings = metadata?.csvMappings;\n\n // Determine tradelog filename (from mappings or default)\n const tradelogFilename = mappings?.tradelog || \"tradelog.csv\";\n const tradelogPath = path.join(blockPath, tradelogFilename);\n\n // Verify tradelog exists\n try {\n await fs.access(tradelogPath);\n } catch {\n // If using default name failed, try discovery\n if (!mappings?.tradelog) {\n const discovered = await discoverCsvFiles(blockPath);\n if (discovered.mappings.tradelog) {\n // Found a tradelog via discovery - use it\n const trades = await loadTrades(blockPath, discovered.mappings.tradelog);\n const dailyLogs = discovered.mappings.dailylog\n ? await loadDailyLogs(blockPath, blockId, discovered.mappings.dailylog)\n : undefined;\n return { blockId, trades, dailyLogs, metadata };\n }\n }\n throw new Error(`Block not found or missing tradelog: ${blockId}`);\n }\n\n // Determine dailylog filename\n const dailylogFilename = mappings?.dailylog || \"dailylog.csv\";\n\n const trades = await loadTrades(blockPath, tradelogFilename);\n const dailyLogs = await loadDailyLogs(blockPath, blockId, dailylogFilename);\n\n return {\n blockId,\n trades,\n dailyLogs,\n metadata,\n };\n}\n\n/**\n * List all valid blocks in the base directory.\n * Uses flexible CSV discovery to find blocks with non-standard CSV filenames.\n */\nexport async function listBlocks(baseDir: string): Promise<BlockInfo[]> {\n const blocks: BlockInfo[] = [];\n\n try {\n const entries = await fs.readdir(baseDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n if (entry.name.startsWith(\".\")) continue; // Skip hidden folders\n\n const blockPath = path.join(baseDir, entry.name);\n\n // Try to load existing metadata first\n const metadata = await loadMetadata(blockPath);\n\n // Determine CSV filenames - prefer metadata mappings, then exact names, then discovery\n let tradelogFilename: string | undefined;\n let dailylogFilename: string | undefined;\n let needsMetadataUpdate = false;\n\n if (metadata?.csvMappings?.tradelog) {\n // Use cached mappings from metadata\n tradelogFilename = metadata.csvMappings.tradelog;\n dailylogFilename = metadata.csvMappings.dailylog;\n } else {\n // Check for exact standard names first\n const standardTradelogPath = path.join(blockPath, \"tradelog.csv\");\n const standardDailylogPath = path.join(blockPath, \"dailylog.csv\");\n\n let hasStandardTradelog = false;\n\n try {\n await fs.access(standardTradelogPath);\n hasStandardTradelog = true;\n tradelogFilename = \"tradelog.csv\";\n } catch {\n // No standard tradelog\n }\n\n try {\n await fs.access(standardDailylogPath);\n dailylogFilename = \"dailylog.csv\";\n } catch {\n // No standard dailylog\n }\n\n // If no standard tradelog, try discovery\n if (!hasStandardTradelog) {\n const discovered = await discoverCsvFiles(blockPath);\n\n if (discovered.mappings.tradelog) {\n tradelogFilename = discovered.mappings.tradelog;\n dailylogFilename = discovered.mappings.dailylog || dailylogFilename;\n needsMetadataUpdate = true;\n\n // Log that we discovered non-standard files\n if (tradelogFilename !== \"tradelog.csv\") {\n console.error(\n `Discovered trade log in '${entry.name}': ${tradelogFilename}`\n );\n }\n } else if (discovered.unrecognized.length > 0) {\n // Has CSV files but none recognized as tradelog\n logCsvDiscoveryWarning(entry.name, discovered.unrecognized);\n continue; // Skip this folder\n } else {\n // No CSV files at all\n continue;\n }\n }\n }\n\n // If still no tradelog, skip this folder\n if (!tradelogFilename) {\n continue;\n }\n\n const hasDailyLog = !!dailylogFilename;\n\n // Check if cached stats are valid (files haven't changed)\n const cacheValid =\n metadata?.cachedStats &&\n !needsMetadataUpdate &&\n (await isCacheValid(blockPath, metadata));\n\n // Use cached stats from metadata if available and valid\n if (cacheValid && metadata?.cachedStats) {\n blocks.push({\n blockId: entry.name,\n name: metadata.name || entry.name,\n tradeCount: metadata.tradeCount,\n hasDailyLog,\n dateRange: {\n start: metadata.dateRange.start\n ? new Date(metadata.dateRange.start)\n : null,\n end: metadata.dateRange.end\n ? new Date(metadata.dateRange.end)\n : null,\n },\n strategies: metadata.strategies,\n totalPl: metadata.cachedStats.totalPl,\n netPl: metadata.cachedStats.netPl,\n });\n } else {\n // Load trades to get basic info\n try {\n const trades = await loadTrades(blockPath, tradelogFilename);\n const totalPl = trades.reduce((sum, t) => sum + t.pl, 0);\n const totalCommissions = trades.reduce(\n (sum, t) =>\n sum + t.openingCommissionsFees + t.closingCommissionsFees,\n 0\n );\n\n // Build CSV mappings\n const csvMappings: CsvMappings = { tradelog: tradelogFilename };\n if (dailylogFilename) {\n csvMappings.dailylog = dailylogFilename;\n }\n\n // Build and save metadata using the centralized helper\n const updatedMetadata = await buildBlockMetadata({\n blockId: entry.name,\n blockPath,\n trades,\n existingMetadata: metadata,\n csvMappings,\n });\n\n blocks.push({\n blockId: entry.name,\n name: updatedMetadata.name,\n tradeCount: trades.length,\n hasDailyLog,\n dateRange: {\n start: updatedMetadata.dateRange.start\n ? new Date(updatedMetadata.dateRange.start)\n : null,\n end: updatedMetadata.dateRange.end\n ? new Date(updatedMetadata.dateRange.end)\n : null,\n },\n strategies: updatedMetadata.strategies,\n totalPl,\n netPl: totalPl - totalCommissions,\n });\n\n // Save updated metadata (cache the mappings and mtimes)\n await saveMetadata(blockPath, updatedMetadata);\n } catch (error) {\n console.error(`Error loading block ${entry.name}:`, error);\n }\n }\n }\n\n // Sort by name\n blocks.sort((a, b) => a.name.localeCompare(b.name));\n\n return blocks;\n } catch (error) {\n throw new Error(`Failed to list blocks: ${(error as Error).message}`);\n }\n}\n\n/**\n * Normalize header names using column aliases\n */\nfunction normalizeRecordHeaders(\n raw: Record<string, string>\n): Record<string, string> {\n const normalized: Record<string, string> = { ...raw };\n Object.entries(REPORTING_TRADE_COLUMN_ALIASES).forEach(\n ([alias, canonical]) => {\n if (normalized[alias] !== undefined) {\n normalized[canonical] = normalized[alias];\n delete normalized[alias];\n }\n }\n );\n return normalized;\n}\n\n/**\n * Convert raw CSV record to ReportingTrade object\n */\nfunction convertToReportingTrade(\n raw: Record<string, string>\n): ReportingTrade | null {\n try {\n const normalized = normalizeRecordHeaders(raw);\n\n const dateOpened = parseDatePreservingCalendarDay(normalized[\"Date Opened\"]);\n if (isNaN(dateOpened.getTime())) return null;\n\n const dateClosed = normalized[\"Date Closed\"]\n ? parseDatePreservingCalendarDay(normalized[\"Date Closed\"])\n : undefined;\n\n const strategy = (normalized[\"Strategy\"] || \"\").trim() || \"Unknown\";\n\n return {\n strategy,\n dateOpened,\n timeOpened: normalized[\"Time Opened\"] || undefined,\n openingPrice: parseNumber(normalized[\"Opening Price\"]),\n legs: normalized[\"Legs\"] || \"\",\n initialPremium: parseNumber(normalized[\"Initial Premium\"]),\n numContracts: parseNumber(normalized[\"No. of Contracts\"], 1),\n pl: parseNumber(normalized[\"P/L\"]),\n closingPrice: normalized[\"Closing Price\"]\n ? parseNumber(normalized[\"Closing Price\"])\n : undefined,\n dateClosed,\n timeClosed: normalized[\"Time Closed\"] || undefined,\n avgClosingCost: normalized[\"Avg. Closing Cost\"]\n ? parseNumber(normalized[\"Avg. Closing Cost\"])\n : undefined,\n reasonForClose: normalized[\"Reason For Close\"] || undefined,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Load reporting log (actual trades) from reportinglog CSV\n * Uses CSV mappings from metadata if available.\n * @throws Error if reportinglog CSV does not exist\n */\nexport async function loadReportingLog(\n baseDir: string,\n blockId: string\n): Promise<ReportingTrade[]> {\n const blockPath = path.join(baseDir, blockId);\n\n // Load metadata to check for CSV mappings\n const metadata = await loadMetadata(blockPath);\n const filename = metadata?.csvMappings?.reportinglog || \"reportinglog.csv\";\n const reportingLogPath = path.join(blockPath, filename);\n\n // Check if file exists - throw if not\n try {\n await fs.access(reportingLogPath);\n } catch {\n // Try discovery as fallback\n if (filename === \"reportinglog.csv\") {\n const discovered = await discoverCsvFiles(blockPath);\n if (discovered.mappings.reportinglog) {\n const altPath = path.join(blockPath, discovered.mappings.reportinglog);\n const content = await fs.readFile(altPath, \"utf-8\");\n const records = parseCSV(content);\n const trades: ReportingTrade[] = [];\n for (const record of records) {\n const trade = convertToReportingTrade(record);\n if (trade) trades.push(trade);\n }\n trades.sort(\n (a, b) =>\n new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()\n );\n return trades;\n }\n }\n throw new Error(`reportinglog.csv not found in block: ${blockId}`);\n }\n\n const content = await fs.readFile(reportingLogPath, \"utf-8\");\n const records = parseCSV(content);\n\n const trades: ReportingTrade[] = [];\n for (const record of records) {\n const trade = convertToReportingTrade(record);\n if (trade) {\n trades.push(trade);\n }\n }\n\n // Sort by date\n trades.sort(\n (a, b) =>\n new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime()\n );\n\n return trades;\n}\n\n/**\n * Import CSV result\n */\nexport interface ImportCsvResult {\n blockId: string;\n name: string;\n recordCount: number;\n dateRange: {\n start: string | null;\n end: string | null;\n };\n strategies: string[];\n blockPath: string;\n}\n\n/**\n * Import CSV options\n */\nexport interface ImportCsvOptions {\n /** Absolute path to the CSV file */\n csvPath: string;\n /** Name for the block */\n blockName: string;\n /** Type of CSV data */\n csvType?: \"tradelog\" | \"dailylog\" | \"reportinglog\";\n}\n\n/**\n * Convert a string to kebab-case for blockId\n */\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\") // camelCase to kebab-case\n .replace(/[\\s_]+/g, \"-\") // spaces and underscores to hyphens\n .replace(/[^a-zA-Z0-9-]/g, \"\") // remove special characters\n .toLowerCase()\n .replace(/-+/g, \"-\") // collapse multiple hyphens\n .replace(/^-|-$/g, \"\"); // trim leading/trailing hyphens\n}\n\n/**\n * Validate CSV has required columns for the specified type\n */\nfunction validateCsvColumns(\n records: Record<string, string>[],\n csvType: \"tradelog\" | \"dailylog\" | \"reportinglog\"\n): { valid: boolean; error?: string } {\n if (records.length === 0) {\n return { valid: false, error: \"CSV file is empty or has no data rows\" };\n }\n\n const headers = Object.keys(records[0]);\n\n switch (csvType) {\n case \"tradelog\": {\n // Required columns for trade log\n const required = [\"Date Opened\", \"P/L\"];\n const missing = required.filter((col) => !headers.includes(col));\n if (missing.length > 0) {\n return {\n valid: false,\n error: `Missing required columns for tradelog: ${missing.join(\", \")}. Expected columns include: Date Opened, P/L, Strategy, Legs, etc.`,\n };\n }\n break;\n }\n case \"dailylog\": {\n // Required columns for daily log\n const required = [\"Date\", \"Net Liquidity\"];\n const missing = required.filter((col) => !headers.includes(col));\n if (missing.length > 0) {\n return {\n valid: false,\n error: `Missing required columns for dailylog: ${missing.join(\", \")}. Expected columns include: Date, Net Liquidity, P/L, etc.`,\n };\n }\n break;\n }\n case \"reportinglog\": {\n // Required columns for reporting log (with aliases)\n const dateOpenedAliases = [\"Date Opened\", \"date_opened\"];\n const plAliases = [\"P/L\", \"pl\"];\n const hasDateOpened = dateOpenedAliases.some((col) =>\n headers.includes(col)\n );\n const hasPl = plAliases.some((col) => headers.includes(col));\n const missing: string[] = [];\n if (!hasDateOpened) missing.push(\"Date Opened\");\n if (!hasPl) missing.push(\"P/L\");\n if (missing.length > 0) {\n return {\n valid: false,\n error: `Missing required columns for reportinglog: ${missing.join(\", \")}. Expected columns include: Date Opened (or date_opened), P/L (or pl), Strategy, etc.`,\n };\n }\n break;\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Import a CSV file into the blocks directory\n *\n * Requires local filesystem access. The MCP server must be running locally\n * (via npx tradeblocks-mcp or mcpb desktop extension) to access files.\n *\n * @param baseDir - Base directory for blocks\n * @param options - Import options: csvPath, blockName, csvType\n * @returns Import result with block info\n */\nexport async function importCsv(\n baseDir: string,\n options: ImportCsvOptions\n): Promise<ImportCsvResult> {\n const { csvPath, blockName, csvType = \"tradelog\" } = options;\n\n // Validate source file exists\n try {\n await fs.access(csvPath);\n } catch {\n throw new Error(`CSV file not found: ${csvPath}`);\n }\n\n // Read and parse the CSV\n const content = await fs.readFile(csvPath, \"utf-8\");\n const records = parseCSV(content);\n\n // Validate CSV has required columns\n const validation = validateCsvColumns(records, csvType);\n if (!validation.valid) {\n throw new Error(validation.error);\n }\n\n // Convert blockName to kebab-case for blockId\n const name = blockName;\n const blockId = toKebabCase(name);\n\n if (!blockId) {\n throw new Error(\n \"Could not derive a valid block ID from the filename or provided name\"\n );\n }\n\n // Check if block already exists\n const blockPath = path.join(baseDir, blockId);\n try {\n await fs.access(blockPath);\n throw new Error(\n `Block \"${blockId}\" already exists. Use a different blockName or delete the existing block first.`\n );\n } catch (error) {\n // Directory doesn't exist - good, we can create it\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw error; // Re-throw if it's not a \"not found\" error\n }\n }\n\n // Create block directory\n await fs.mkdir(blockPath, { recursive: true });\n\n // Determine target filename\n const targetFilename =\n csvType === \"tradelog\"\n ? \"tradelog.csv\"\n : csvType === \"dailylog\"\n ? \"dailylog.csv\"\n : \"reportinglog.csv\";\n\n // Copy CSV to block directory\n const targetPath = path.join(blockPath, targetFilename);\n await fs.copyFile(csvPath, targetPath);\n\n // Extract metadata based on CSV type\n let dateRange: { start: string | null; end: string | null } = {\n start: null,\n end: null,\n };\n let strategies: string[] = [];\n\n if (csvType === \"tradelog\") {\n // Parse trades to extract metadata\n const trades: Trade[] = [];\n for (const record of records) {\n const trade = convertToTrade(record);\n if (trade) trades.push(trade);\n }\n\n if (trades.length > 0) {\n // Build and save metadata using the centralized helper\n const csvMappings: CsvMappings = { tradelog: targetFilename };\n const metadata = await buildBlockMetadata({\n blockId,\n blockPath,\n trades,\n csvMappings,\n });\n // Override name since buildBlockMetadata defaults to blockId\n metadata.name = name;\n await saveMetadata(blockPath, metadata);\n\n // Extract for return value\n dateRange = metadata.dateRange;\n strategies = metadata.strategies;\n }\n } else if (csvType === \"dailylog\") {\n // Parse daily logs to extract date range\n const entries: DailyLogEntry[] = [];\n for (const record of records) {\n const entry = convertToDailyLogEntry(record, blockId);\n if (entry) entries.push(entry);\n }\n\n if (entries.length > 0) {\n const dates = entries.map((e) => new Date(e.date).getTime());\n dateRange = {\n start: new Date(Math.min(...dates)).toISOString(),\n end: new Date(Math.max(...dates)).toISOString(),\n };\n }\n // Note: dailylog-only blocks won't have a .block.json created here\n // They would typically be added to an existing tradelog block\n } else if (csvType === \"reportinglog\") {\n // Parse reporting trades to extract metadata\n const trades: ReportingTrade[] = [];\n for (const record of records) {\n const trade = convertToReportingTrade(record);\n if (trade) trades.push(trade);\n }\n\n if (trades.length > 0) {\n const dates = trades.map((t) => new Date(t.dateOpened).getTime());\n dateRange = {\n start: new Date(Math.min(...dates)).toISOString(),\n end: new Date(Math.max(...dates)).toISOString(),\n };\n strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();\n }\n }\n\n return {\n blockId,\n name,\n recordCount: records.length,\n dateRange,\n strategies,\n blockPath,\n };\n}\n","/**\n * Reporting trade model represents backtested strategy executions coming from the\n * strategy-trade-log.csv export. These records are used to align theoretical\n * performance with the real trade log for a block.\n */\nexport interface ReportingTrade {\n strategy: string\n dateOpened: Date\n timeOpened?: string\n openingPrice: number\n legs: string\n initialPremium: number\n numContracts: number\n pl: number\n closingPrice?: number\n dateClosed?: Date\n timeClosed?: string\n avgClosingCost?: number\n reasonForClose?: string\n}\n\n/**\n * Raw reporting trade data direct from the CSV prior to conversion.\n */\nexport interface RawReportingTradeData {\n \"Strategy\": string\n \"Date Opened\": string\n \"Time Opened\"?: string\n \"Opening Price\": string\n \"Legs\": string\n \"Initial Premium\": string\n \"No. of Contracts\": string\n \"P/L\": string\n \"Closing Price\"?: string\n \"Date Closed\"?: string\n \"Time Closed\"?: string\n \"Avg. Closing Cost\"?: string\n \"Reason For Close\"?: string\n}\n\n/**\n * Required columns that must be present for a reporting log import to be valid.\n */\nexport const REQUIRED_REPORTING_TRADE_COLUMNS = [\n \"Strategy\",\n \"Date Opened\",\n \"Opening Price\",\n \"Legs\",\n \"Initial Premium\",\n \"No. of Contracts\",\n \"P/L\",\n] as const\n\n/**\n * Column aliases to support slight variations in exports.\n */\nexport const REPORTING_TRADE_COLUMN_ALIASES: Record<string, string> = {\n \"Initial Premium ($)\": \"Initial Premium\",\n \"Initial Credit\": \"Initial Premium\",\n \"Contracts\": \"No. of Contracts\",\n \"Contracts Traded\": \"No. of Contracts\",\n \"PL\": \"P/L\",\n}\n"],"mappings":"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","names":["stat","trades","dailyLogs","content","records"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tradeblocks-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server for options trade analysis - works with Claude Desktop/Cowork",
5
5
  "author": "David Romeo <davidmromeo@gmail.com>",
6
6
  "type": "module",