tradeblocks-mcp 3.10.2 → 4.0.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.
Files changed (165) hide show
  1. package/README.md +208 -23
  2. package/dist/{chunk-TKUG3FOR.js → chunk-BH6SQOWF.js} +23 -5
  3. package/dist/chunk-BH6SQOWF.js.map +1 -0
  4. package/dist/{chunk-3KDAY4J4.js → chunk-D4AWELQU.js} +8 -9
  5. package/{server/chunk-35ZZUXY7.js.map → dist/chunk-D4AWELQU.js.map} +1 -1
  6. package/dist/{chunk-XANRFYQT.js → chunk-HQQNVMYF.js} +6340 -5569
  7. package/dist/chunk-HQQNVMYF.js.map +1 -0
  8. package/dist/{chunk-5SZL2WXB.js → chunk-IW75IC6I.js} +5 -32
  9. package/dist/chunk-IW75IC6I.js.map +1 -0
  10. package/dist/chunk-XF3F3S7D.js +5674 -0
  11. package/dist/chunk-XF3F3S7D.js.map +1 -0
  12. package/dist/chunk-YQMQ3PI4.js +5833 -0
  13. package/dist/chunk-YQMQ3PI4.js.map +1 -0
  14. package/dist/{chunk-Q4ZODPP6.js → chunk-ZSVYX5PP.js} +6 -23
  15. package/dist/chunk-ZSVYX5PP.js.map +1 -0
  16. package/dist/daily-log-processor-T2QMVGBA.js +9 -0
  17. package/dist/daily-log-processor-ZFTGQDOD.js +10 -0
  18. package/dist/{daily-logs-store-3FVXJCAS.js → daily-logs-store-LRZBF2UA.js} +3 -5
  19. package/dist/{daily-logs-store-X4IAX7S5.js → daily-logs-store-UNX5DAT7.js} +3 -5
  20. package/dist/market/provenance/index.d.ts +5 -18
  21. package/dist/market/provenance/index.js +566 -18905
  22. package/dist/market/provenance/index.js.map +1 -1
  23. package/dist/{sync-DVSXIIUZ.js → sync-I2S5TFQS.js} +7 -8
  24. package/dist/test-exports.js +4011 -712
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-6QSOAWJN.js +9 -0
  27. package/dist/trade-processor-VJ3XUDTH.js +10 -0
  28. package/dist/{trades-store-HDB2Q3EC.js → trades-store-O4ETTDGU.js} +3 -5
  29. package/dist/{trades-store-5SLXLMGH.js → trades-store-P6QZQMAD.js} +3 -5
  30. package/package.json +6 -6
  31. package/server/{chunk-4AGMJIIZ.js → chunk-24O66TM5.js} +6 -23
  32. package/server/chunk-24O66TM5.js.map +1 -0
  33. package/server/{chunk-N2RXXORM.js → chunk-2E3K452Q.js} +23 -5
  34. package/server/chunk-2E3K452Q.js.map +1 -0
  35. package/server/chunk-3XKPTACI.js +6035 -0
  36. package/server/chunk-3XKPTACI.js.map +1 -0
  37. package/server/{chunk-35ZZUXY7.js → chunk-76OEJGQX.js} +8 -9
  38. package/{dist/chunk-3KDAY4J4.js.map → server/chunk-76OEJGQX.js.map} +1 -1
  39. package/server/{chunk-I6FEESXT.js → chunk-JI2QLM6W.js} +25413 -24241
  40. package/server/chunk-JI2QLM6W.js.map +1 -0
  41. package/server/chunk-MCT6A2WW.js +593 -0
  42. package/server/chunk-MCT6A2WW.js.map +1 -0
  43. package/server/{chunk-YKGDAXGE.js → chunk-NLYLE5C6.js} +5 -32
  44. package/server/chunk-NLYLE5C6.js.map +1 -0
  45. package/server/cli.d.ts +2 -0
  46. package/server/cli.js +62 -0
  47. package/server/cli.js.map +1 -0
  48. package/server/daily-log-processor-NXSUFX32.js +11 -0
  49. package/server/{daily-logs-store-GK6B3X2V.js → daily-logs-store-EMWH6MR4.js} +3 -5
  50. package/server/index.d.ts +17 -7
  51. package/server/index.js +4579 -1771
  52. package/server/index.js.map +1 -1
  53. package/server/setup-4EJSSDL4.js +9 -0
  54. package/server/{sync-MW333YEK.js → sync-NNHKLGNZ.js} +7 -8
  55. package/server/trade-processor-NYDJ3VMH.js +11 -0
  56. package/server/{trades-store-QH6Z3OYG.js → trades-store-3QYMUVA5.js} +3 -5
  57. package/src/cli.ts +63 -0
  58. package/src/db/connection.ts +5 -6
  59. package/src/db/data-root.ts +1 -1
  60. package/src/index.ts +45 -20
  61. package/src/market/provenance/content-object-store.ts +6 -46
  62. package/src/market/provenance/durable-directory.ts +92 -0
  63. package/src/market/provenance/index.ts +1 -0
  64. package/src/market/provenance/partition-commit-store.ts +26 -62
  65. package/src/market/provenance/xnys-calendar-data.ts +117 -0
  66. package/src/market/provenance/xnys-session-calendar.ts +35 -138
  67. package/src/market/published-rates.ts +52 -0
  68. package/src/market/stores/duckdb-quote-store.ts +11 -3
  69. package/src/market/stores/duckdb-spot-store.ts +2 -2
  70. package/src/market/stores/parquet-quote-store.ts +11 -3
  71. package/src/market/stores/parquet-spot-store.ts +7 -2
  72. package/src/market/stores/quote-store.ts +8 -1
  73. package/src/market/stores/spot-sql.ts +20 -2
  74. package/src/market/stores/spot-store.ts +9 -3
  75. package/src/plugins.ts +6 -0
  76. package/src/prompts.ts +393 -0
  77. package/src/setup.ts +708 -0
  78. package/src/sync/block-sync.ts +2 -1
  79. package/src/test-exports.ts +8 -1
  80. package/src/tools/analysis.ts +16 -56
  81. package/src/tools/blocks/analysis.ts +25 -243
  82. package/src/tools/blocks/comparison.ts +8 -4
  83. package/src/tools/blocks/core.ts +61 -32
  84. package/src/tools/blocks/paired-comparison.ts +7 -20
  85. package/src/tools/blocks/similarity.ts +13 -161
  86. package/src/tools/blocks.ts +1 -1
  87. package/src/tools/edge-decay.ts +1 -0
  88. package/src/tools/exit-analysis.ts +6 -1
  89. package/src/tools/greeks-attribution.ts +11 -14
  90. package/src/tools/imports.ts +58 -42
  91. package/src/tools/middleware/strict-arguments.ts +67 -0
  92. package/src/tools/performance.ts +46 -694
  93. package/src/tools/replay.ts +36 -6
  94. package/src/tools/reports/fields.ts +7 -39
  95. package/src/tools/reports/helpers.ts +16 -40
  96. package/src/tools/reports/predictive.ts +12 -122
  97. package/src/tools/reports.ts +4 -3
  98. package/src/tools/sql.ts +8 -3
  99. package/src/utils/block-loader.ts +254 -122
  100. package/src/utils/csv-discovery.ts +1 -3
  101. package/src/utils/data-availability.ts +2 -3
  102. package/src/utils/greeks-decomposition.ts +90 -53
  103. package/src/utils/market-enricher.ts +93 -115
  104. package/src/utils/option-quote-greeks.ts +10 -6
  105. package/src/utils/providers/massive.ts +2 -3
  106. package/src/utils/providers/thetadata/backfill.ts +12 -20
  107. package/src/utils/quote-parquet-projection.ts +13 -7
  108. package/src/utils/schema-metadata.ts +3 -2
  109. package/src/utils/starting-capital.ts +38 -0
  110. package/src/utils/trade-replay.ts +44 -0
  111. package/dist/chunk-4VWUMBAZ.js +0 -859
  112. package/dist/chunk-4VWUMBAZ.js.map +0 -1
  113. package/dist/chunk-5SZL2WXB.js.map +0 -1
  114. package/dist/chunk-AH7VIHDT.js +0 -329
  115. package/dist/chunk-AH7VIHDT.js.map +0 -1
  116. package/dist/chunk-COSLRCST.js +0 -3374
  117. package/dist/chunk-COSLRCST.js.map +0 -1
  118. package/dist/chunk-DJRGJXYP.js +0 -453
  119. package/dist/chunk-DJRGJXYP.js.map +0 -1
  120. package/dist/chunk-P3IETL3K.js +0 -93
  121. package/dist/chunk-P3IETL3K.js.map +0 -1
  122. package/dist/chunk-PS2XIVFN.js +0 -359
  123. package/dist/chunk-PS2XIVFN.js.map +0 -1
  124. package/dist/chunk-Q4ZODPP6.js.map +0 -1
  125. package/dist/chunk-TEJVVTIM.js +0 -912
  126. package/dist/chunk-TEJVVTIM.js.map +0 -1
  127. package/dist/chunk-TKUG3FOR.js.map +0 -1
  128. package/dist/chunk-TZRDK5VL.js +0 -5626
  129. package/dist/chunk-TZRDK5VL.js.map +0 -1
  130. package/dist/chunk-XANRFYQT.js.map +0 -1
  131. package/dist/daily-log-processor-DMJA7D2X.js +0 -10
  132. package/dist/daily-log-processor-IBZMMS72.js +0 -9
  133. package/dist/market-provider-FWKON2ZG.js +0 -17
  134. package/dist/trade-processor-DQMN7JZF.js +0 -10
  135. package/dist/trade-processor-EWPJXBZZ.js +0 -9
  136. package/server/chunk-4AGMJIIZ.js.map +0 -1
  137. package/server/chunk-GKB4EQFU.js +0 -5317
  138. package/server/chunk-GKB4EQFU.js.map +0 -1
  139. package/server/chunk-I6FEESXT.js.map +0 -1
  140. package/server/chunk-N2RXXORM.js.map +0 -1
  141. package/server/chunk-RY37QPQJ.js +0 -3127
  142. package/server/chunk-RY37QPQJ.js.map +0 -1
  143. package/server/chunk-YKGDAXGE.js.map +0 -1
  144. package/server/chunk-YQZDWXAU.js +0 -938
  145. package/server/chunk-YQZDWXAU.js.map +0 -1
  146. package/server/daily-log-processor-XNMWJNJ6.js +0 -11
  147. package/server/market-provider-I6JMSBIK.js +0 -18
  148. package/server/trade-processor-TO27NFQW.js +0 -11
  149. package/server/trades-store-QH6Z3OYG.js.map +0 -1
  150. package/src/tools/blocks/stress-scenarios.ts +0 -72
  151. /package/dist/{daily-log-processor-DMJA7D2X.js.map → daily-log-processor-T2QMVGBA.js.map} +0 -0
  152. /package/dist/{daily-log-processor-IBZMMS72.js.map → daily-log-processor-ZFTGQDOD.js.map} +0 -0
  153. /package/dist/{daily-logs-store-3FVXJCAS.js.map → daily-logs-store-LRZBF2UA.js.map} +0 -0
  154. /package/dist/{daily-logs-store-X4IAX7S5.js.map → daily-logs-store-UNX5DAT7.js.map} +0 -0
  155. /package/dist/{market-provider-FWKON2ZG.js.map → sync-I2S5TFQS.js.map} +0 -0
  156. /package/dist/{sync-DVSXIIUZ.js.map → trade-processor-6QSOAWJN.js.map} +0 -0
  157. /package/dist/{trade-processor-DQMN7JZF.js.map → trade-processor-VJ3XUDTH.js.map} +0 -0
  158. /package/dist/{trade-processor-EWPJXBZZ.js.map → trades-store-O4ETTDGU.js.map} +0 -0
  159. /package/dist/{trades-store-5SLXLMGH.js.map → trades-store-P6QZQMAD.js.map} +0 -0
  160. /package/{dist/trades-store-HDB2Q3EC.js.map → server/daily-log-processor-NXSUFX32.js.map} +0 -0
  161. /package/server/{daily-log-processor-XNMWJNJ6.js.map → daily-logs-store-EMWH6MR4.js.map} +0 -0
  162. /package/server/{daily-logs-store-GK6B3X2V.js.map → setup-4EJSSDL4.js.map} +0 -0
  163. /package/server/{market-provider-I6JMSBIK.js.map → sync-NNHKLGNZ.js.map} +0 -0
  164. /package/server/{sync-MW333YEK.js.map → trade-processor-NYDJ3VMH.js.map} +0 -0
  165. /package/server/{trade-processor-TO27NFQW.js.map → trades-store-3QYMUVA5.js.map} +0 -0
package/README.md CHANGED
@@ -14,6 +14,102 @@ Model Context Protocol (MCP) server for options trading analysis. Works with Cla
14
14
 
15
15
  ## Installation
16
16
 
17
+ ### Prerequisites (before using npm or npx)
18
+
19
+ Open a terminal (PowerShell on Windows) and run `node --version`, `npm --version`,
20
+ and `npx --version`. TradeBlocks' packaged server requires Node **18 or newer**;
21
+ Node **24 LTS** is recommended (and used for development). Your chosen client
22
+ may require a newer Node version, so Node 24 is the easiest shared choice.
23
+
24
+ If any command is missing, install Node **with npm**, then close and reopen the terminal:
25
+
26
+ | Platform | Install Node/npm |
27
+ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
28
+ | macOS | Use the macOS installer from [nodejs.org](https://nodejs.org/en/download), or `brew install node` if you already use Homebrew. |
29
+ | Windows | Use the Windows installer from [nodejs.org](https://nodejs.org/en/download), or `winget install OpenJS.NodeJS.LTS`. Reopen PowerShell afterward. |
30
+ | Linux | Follow the Linux instructions at [nodejs.org](https://nodejs.org/en/download) for your distribution or version manager; choose Node 24 with npm rather than an older distribution package. |
31
+
32
+ Then repeat the three version checks. Install/open your chosen client separately:
33
+ [Claude Desktop](https://claude.ai/download),
34
+ [Claude Code](https://code.claude.com/docs/en/setup),
35
+ [Codex CLI](https://developers.openai.com/codex/cli/) (`npm install -g @openai/codex`),
36
+ or [Gemini CLI](https://geminicli.com/docs/get-started/installation/)
37
+ (`npm install -g @google/gemini-cli`). Setup does not install any of these.
38
+ Desktop's configuration directory must already exist; on Linux follow your Desktop
39
+ distribution's instructions. Local setup does not need OO, a provider key, or a skills plugin.
40
+
41
+ ### Guided local setup
42
+
43
+ ```bash
44
+ # Interactive: select a client and folder, review changes, explicitly approve
45
+ npx tradeblocks-mcp setup
46
+
47
+ # Agent/non-interactive: preview only (non-zero consent_required result)
48
+ npx tradeblocks-mcp setup --client codex --folder "/path/to/backtests" --json
49
+
50
+ # Apply the reviewed preview and verify
51
+ npx tradeblocks-mcp setup --client codex --folder "/path/to/backtests" --yes --json
52
+ ```
53
+
54
+ Client choices are `claude-desktop`, `claude-code`, `codex`, and `gemini`.
55
+ Interactive setup shows all four clients numbered with detected/not-detected
56
+ markers and accepts a number or client name; it never selects one automatically.
57
+ Without `--json`, the preview and result are readable prose, including the
58
+ quoted launch command, verification, client action and next steps.
59
+ Folder paths are resolved to absolute paths, including paths with spaces. Setup
60
+ previews the exact file, entry and official user-scope registration command.
61
+ It can create a missing data folder only under the same consent. Without a TTY,
62
+ setup never prompts and requires `--client`, `--folder`, and `--yes` to write.
63
+ `--json` emits exactly one final result object on stdout; a redacted preview goes
64
+ to stderr before any write, even with `--yes`. Inspect `plannedChange` in a
65
+ preview run before approving it with `--yes`. Exit 0 means both registration
66
+ (or an equivalent existing entry) **and** MCP verification succeeded.
67
+
68
+ A differing `tradeblocks` entry needs separate replacement permission:
69
+ interactive setup asks separately, or agents add **both** `--replace` and
70
+ `--yes`. Replacement preserves existing `env` keys **and their values** and removes
71
+ other TradeBlocks-specific launch settings. Unknown entry fields (including URLs)
72
+ are conflicts, not equivalent stdio registrations. Gemini replacement refuses
73
+ environment values containing `=` before writing: its CLI truncates those values;
74
+ update the launch manually in its settings file while retaining the environment.
75
+ Previews show differing field names and environment key names, never existing
76
+ values or client diagnostics. Desktop updates preserve unrelated settings/servers,
77
+ use a same-directory temporary file and atomic rename, and back up existing files.
78
+ For all CLI clients, setup backs up an existing config before registration and
79
+ restores it if remove/add fails or read-back differs from the preview. The vendor
80
+ CLI may itself normalize or remove unrelated settings on successful registration
81
+ (Claude Code can drop unknown top-level keys); setup does not promise to undo a
82
+ successful vendor rewrite. The backup retains the original bytes. Backups may
83
+ contain secrets; protect them like the original and remove them when no longer needed.
84
+
85
+ Setup refuses unreadable or malformed configuration without rewriting it. This
86
+ includes Gemini settings with comments: repair/export strict JSON yourself
87
+ before using guided setup; manual configuration remains available below.
88
+ CLI registration uses `claude mcp add -s user`, `codex mcp add`, or
89
+ `gemini mcp add -s user`, not project-scope edits. Desktop uses the per-OS paths
90
+ below. The registered command uses the absolute `npx` found on your PATH.
91
+ For Desktop, setup also supplies Node/npm's directory in the server's `PATH`
92
+ when no existing server `PATH` is present, because a GUI may not inherit an
93
+ nvm shell's PATH. Moving/upgrading that Node installation can require setup
94
+ again; an existing server `PATH` is preserved, not overridden.
95
+
96
+ Verification starts the **read-back configured server subprocess**, performs MCP
97
+ `initialize` and `tools/list` with a 120-second timeout, and reports server
98
+ name/version and tool count. It does **not** prove that the AI client itself
99
+ connected. Restart/reopen Desktop or start a new CLI-client session afterward.
100
+ If verification fails, registration may already have succeeded: inspect the
101
+ result's `appliedChange`, fix the indicated prerequisites, and rerun.
102
+ The bounded two-minute window allows for a first npx launch downloading the
103
+ package and native DuckDB binaries. Setup explains that possibility before
104
+ verification starts.
105
+ The server's first npx launch may need network access to download TradeBlocks;
106
+ setup does not install clients, Node/npm, or change system settings.
107
+
108
+ For **Claude Code only**, the separate
109
+ [tradeblocks-skills plugin](https://github.com/tradeblocks-org/tradeblocks-skills)
110
+ is optional and is not installed by setup. Manual CSV import and core analysis
111
+ remain available without the plugin, OO, or market-data credentials.
112
+
17
113
  ### Option 1: npx (All Platforms)
18
114
 
19
115
  Run directly without installation:
@@ -37,7 +133,7 @@ npm install
37
133
  npm run build -w packages/mcp-server
38
134
 
39
135
  # Run the server
40
- node packages/mcp-server/server/index.js ~/Trading/backtests
136
+ node packages/mcp-server/server/cli.js ~/Trading/backtests
41
137
  ```
42
138
 
43
139
  ## Quick Start
@@ -251,23 +347,17 @@ Or set `TRADEBLOCKS_NO_AUTH=true` in `.env`.
251
347
 
252
348
  ## Agent Skills
253
349
 
254
- For guided conversational workflows, install the bundled agent skills:
350
+ For guided conversational workflows, install the agent skills from their standalone plugin,
351
+ [tradeblocks-skills](https://github.com/tradeblocks-org/tradeblocks-skills). In Claude Code:
255
352
 
256
353
  ```bash
257
- # Install skills to Claude Code
258
- tradeblocks-mcp install-skills
259
-
260
- # Install to other platforms
261
- tradeblocks-mcp install-skills --platform codex
262
- tradeblocks-mcp install-skills --platform gemini
263
-
264
- # Check installation status
265
- tradeblocks-mcp check-skills
266
-
267
- # Remove skills
268
- tradeblocks-mcp uninstall-skills
354
+ /plugin marketplace add tradeblocks-org/tradeblocks-skills
355
+ /plugin install tradeblocks@tradeblocks-skills
269
356
  ```
270
357
 
358
+ The former `install-skills`, `check-skills`, and `uninstall-skills` commands now only print these
359
+ instructions.
360
+
271
361
  Skills provide structured prompts for tasks like:
272
362
 
273
363
  - Strategy health checks
@@ -275,7 +365,68 @@ Skills provide structured prompts for tasks like:
275
365
  - Portfolio addition recommendations
276
366
  - Correlation analysis
277
367
 
278
- See [Agent Skills README](../agent-skills/README.md) for details.
368
+ See the [tradeblocks-skills README](https://github.com/tradeblocks-org/tradeblocks-skills) for details.
369
+
370
+ ## Prompts
371
+
372
+ The same MCP server exposes five prompts in stdio and HTTP. In Claude Code, when
373
+ the server is named `tradeblocks`, pick them from the `/` menu, where they appear
374
+ as `/tradeblocks:bring-in-oo-backtest (MCP)`, `/tradeblocks:is-this-optimum-real (MCP)`,
375
+ `/tradeblocks:stress-oo-portfolio (MCP)`, `/tradeblocks:live-vs-oo (MCP)` and
376
+ `/tradeblocks:allocate-oo-portfolio (MCP)`, or type
377
+ `/mcp__tradeblocks__bring-in-oo-backtest` (likewise for the others).
378
+ Other MCP clients can list and get these prompts through their prompt interface.
379
+ The server name is chosen by the user; these are not Option Omega tool prefixes.
380
+
381
+ Each prompt takes optional arguments, typed after the command in Claude Code in
382
+ this order, for example
383
+ `/mcp__tradeblocks__bring-in-oo-backtest <savedBacktestId> my-strategy`:
384
+ `bring-in-oo-backtest` and `stress-oo-portfolio` take `ooId` (an OO saved
385
+ backtest or portfolio ID, or a run's `runId`) and `block`; `is-this-optimum-real`
386
+ takes `optimizationId`; `live-vs-oo` takes `block`; `allocate-oo-portfolio`
387
+ takes `ooId` (saved portfolio ID or run ID) and `block`. Without arguments the
388
+ prompts work from the conversation.
389
+
390
+ | Prompt | Purpose |
391
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
392
+ | `bring-in-oo-backtest` | Capture a backtest's trade log as a block, or import an OO-exported trade CSV. |
393
+ | `is-this-optimum-real` | Compare the optimizer's best cell with a stable-region candidate using scratch runs and TradeBlocks robustness tests. |
394
+ | `stress-oo-portfolio` | Stress a portfolio's economic trades in an existing or imported block. |
395
+ | `live-vs-oo` | Compare live trades in a reporting log with an OO reference backtest's block. |
396
+ | `allocate-oo-portfolio` | Check TradeBlocks allocation leads in OO shared-funds runs; never alter the saved source. |
397
+
398
+ TradeBlocks never calls Option Omega. With the `tradeblocks-skills` Claude Code
399
+ plugin installed, the import prompt uses `/tradeblocks:oo-capture`, and the
400
+ optimum prompt directs to `/tradeblocks:is-this-optimum-real`. That skill
401
+ captures each scratch run separately and combines verified trades under
402
+ distinct strategies in one trade-only comparison block for a paired
403
+ best-minus-centre test. This test covers jointly traded days, not the
404
+ optimizer's selection from the grid. Without the plugin,
405
+ `bring-in-oo-backtest` asks for OO's trade-log and daily-log CSVs and imports
406
+ both in one `import_csv` call with `dailyLogPath`, so the block carries OO's
407
+ marked daily curve; it says so when the daily log is missing. The optimum
408
+ prompt without the plugin has each run's trade-log CSV exported from OO and
409
+ imported into separate blocks: it runs tests against zero per block and states
410
+ that no paired difference ran. The CSV must
411
+ be readable by the TradeBlocks server; for Docker/HTTP, put it inside the
412
+ mounted server data directory. If unavailable, stop instead of rebuilding
413
+ CSV from model responses.
414
+
415
+ `allocate-oo-portfolio` and the read-only `stress-oo-portfolio` use
416
+ `/tradeblocks:oo-capture` for a saved portfolio only when the installed skill
417
+ describes saved-portfolio capture (older plugin versions do not); otherwise
418
+ they use an existing block, or import OO's portfolio trade-log CSV
419
+ with `import_csv` and optionally its whole-book daily log with `dailyLogPath`.
420
+ TradeBlocks' correlation, marginal, tail, what-if and health results are
421
+ trade-derived counterfactual proposals, never OO marked equity or OO headline
422
+ figures. The whole-book curve has no per-member marks. A candidate is OO-tested
423
+ only after `get_portfolio_status` reports complete and `get_portfolio_results`
424
+ is read; saving requires an explicit request and a new portfolio. OO's headline
425
+ and marked-equity figures remain OO's; TradeBlocks' realized trade statistics
426
+ do not replace marked-account drawdown.
427
+
428
+ `live-vs-oo` reads the reporting log from the OO reference block's own folder
429
+ (see below); put the CSV there instead of importing it as a separate block.
279
430
 
280
431
  ## Block Directory Structure
281
432
 
@@ -297,20 +448,25 @@ backtests/
297
448
  **tradelog.csv** - Trade records with these key columns:
298
449
 
299
450
  - Date Opened, Time Opened, Date Closed, Time Closed
300
- - P/L (gross profit/loss)
301
- - Strategy, Legs (or Symbol)
302
- - No. of Contracts, Premium (optional)
451
+ - P/L (Option Omega exports already include fees; `import_csv` defaults to
452
+ `plBasis: "net_includes_fees"`. Use `"gross_before_fees"` only if fees still need deducting)
453
+ - Strategy (optional; the block ID is used when missing), Legs (or Symbol)
454
+ - No. of Contracts, Premium (optional; dollars per contract: `250` and `250.00` both mean $250)
455
+ - P/L % (optional; when numeric, report tools use Option Omega's value as the trade's `plPct`;
456
+ otherwise `plPct` is P/L ÷ |Premium × No. of Contracts| × 100)
303
457
 
304
458
  **dailylog.csv** - Daily portfolio values:
305
459
 
306
460
  - Date
307
- - Net Liquidity (or Portfolio Value, Equity)
461
+ - Net Liquidity (or Portfolio Value, Value, Equity)
308
462
  - P/L, Drawdown % (optional)
309
463
 
310
464
  **Flexible Detection**: Files don't need standard names. The server detects CSV types by examining column headers (ISS-006).
311
465
 
312
466
  ## Available Tools
313
467
 
468
+ These tables show common tools; [MCP Tools Reference](../../docs/mcp-tools.md) lists every registered tool.
469
+
314
470
  ### Core Tools
315
471
 
316
472
  | Tool | Description |
@@ -321,6 +477,12 @@ backtests/
321
477
  | `get_strategy_comparison` | Compare strategies within a block |
322
478
  | `compare_blocks` | Compare statistics across multiple blocks |
323
479
 
480
+ Trade-calendar dates in tool output are calendar days in `YYYY-MM-DD` form and name
481
+ the same day on every server timezone. That covers a trade's opened or closed day, a
482
+ daily-log day, and any range or window built from them, such as `dateRange`,
483
+ walk-forward windows, `peakExposure` dates, and `run_sql` DATE columns. Real instants,
484
+ such as `calculatedAt`, sync times, and market-feed timestamps, remain ISO timestamps.
485
+
324
486
  ### Analysis Tools
325
487
 
326
488
  | Tool | Description |
@@ -378,9 +540,32 @@ backtests/
378
540
 
379
541
  ### Import Tools
380
542
 
381
- | Tool | Description |
382
- | ------------ | ------------------------------------------------------------------------------- |
383
- | `import_csv` | Import a CSV file as a new block _(CLI only - not available in Claude Desktop)_ |
543
+ | Tool | Description |
544
+ | ------------ | -------------------------------------------------------- |
545
+ | `import_csv` | Import a CSV file as a block from a server-readable path |
546
+
547
+ `import_csv` accepts `csvPath`, `blockName`, optional `csvType` (default `tradelog`),
548
+ optional `dailyLogPath` for a paired daily log, and `plBasis` (default
549
+ `net_includes_fees` for Option Omega exports; `gross_before_fees` when fee columns
550
+ still need deducting). Both paths must be readable by the server: local for stdio,
551
+ or inside its mounted data directory for Docker/HTTP. Paths can be absolute, use
552
+ `~`, or use filename-only search via `searchPaths` (default: Downloads, Desktop, Documents).
553
+ The pair creates one block with a stamped `tradelog.csv` and verbatim `dailylog.csv`.
554
+ A bad daily log rejects the whole import without creating a block. The usual
555
+ result fields describe the primary CSV; its `dateRange` holds the first and last
556
+ calendar dates (`YYYY-MM-DD`).
557
+ Paired results also include `dailyLog.recordCount` and `dailyLog.dateRange`
558
+ in the same calendar-date form. Unfiltered `get_statistics` uses daily-log drawdown;
559
+ strategy-filtered statistics use the trade log instead.
560
+
561
+ Before writing a block, `import_csv` refuses an invalid opened/closed trade or reporting
562
+ calendar date, missing or unparseable P/L, or missing/unparseable daily-log value. For
563
+ gross-before-fees trade imports, both commission values are also required. Errors
564
+ identify the CSV file line including the header; a rejected pair leaves no block.
565
+ Daily-log aliases load as net liquidity in imported and folder-discovered blocks;
566
+ folder loading drops a row with a missing/unparseable value rather than using zero.
567
+ `recordCount` counts loaded rows, and a missing Strategy in a trade log uses the
568
+ block ID in the receipt and in subsequent tools.
384
569
 
385
570
  ## Development
386
571
 
@@ -428,6 +613,6 @@ See the [Market Data Guide](../../docs/market-data.md) for import examples, tick
428
613
 
429
614
  - [Usage Guide](../../docs/usage.md) - Detailed usage examples and workflows
430
615
  - [Web Platforms Guide](../../docs/web-platforms.md) - Connect to ChatGPT, Google AI Studio, Julius
431
- - [Agent Skills](../agent-skills/README.md) - Conversational workflows for guided analysis
616
+ - [Agent Skills](https://github.com/tradeblocks-org/tradeblocks-skills) - Conversational workflows for guided analysis
432
617
  - [Market Data Guide](../../docs/market-data.md) - Import workflow, Massive API, and column mapping reference
433
618
  - [Main Application](../../README.md) - Web-based UI for TradeBlocks
@@ -15,6 +15,7 @@ var TRADE_COLUMN_MAPPING = {
15
15
  "Avg. Closing Cost": "avgClosingCost",
16
16
  "Reason For Close": "reasonForClose",
17
17
  "P/L": "pl",
18
+ "P/L %": "plPct",
18
19
  "No. of Contracts": "numContracts",
19
20
  "Funds at Close": "fundsAtClose",
20
21
  "Margin Req.": "marginReq",
@@ -34,9 +35,7 @@ var TRADE_COLUMN_ALIASES = {
34
35
  "Opening comms & fees": "Opening Commissions + Fees",
35
36
  "Opening Commissions & Fees": "Opening Commissions + Fees",
36
37
  "Closing comms & fees": "Closing Commissions + Fees",
37
- "Closing Commissions & Fees": "Closing Commissions + Fees",
38
- "P/L %": "P/L %"
39
- // Recognized but ignored (we calculate our own plPct)
38
+ "Closing Commissions & Fees": "Closing Commissions + Fees"
40
39
  };
41
40
  var REQUIRED_TRADE_COLUMNS = [
42
41
  "Date Opened",
@@ -56,10 +55,29 @@ var REQUIRED_TRADE_COLUMNS = [
56
55
  "Strategy"
57
56
  ];
58
57
 
58
+ // ../lib/utils/csv-headers.ts
59
+ var BOM = "\uFEFF";
60
+ function stripBom(value) {
61
+ return value.startsWith(BOM) ? value.slice(1) : value;
62
+ }
63
+ function normalizeHeader(header, aliases) {
64
+ const trimmed = stripBom(header).trim();
65
+ return aliases?.[trimmed] ?? trimmed;
66
+ }
67
+ function normalizeHeaders(headers, aliases) {
68
+ return headers.map((header) => normalizeHeader(header, aliases));
69
+ }
70
+ function findMissingHeaders(headers, required) {
71
+ const headerSet = new Set(headers);
72
+ return required.filter((requiredHeader) => !headerSet.has(requiredHeader));
73
+ }
74
+
59
75
  export {
60
76
  PlBasis,
61
77
  TRADE_COLUMN_MAPPING,
62
78
  TRADE_COLUMN_ALIASES,
63
- REQUIRED_TRADE_COLUMNS
79
+ REQUIRED_TRADE_COLUMNS,
80
+ normalizeHeaders,
81
+ findMissingHeaders
64
82
  };
65
- //# sourceMappingURL=chunk-TKUG3FOR.js.map
83
+ //# sourceMappingURL=chunk-BH6SQOWF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../lib/models/trade.ts","../../lib/utils/csv-headers.ts"],"sourcesContent":["/**\n * Trade model based on legacy Python Trade class\n * Represents individual trade record from portfolio CSV\n */\nexport const PlBasis = {\n NetIncludesFees: \"net_includes_fees\",\n GrossBeforeFees: \"gross_before_fees\",\n} as const;\nexport type PlBasis = (typeof PlBasis)[keyof typeof PlBasis];\n\nexport interface Trade {\n // Core trade identification\n dateOpened: Date;\n timeOpened: string; // HH:mm:ss format\n openingPrice: number;\n legs: string; // Option legs description\n /** Opening premium in dollars for one contract (one lot). */\n premium: number;\n\n // Closing information (optional for open trades)\n closingPrice?: number;\n dateClosed?: Date;\n timeClosed?: string;\n avgClosingCost?: number;\n reasonForClose?: string;\n\n // Financial metrics\n pl: number; // Profit/Loss in the basis declared by plBasis\n /**\n * Declares whether `pl` already includes commissions and fees.\n *\n * Option Omega trade-log exports use `net_includes_fees`. Programmatic\n * callers that omit this field retain the legacy TradeBlocks assumption\n * that `pl` is gross and fees must be deducted once.\n */\n plBasis?: PlBasis;\n /**\n * P/L as a percent of total premium, as reported by the source CSV's `P/L %` column\n * (Option Omega writes `97.9` for 97.9%). Absent when the column is missing or its cell is\n * blank or unparseable; enrichment then computes `pl / |premium × contracts| × 100`.\n */\n plPct?: number;\n numContracts: number;\n fundsAtClose: number;\n /** False when the MCP trade CSV omitted or left Funds at Close blank. */\n fundsAtCloseProvided?: boolean;\n marginReq: number;\n\n // Trade metadata\n strategy: string;\n openingCommissionsFees: number;\n closingCommissionsFees: number;\n\n // Ratios and market data\n openingShortLongRatio: number;\n closingShortLongRatio?: number;\n openingVix?: number;\n closingVix?: number;\n\n // Additional metrics\n gap?: number;\n movement?: number;\n maxProfit?: number;\n maxLoss?: number;\n /**\n * Synthetic-only: ratio of the worst observed loss to account capital at the time\n * Used to scale synthetic losses relative to current account size\n */\n syntheticCapitalRatio?: number;\n\n /**\n * Custom fields from extra columns in the trade CSV\n * Keys are the original column names, values are auto-detected as number or string\n */\n customFields?: Record<string, number | string>;\n}\n\n/**\n * Raw trade data as it comes from CSV before processing\n */\nexport interface RawTradeData {\n \"Date Opened\": string;\n \"Time Opened\": string;\n \"Opening Price\": string;\n Legs: string;\n Premium: string;\n \"Closing Price\"?: string;\n \"Date Closed\"?: string;\n \"Time Closed\"?: string;\n \"Avg. Closing Cost\"?: string;\n \"Reason For Close\"?: string;\n \"P/L\": string;\n \"P/L %\"?: string;\n \"No. of Contracts\": string;\n \"Funds at Close\": string;\n \"Margin Req.\": string;\n Strategy: string;\n \"Opening Commissions + Fees\": string;\n \"Closing Commissions + Fees\"?: string;\n \"Opening Short/Long Ratio\": string;\n \"Closing Short/Long Ratio\"?: string;\n \"Opening VIX\"?: string;\n \"Closing VIX\"?: string;\n Gap?: string;\n Movement?: string;\n \"Max Profit\"?: string;\n \"Max Loss\"?: string;\n}\n\n/**\n * Column mapping from CSV headers to Trade interface properties\n */\nexport const TRADE_COLUMN_MAPPING = {\n \"Date Opened\": \"dateOpened\",\n \"Time Opened\": \"timeOpened\",\n \"Opening Price\": \"openingPrice\",\n Legs: \"legs\",\n Premium: \"premium\",\n \"Closing Price\": \"closingPrice\",\n \"Date Closed\": \"dateClosed\",\n \"Time Closed\": \"timeClosed\",\n \"Avg. Closing Cost\": \"avgClosingCost\",\n \"Reason For Close\": \"reasonForClose\",\n \"P/L\": \"pl\",\n \"P/L %\": \"plPct\",\n \"No. of Contracts\": \"numContracts\",\n \"Funds at Close\": \"fundsAtClose\",\n \"Margin Req.\": \"marginReq\",\n Strategy: \"strategy\",\n \"Opening Commissions + Fees\": \"openingCommissionsFees\",\n \"Closing Commissions + Fees\": \"closingCommissionsFees\",\n \"Opening Short/Long Ratio\": \"openingShortLongRatio\",\n \"Closing Short/Long Ratio\": \"closingShortLongRatio\",\n \"Opening VIX\": \"openingVix\",\n \"Closing VIX\": \"closingVix\",\n Gap: \"gap\",\n Movement: \"movement\",\n \"Max Profit\": \"maxProfit\",\n \"Max Loss\": \"maxLoss\",\n} as const;\n\n/**\n * Column aliases for different CSV export variations\n */\nexport const TRADE_COLUMN_ALIASES = {\n \"Opening comms & fees\": \"Opening Commissions + Fees\",\n \"Opening Commissions & Fees\": \"Opening Commissions + Fees\",\n \"Closing comms & fees\": \"Closing Commissions + Fees\",\n \"Closing Commissions & Fees\": \"Closing Commissions + Fees\",\n} as const;\n\n/**\n * Minimum required columns for a valid trade log\n */\nexport const REQUIRED_TRADE_COLUMNS = [\n \"Date Opened\",\n \"Time Opened\",\n \"Opening Price\",\n \"Legs\",\n \"Premium\",\n \"Closing Price\",\n \"Date Closed\",\n \"Time Closed\",\n \"Avg. Closing Cost\",\n \"Reason For Close\",\n \"P/L\",\n \"No. of Contracts\",\n \"Funds at Close\",\n \"Margin Req.\",\n \"Strategy\",\n] as const;\n","/**\n * CSV header utilities\n */\n\nexport interface HeaderValidationOptions {\n /** Optional map of alternate header names to canonical names */\n aliases?: Record<string, string> | Readonly<Record<string, string>>;\n /** Human-readable label used in error messages */\n contextLabel?: string;\n}\n\nconst BOM = \"\\uFEFF\";\n\n/**\n * Remove UTF-8 byte order mark from a string if present\n */\nexport function stripBom(value: string): string {\n return value.startsWith(BOM) ? value.slice(1) : value;\n}\n\n/**\n * Parse a single CSV line into values, handling quoted fields and commas\n */\nexport function 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 const nextChar = line[i + 1];\n if (inQuotes && nextChar === '\"') {\n // Escaped quote inside quoted value\n current += '\"';\n i++;\n } else {\n inQuotes = !inQuotes;\n }\n continue;\n }\n\n if (char === \",\" && !inQuotes) {\n result.push(current.trim());\n current = \"\";\n } else {\n current += char;\n }\n }\n\n result.push(current.trim());\n\n if (result.length > 0) {\n result[0] = stripBom(result[0]);\n }\n\n return result;\n}\n\n/**\n * Normalize a CSV header by trimming whitespace, stripping BOM, and applying aliases\n */\nexport function normalizeHeader(\n header: string,\n aliases?: Record<string, string> | Readonly<Record<string, string>>,\n): string {\n const trimmed = stripBom(header).trim();\n return aliases?.[trimmed] ?? trimmed;\n}\n\n/**\n * Normalize an array of headers\n */\nexport function normalizeHeaders(\n headers: string[],\n aliases?: Record<string, string> | Readonly<Record<string, string>>,\n): string[] {\n return headers.map((header) => normalizeHeader(header, aliases));\n}\n\n/**\n * Validate that required headers are present. Returns the missing headers without throwing.\n */\nexport function findMissingHeaders(headers: string[], required: readonly string[]): string[] {\n const headerSet = new Set(headers);\n return required.filter((requiredHeader) => !headerSet.has(requiredHeader));\n}\n\n/**\n * Ensure required headers are present, throwing an Error with a helpful message when missing.\n */\nexport function assertRequiredHeaders(\n headers: string[],\n required: readonly string[],\n options: HeaderValidationOptions = {},\n): void {\n const missing = findMissingHeaders(headers, required);\n if (missing.length === 0) {\n return;\n }\n\n const label = options.contextLabel ? `${options.contextLabel} ` : \"\";\n throw new Error(`Missing required ${label}columns: ${missing.join(\", \")}`);\n}\n"],"mappings":";AAIO,IAAM,UAAU;AAAA,EACrB,iBAAiB;AAAA,EACjB,iBAAiB;AACnB;AAyGO,IAAM,uBAAuB;AAAA,EAClC,eAAe;AAAA,EACf,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,MAAM;AAAA,EACN,SAAS;AAAA,EACT,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,eAAe;AAAA,EACf,qBAAqB;AAAA,EACrB,oBAAoB;AAAA,EACpB,OAAO;AAAA,EACP,SAAS;AAAA,EACT,oBAAoB;AAAA,EACpB,kBAAkB;AAAA,EAClB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,8BAA8B;AAAA,EAC9B,8BAA8B;AAAA,EAC9B,4BAA4B;AAAA,EAC5B,4BAA4B;AAAA,EAC5B,eAAe;AAAA,EACf,eAAe;AAAA,EACf,KAAK;AAAA,EACL,UAAU;AAAA,EACV,cAAc;AAAA,EACd,YAAY;AACd;AAKO,IAAM,uBAAuB;AAAA,EAClC,wBAAwB;AAAA,EACxB,8BAA8B;AAAA,EAC9B,wBAAwB;AAAA,EACxB,8BAA8B;AAChC;AAKO,IAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;;;AC/JA,IAAM,MAAM;AAKL,SAAS,SAAS,OAAuB;AAC9C,SAAO,MAAM,WAAW,GAAG,IAAI,MAAM,MAAM,CAAC,IAAI;AAClD;AA6CO,SAAS,gBACd,QACA,SACQ;AACR,QAAM,UAAU,SAAS,MAAM,EAAE,KAAK;AACtC,SAAO,UAAU,OAAO,KAAK;AAC/B;AAKO,SAAS,iBACd,SACA,SACU;AACV,SAAO,QAAQ,IAAI,CAAC,WAAW,gBAAgB,QAAQ,OAAO,CAAC;AACjE;AAKO,SAAS,mBAAmB,SAAmB,UAAuC;AAC3F,QAAM,YAAY,IAAI,IAAI,OAAO;AACjC,SAAO,SAAS,OAAO,CAAC,mBAAmB,CAAC,UAAU,IAAI,cAAc,CAAC;AAC3E;","names":[]}
@@ -1,16 +1,16 @@
1
1
  import {
2
2
  CSVParser,
3
- findMissingHeaders,
4
- normalizeHeaders,
5
3
  rawTradeDataSchema,
6
4
  tradeSchema
7
- } from "./chunk-DJRGJXYP.js";
5
+ } from "./chunk-ZSVYX5PP.js";
8
6
  import {
9
7
  PlBasis,
10
8
  REQUIRED_TRADE_COLUMNS,
11
9
  TRADE_COLUMN_ALIASES,
12
- TRADE_COLUMN_MAPPING
13
- } from "./chunk-P3IETL3K.js";
10
+ TRADE_COLUMN_MAPPING,
11
+ findMissingHeaders,
12
+ normalizeHeaders
13
+ } from "./chunk-BH6SQOWF.js";
14
14
 
15
15
  // ../lib/processing/trade-processor.ts
16
16
  var KNOWN_TRADE_COLUMNS = /* @__PURE__ */ new Set([
@@ -277,15 +277,13 @@ var TradeProcessor = class {
277
277
  }
278
278
  return parsed;
279
279
  };
280
- const rawPremiumString = (rawData["Premium"] || "").replace(/[$,]/g, "").trim();
281
- const premiumPrecision = rawPremiumString && !rawPremiumString.includes(".") ? "cents" : "dollars";
280
+ const reportedPlPct = parseNumber(rawData["P/L %"], "P/L %", NaN);
282
281
  const trade = {
283
282
  dateOpened,
284
283
  timeOpened: rawData["Time Opened"] || "00:00:00",
285
284
  openingPrice: parseNumber(rawData["Opening Price"], "Opening Price"),
286
285
  legs: rawData["Legs"] || "",
287
286
  premium: parseNumber(rawData["Premium"], "Premium"),
288
- premiumPrecision,
289
287
  closingPrice: rawData["Closing Price"] ? parseNumber(rawData["Closing Price"], "Closing Price") : void 0,
290
288
  dateClosed,
291
289
  timeClosed: rawData["Time Closed"] || void 0,
@@ -293,6 +291,7 @@ var TradeProcessor = class {
293
291
  reasonForClose: rawData["Reason For Close"] || void 0,
294
292
  pl: parseNumber(rawData["P/L"], "P/L"),
295
293
  plBasis: PlBasis.NetIncludesFees,
294
+ plPct: Number.isFinite(reportedPlPct) ? reportedPlPct : void 0,
296
295
  numContracts: Math.round(parseNumber(rawData["No. of Contracts"], "No. of Contracts")),
297
296
  fundsAtClose: parseNumber(rawData["Funds at Close"], "Funds at Close"),
298
297
  marginReq: parseNumber(rawData["Margin Req."], "Margin Req."),
@@ -356,4 +355,4 @@ var TradeProcessor = class {
356
355
  export {
357
356
  TradeProcessor
358
357
  };
359
- //# sourceMappingURL=chunk-3KDAY4J4.js.map
358
+ //# sourceMappingURL=chunk-D4AWELQU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../lib/processing/trade-processor.ts"],"sourcesContent":["/**\n * Trade Processor\n *\n * Handles parsing and processing of trade log CSV files from OptionOmega.\n * Converts raw CSV data to validated Trade objects.\n */\n\nimport {\n PlBasis,\n type Trade,\n TRADE_COLUMN_ALIASES,\n REQUIRED_TRADE_COLUMNS,\n TRADE_COLUMN_MAPPING,\n} from \"../models/trade.ts\";\nimport type { ValidationError, ProcessingError } from \"../models/index.ts\";\nimport { rawTradeDataSchema, tradeSchema } from \"../models/validators.ts\";\nimport { CSVParser, type ParseProgress } from \"./csv-parser.ts\";\nimport { findMissingHeaders, normalizeHeaders } from \"../utils/csv-headers.ts\";\n\n/**\n * Set of known trade column names (canonical names from TRADE_COLUMN_MAPPING)\n * Used to identify custom columns that should be preserved\n */\nconst KNOWN_TRADE_COLUMNS = new Set([\n ...Object.keys(TRADE_COLUMN_MAPPING),\n ...Object.keys(TRADE_COLUMN_ALIASES),\n]);\n\n/**\n * Trade processing configuration\n */\nexport interface TradeProcessingConfig {\n maxTrades?: number;\n strictValidation?: boolean;\n progressCallback?: (progress: TradeProcessingProgress) => void;\n}\n\n/**\n * Trade processing progress\n */\nexport interface TradeProcessingProgress extends ParseProgress {\n stage: \"reading\" | \"parsing\" | \"validating\" | \"converting\" | \"completed\";\n validTrades: number;\n invalidTrades: number;\n}\n\n/**\n * Trade processing result\n */\nexport interface TradeProcessingResult {\n trades: Trade[];\n totalRows: number;\n validTrades: number;\n invalidTrades: number;\n errors: ProcessingError[];\n warnings: string[];\n stats: {\n processingTimeMs: number;\n strategies: string[];\n dateRange: { start: Date | null; end: Date | null };\n totalPL: number;\n };\n}\n\n/**\n * Trade processor class\n */\nexport class TradeProcessor {\n private config: Required<TradeProcessingConfig>;\n\n constructor(config: TradeProcessingConfig = {}) {\n this.config = {\n maxTrades: 50000,\n strictValidation: false,\n progressCallback: () => {},\n ...config,\n };\n }\n\n /**\n * Process trade log file\n */\n async processFile(file: File): Promise<TradeProcessingResult> {\n const startTime = Date.now();\n const errors: ProcessingError[] = [];\n const warnings: string[] = [];\n\n try {\n // Validate file\n const fileValidation = CSVParser.validateCSVFile(file);\n if (!fileValidation.valid) {\n throw new Error(fileValidation.error);\n }\n\n // Configure CSV parser\n const csvParser = new CSVParser({\n maxRows: this.config.maxTrades,\n progressCallback: (progress, rowsProcessed) => {\n this.config.progressCallback({\n stage: \"parsing\",\n progress,\n rowsProcessed,\n totalRows: 0,\n errors: 0,\n validTrades: 0,\n invalidTrades: 0,\n });\n },\n });\n\n // Parse CSV with validation\n const parseResult = await csvParser.parseFileObject(\n file,\n (row, rowIndex) => this.validateRawTradeData(row, rowIndex),\n (progress) => {\n this.config.progressCallback({\n ...progress,\n validTrades: 0,\n invalidTrades: 0,\n });\n },\n );\n\n // Collect parsing errors\n errors.push(...parseResult.errors);\n warnings.push(...parseResult.warnings);\n\n // Check for required columns\n const normalizedHeaders = normalizeHeaders(parseResult.headers, TRADE_COLUMN_ALIASES);\n const missingColumns = findMissingHeaders(normalizedHeaders, REQUIRED_TRADE_COLUMNS);\n if (missingColumns.length > 0) {\n throw new Error(`Missing required columns: ${missingColumns.join(\", \")}`);\n }\n\n // Update progress for conversion stage\n this.config.progressCallback({\n stage: \"converting\",\n progress: 0,\n rowsProcessed: 0,\n totalRows: parseResult.data.length,\n errors: errors.length,\n validTrades: 0,\n invalidTrades: 0,\n });\n\n // Convert validated data to Trade objects\n const trades: Trade[] = [];\n let validTrades = 0;\n let invalidTrades = 0;\n\n for (let i = 0; i < parseResult.data.length; i++) {\n try {\n const trade = this.convertToTrade(parseResult.data[i]);\n trades.push(trade);\n validTrades++;\n } catch (error) {\n invalidTrades++;\n const errorMessage = `Trade conversion failed at row ${i + 2}: ${error instanceof Error ? error.message : String(error)}`;\n\n // Log conversion errors to console for debugging\n console.warn(`[TradeProcessor] ${errorMessage}`);\n\n const validationError: ValidationError = {\n type: \"validation\",\n message: errorMessage,\n details: { row: parseResult.data[i], rowIndex: i + 2 },\n field: \"unknown\",\n value: parseResult.data[i],\n expected: \"Valid trade data\",\n };\n errors.push(validationError);\n\n if (!this.config.strictValidation) {\n continue; // Skip invalid row in non-strict mode\n } else {\n throw error; // Fail fast in strict mode\n }\n }\n\n // Update progress\n if (i % 100 === 0 || i === parseResult.data.length - 1) {\n const progress = Math.round((i / parseResult.data.length) * 100);\n this.config.progressCallback({\n stage: \"converting\",\n progress,\n rowsProcessed: i + 1,\n totalRows: parseResult.data.length,\n errors: errors.length,\n validTrades,\n invalidTrades,\n });\n }\n }\n\n // Sort trades for consistent ordering (handles simultaneous trades)\n trades.sort((a, b) => {\n const dateCompare = new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime();\n if (dateCompare !== 0) return dateCompare;\n\n // Secondary sort by time\n const timeCompare = a.timeOpened.localeCompare(b.timeOpened);\n if (timeCompare !== 0) return timeCompare;\n\n // Tertiary sort by funds_at_close (lower first for simultaneous trades)\n return a.fundsAtClose - b.fundsAtClose;\n });\n\n // Calculate statistics\n const processingTimeMs = Date.now() - startTime;\n const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();\n const dates = trades.map((t) => new Date(t.dateOpened));\n const dateRange = {\n start: dates.length > 0 ? new Date(Math.min(...dates.map((d) => d.getTime()))) : null,\n end: dates.length > 0 ? new Date(Math.max(...dates.map((d) => d.getTime()))) : null,\n };\n const totalPL = trades.reduce((sum, t) => sum + t.pl, 0);\n\n // Final progress update\n this.config.progressCallback({\n stage: \"completed\",\n progress: 100,\n rowsProcessed: parseResult.data.length,\n totalRows: parseResult.data.length,\n errors: errors.length,\n validTrades,\n invalidTrades,\n });\n\n return {\n trades,\n totalRows: parseResult.totalRows,\n validTrades,\n invalidTrades,\n errors,\n warnings,\n stats: {\n processingTimeMs,\n strategies,\n dateRange,\n totalPL,\n },\n };\n } catch (error) {\n const processingError: ProcessingError = {\n type: \"parsing\",\n message: `Trade processing failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { fileName: file.name, fileSize: file.size },\n };\n\n return {\n trades: [],\n totalRows: 0,\n validTrades: 0,\n invalidTrades: 0,\n errors: [processingError, ...errors],\n warnings,\n stats: {\n processingTimeMs: Date.now() - startTime,\n strategies: [],\n dateRange: { start: null, end: null },\n totalPL: 0,\n },\n };\n }\n }\n\n /**\n * Validate raw trade data from CSV\n */\n private validateRawTradeData(\n row: Record<string, string>,\n rowIndex: number,\n ): Record<string, string> | null {\n try {\n // Apply column aliases to normalize variations\n const normalizedRow = { ...row };\n Object.entries(TRADE_COLUMN_ALIASES).forEach(([alias, canonical]) => {\n if (normalizedRow[alias] !== undefined) {\n normalizedRow[canonical] = normalizedRow[alias];\n delete normalizedRow[alias];\n }\n });\n\n // OptionOmega sometimes leaves strategy blank; default to Unknown so downstream stats still work\n if (!normalizedRow[\"Strategy\"] || normalizedRow[\"Strategy\"].trim() === \"\") {\n normalizedRow[\"Strategy\"] = \"Unknown\";\n }\n\n // Ensure required columns have values\n const requiredFields = [\"Date Opened\", \"P/L\", \"Strategy\"];\n for (const field of requiredFields) {\n if (!normalizedRow[field] || normalizedRow[field].trim() === \"\") {\n throw new Error(`Missing required field: ${field}`);\n }\n }\n\n // Set default values for missing optional fields\n if (!normalizedRow[\"Opening Commissions + Fees\"]) {\n normalizedRow[\"Opening Commissions + Fees\"] = \"0\";\n }\n if (!normalizedRow[\"Closing Commissions + Fees\"]) {\n normalizedRow[\"Closing Commissions + Fees\"] = \"0\";\n }\n if (!normalizedRow[\"Opening Short/Long Ratio\"]) {\n normalizedRow[\"Opening Short/Long Ratio\"] = \"0\";\n }\n\n const optionalNumericFieldsWithDefaultZero = [\n \"Opening VIX\",\n \"Closing VIX\",\n \"Gap\",\n \"Movement\",\n ] as const;\n optionalNumericFieldsWithDefaultZero.forEach((field) => {\n if (!normalizedRow[field] || normalizedRow[field].trim() === \"\") {\n normalizedRow[field] = \"0\";\n }\n });\n\n // Basic format validation (detailed validation happens in conversion)\n rawTradeDataSchema.parse(normalizedRow);\n\n return normalizedRow;\n } catch (error) {\n // Log validation errors to console for debugging\n console.warn(\n `[TradeProcessor] Row ${rowIndex + 2} validation failed:`,\n error instanceof Error ? error.message : error,\n );\n // Return null for invalid rows - they'll be counted as invalid\n return null;\n }\n }\n\n /**\n * Parse a YYYY-MM-DD date string preserving the calendar date.\n *\n * Option Omega exports dates in Eastern time. JavaScript's new Date('YYYY-MM-DD')\n * parses as UTC midnight, which when converted to local time can shift to the\n * previous day (e.g., Dec 11 UTC → Dec 10 7pm EST).\n *\n * This method creates a Date representing midnight local time on the specified\n * calendar date, so Dec 11 in the CSV becomes Dec 11 in the app regardless of timezone.\n */\n private 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 // Create date at midnight local time - this preserves the calendar date\n return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));\n }\n // Fall back to default parsing for other formats\n return new Date(dateStr);\n }\n\n /**\n * Convert validated CSV row to Trade object\n */\n private convertToTrade(rawData: Record<string, string>): Trade {\n try {\n // Parse dates preserving calendar day\n const dateOpened = this.parseDatePreservingCalendarDay(rawData[\"Date Opened\"]);\n if (isNaN(dateOpened.getTime())) {\n throw new Error(`Invalid Date Opened: ${rawData[\"Date Opened\"]}`);\n }\n\n const dateClosed = rawData[\"Date Closed\"]\n ? this.parseDatePreservingCalendarDay(rawData[\"Date Closed\"])\n : undefined;\n if (dateClosed && isNaN(dateClosed.getTime())) {\n throw new Error(`Invalid Date Closed: ${rawData[\"Date Closed\"]}`);\n }\n\n // Normalize strategy name (handle empty strings)\n const strategy = (rawData[\"Strategy\"] || \"\").trim() || \"Unknown\";\n\n // Parse numeric values with error handling and NaN handling\n const parseNumber = (\n value: string | undefined,\n fieldName: string,\n defaultValue?: number,\n ): number => {\n if (!value || value.trim() === \"\" || value.toLowerCase() === \"nan\") {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Missing required numeric field: ${fieldName}`);\n }\n\n // Remove currency symbols and commas\n const cleaned = value.replace(/[$,]/g, \"\").trim();\n const parsed = parseFloat(cleaned);\n\n if (isNaN(parsed)) {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Invalid numeric value for ${fieldName}: ${value}`);\n }\n\n return parsed;\n };\n\n // Build trade object\n const rawPremiumString = (rawData[\"Premium\"] || \"\").replace(/[$,]/g, \"\").trim();\n const premiumPrecision: Trade[\"premiumPrecision\"] =\n rawPremiumString && !rawPremiumString.includes(\".\") ? \"cents\" : \"dollars\";\n\n const trade: Trade = {\n dateOpened,\n timeOpened: rawData[\"Time Opened\"] || \"00:00:00\",\n openingPrice: parseNumber(rawData[\"Opening Price\"], \"Opening Price\"),\n legs: rawData[\"Legs\"] || \"\",\n premium: parseNumber(rawData[\"Premium\"], \"Premium\"),\n premiumPrecision,\n closingPrice: rawData[\"Closing Price\"]\n ? parseNumber(rawData[\"Closing Price\"], \"Closing Price\")\n : undefined,\n dateClosed,\n timeClosed: rawData[\"Time Closed\"] || undefined,\n avgClosingCost: rawData[\"Avg. Closing Cost\"]\n ? parseNumber(rawData[\"Avg. Closing Cost\"], \"Avg. Closing Cost\")\n : undefined,\n reasonForClose: rawData[\"Reason For Close\"] || undefined,\n pl: parseNumber(rawData[\"P/L\"], \"P/L\"),\n plBasis: PlBasis.NetIncludesFees,\n numContracts: Math.round(parseNumber(rawData[\"No. of Contracts\"], \"No. of Contracts\")),\n fundsAtClose: parseNumber(rawData[\"Funds at Close\"], \"Funds at Close\"),\n marginReq: parseNumber(rawData[\"Margin Req.\"], \"Margin Req.\"),\n strategy,\n openingCommissionsFees: parseNumber(\n rawData[\"Opening Commissions + Fees\"],\n \"Opening Commissions\",\n 0,\n ),\n closingCommissionsFees: parseNumber(\n rawData[\"Closing Commissions + Fees\"],\n \"Closing Commissions\",\n 0,\n ),\n openingShortLongRatio: parseNumber(\n rawData[\"Opening Short/Long Ratio\"],\n \"Opening Short/Long Ratio\",\n 0,\n ),\n closingShortLongRatio: rawData[\"Closing Short/Long Ratio\"]\n ? parseNumber(rawData[\"Closing Short/Long Ratio\"], \"Closing Short/Long Ratio\")\n : undefined,\n openingVix: rawData[\"Opening VIX\"]\n ? parseNumber(rawData[\"Opening VIX\"], \"Opening VIX\")\n : undefined,\n closingVix: rawData[\"Closing VIX\"]\n ? parseNumber(rawData[\"Closing VIX\"], \"Closing VIX\")\n : undefined,\n gap: rawData[\"Gap\"] ? parseNumber(rawData[\"Gap\"], \"Gap\") : undefined,\n movement: rawData[\"Movement\"] ? parseNumber(rawData[\"Movement\"], \"Movement\") : undefined,\n maxProfit: rawData[\"Max Profit\"]\n ? parseNumber(rawData[\"Max Profit\"], \"Max Profit\")\n : undefined,\n maxLoss: rawData[\"Max Loss\"] ? parseNumber(rawData[\"Max Loss\"], \"Max Loss\") : undefined,\n };\n\n // Extract custom fields (columns not in KNOWN_TRADE_COLUMNS)\n const customFields: Record<string, number | string> = {};\n for (const [key, value] of Object.entries(rawData)) {\n if (!KNOWN_TRADE_COLUMNS.has(key) && value !== undefined && value.trim() !== \"\") {\n // Auto-detect type: try to parse as number\n const cleaned = value.replace(/[$,%]/g, \"\").trim();\n const parsed = parseFloat(cleaned);\n if (!isNaN(parsed) && isFinite(parsed)) {\n customFields[key] = parsed;\n } else {\n customFields[key] = value.trim();\n }\n }\n }\n\n // Only add customFields if there are any\n if (Object.keys(customFields).length > 0) {\n trade.customFields = customFields;\n }\n\n // Final validation with Zod schema\n const validatedTrade = tradeSchema.parse(trade);\n return validatedTrade;\n } catch (error) {\n throw new Error(\n `Trade conversion failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Process CSV content directly (for testing)\n */\n async processCSVContent(content: string): Promise<TradeProcessingResult> {\n // Create a mock File object for testing\n const blob = new Blob([content], { type: \"text/csv\" });\n const file = new File([blob], \"test.csv\", { type: \"text/csv\" });\n return this.processFile(file);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../lib/processing/trade-processor.ts"],"sourcesContent":["/**\n * Trade Processor\n *\n * Handles parsing and processing of trade log CSV files from OptionOmega.\n * Converts raw CSV data to validated Trade objects.\n */\n\nimport {\n PlBasis,\n type Trade,\n TRADE_COLUMN_ALIASES,\n REQUIRED_TRADE_COLUMNS,\n TRADE_COLUMN_MAPPING,\n} from \"../models/trade.ts\";\nimport type { ValidationError, ProcessingError } from \"../models/index.ts\";\nimport { rawTradeDataSchema, tradeSchema } from \"../models/validators.ts\";\nimport { CSVParser, type ParseProgress } from \"./csv-parser.ts\";\nimport { findMissingHeaders, normalizeHeaders } from \"../utils/csv-headers.ts\";\n\n/**\n * Set of known trade column names (canonical names from TRADE_COLUMN_MAPPING)\n * Used to identify custom columns that should be preserved\n */\nconst KNOWN_TRADE_COLUMNS = new Set([\n ...Object.keys(TRADE_COLUMN_MAPPING),\n ...Object.keys(TRADE_COLUMN_ALIASES),\n]);\n\n/**\n * Trade processing configuration\n */\nexport interface TradeProcessingConfig {\n maxTrades?: number;\n strictValidation?: boolean;\n progressCallback?: (progress: TradeProcessingProgress) => void;\n}\n\n/**\n * Trade processing progress\n */\nexport interface TradeProcessingProgress extends ParseProgress {\n stage: \"reading\" | \"parsing\" | \"validating\" | \"converting\" | \"completed\";\n validTrades: number;\n invalidTrades: number;\n}\n\n/**\n * Trade processing result\n */\nexport interface TradeProcessingResult {\n trades: Trade[];\n totalRows: number;\n validTrades: number;\n invalidTrades: number;\n errors: ProcessingError[];\n warnings: string[];\n stats: {\n processingTimeMs: number;\n strategies: string[];\n dateRange: { start: Date | null; end: Date | null };\n totalPL: number;\n };\n}\n\n/**\n * Trade processor class\n */\nexport class TradeProcessor {\n private config: Required<TradeProcessingConfig>;\n\n constructor(config: TradeProcessingConfig = {}) {\n this.config = {\n maxTrades: 50000,\n strictValidation: false,\n progressCallback: () => {},\n ...config,\n };\n }\n\n /**\n * Process trade log file\n */\n async processFile(file: File): Promise<TradeProcessingResult> {\n const startTime = Date.now();\n const errors: ProcessingError[] = [];\n const warnings: string[] = [];\n\n try {\n // Validate file\n const fileValidation = CSVParser.validateCSVFile(file);\n if (!fileValidation.valid) {\n throw new Error(fileValidation.error);\n }\n\n // Configure CSV parser\n const csvParser = new CSVParser({\n maxRows: this.config.maxTrades,\n progressCallback: (progress, rowsProcessed) => {\n this.config.progressCallback({\n stage: \"parsing\",\n progress,\n rowsProcessed,\n totalRows: 0,\n errors: 0,\n validTrades: 0,\n invalidTrades: 0,\n });\n },\n });\n\n // Parse CSV with validation\n const parseResult = await csvParser.parseFileObject(\n file,\n (row, rowIndex) => this.validateRawTradeData(row, rowIndex),\n (progress) => {\n this.config.progressCallback({\n ...progress,\n validTrades: 0,\n invalidTrades: 0,\n });\n },\n );\n\n // Collect parsing errors\n errors.push(...parseResult.errors);\n warnings.push(...parseResult.warnings);\n\n // Check for required columns\n const normalizedHeaders = normalizeHeaders(parseResult.headers, TRADE_COLUMN_ALIASES);\n const missingColumns = findMissingHeaders(normalizedHeaders, REQUIRED_TRADE_COLUMNS);\n if (missingColumns.length > 0) {\n throw new Error(`Missing required columns: ${missingColumns.join(\", \")}`);\n }\n\n // Update progress for conversion stage\n this.config.progressCallback({\n stage: \"converting\",\n progress: 0,\n rowsProcessed: 0,\n totalRows: parseResult.data.length,\n errors: errors.length,\n validTrades: 0,\n invalidTrades: 0,\n });\n\n // Convert validated data to Trade objects\n const trades: Trade[] = [];\n let validTrades = 0;\n let invalidTrades = 0;\n\n for (let i = 0; i < parseResult.data.length; i++) {\n try {\n const trade = this.convertToTrade(parseResult.data[i]);\n trades.push(trade);\n validTrades++;\n } catch (error) {\n invalidTrades++;\n const errorMessage = `Trade conversion failed at row ${i + 2}: ${error instanceof Error ? error.message : String(error)}`;\n\n // Log conversion errors to console for debugging\n console.warn(`[TradeProcessor] ${errorMessage}`);\n\n const validationError: ValidationError = {\n type: \"validation\",\n message: errorMessage,\n details: { row: parseResult.data[i], rowIndex: i + 2 },\n field: \"unknown\",\n value: parseResult.data[i],\n expected: \"Valid trade data\",\n };\n errors.push(validationError);\n\n if (!this.config.strictValidation) {\n continue; // Skip invalid row in non-strict mode\n } else {\n throw error; // Fail fast in strict mode\n }\n }\n\n // Update progress\n if (i % 100 === 0 || i === parseResult.data.length - 1) {\n const progress = Math.round((i / parseResult.data.length) * 100);\n this.config.progressCallback({\n stage: \"converting\",\n progress,\n rowsProcessed: i + 1,\n totalRows: parseResult.data.length,\n errors: errors.length,\n validTrades,\n invalidTrades,\n });\n }\n }\n\n // Sort trades for consistent ordering (handles simultaneous trades)\n trades.sort((a, b) => {\n const dateCompare = new Date(a.dateOpened).getTime() - new Date(b.dateOpened).getTime();\n if (dateCompare !== 0) return dateCompare;\n\n // Secondary sort by time\n const timeCompare = a.timeOpened.localeCompare(b.timeOpened);\n if (timeCompare !== 0) return timeCompare;\n\n // Tertiary sort by funds_at_close (lower first for simultaneous trades)\n return a.fundsAtClose - b.fundsAtClose;\n });\n\n // Calculate statistics\n const processingTimeMs = Date.now() - startTime;\n const strategies = Array.from(new Set(trades.map((t) => t.strategy))).sort();\n const dates = trades.map((t) => new Date(t.dateOpened));\n const dateRange = {\n start: dates.length > 0 ? new Date(Math.min(...dates.map((d) => d.getTime()))) : null,\n end: dates.length > 0 ? new Date(Math.max(...dates.map((d) => d.getTime()))) : null,\n };\n const totalPL = trades.reduce((sum, t) => sum + t.pl, 0);\n\n // Final progress update\n this.config.progressCallback({\n stage: \"completed\",\n progress: 100,\n rowsProcessed: parseResult.data.length,\n totalRows: parseResult.data.length,\n errors: errors.length,\n validTrades,\n invalidTrades,\n });\n\n return {\n trades,\n totalRows: parseResult.totalRows,\n validTrades,\n invalidTrades,\n errors,\n warnings,\n stats: {\n processingTimeMs,\n strategies,\n dateRange,\n totalPL,\n },\n };\n } catch (error) {\n const processingError: ProcessingError = {\n type: \"parsing\",\n message: `Trade processing failed: ${error instanceof Error ? error.message : String(error)}`,\n details: { fileName: file.name, fileSize: file.size },\n };\n\n return {\n trades: [],\n totalRows: 0,\n validTrades: 0,\n invalidTrades: 0,\n errors: [processingError, ...errors],\n warnings,\n stats: {\n processingTimeMs: Date.now() - startTime,\n strategies: [],\n dateRange: { start: null, end: null },\n totalPL: 0,\n },\n };\n }\n }\n\n /**\n * Validate raw trade data from CSV\n */\n private validateRawTradeData(\n row: Record<string, string>,\n rowIndex: number,\n ): Record<string, string> | null {\n try {\n // Apply column aliases to normalize variations\n const normalizedRow = { ...row };\n Object.entries(TRADE_COLUMN_ALIASES).forEach(([alias, canonical]) => {\n if (normalizedRow[alias] !== undefined) {\n normalizedRow[canonical] = normalizedRow[alias];\n delete normalizedRow[alias];\n }\n });\n\n // OptionOmega sometimes leaves strategy blank; default to Unknown so downstream stats still work\n if (!normalizedRow[\"Strategy\"] || normalizedRow[\"Strategy\"].trim() === \"\") {\n normalizedRow[\"Strategy\"] = \"Unknown\";\n }\n\n // Ensure required columns have values\n const requiredFields = [\"Date Opened\", \"P/L\", \"Strategy\"];\n for (const field of requiredFields) {\n if (!normalizedRow[field] || normalizedRow[field].trim() === \"\") {\n throw new Error(`Missing required field: ${field}`);\n }\n }\n\n // Set default values for missing optional fields\n if (!normalizedRow[\"Opening Commissions + Fees\"]) {\n normalizedRow[\"Opening Commissions + Fees\"] = \"0\";\n }\n if (!normalizedRow[\"Closing Commissions + Fees\"]) {\n normalizedRow[\"Closing Commissions + Fees\"] = \"0\";\n }\n if (!normalizedRow[\"Opening Short/Long Ratio\"]) {\n normalizedRow[\"Opening Short/Long Ratio\"] = \"0\";\n }\n\n const optionalNumericFieldsWithDefaultZero = [\n \"Opening VIX\",\n \"Closing VIX\",\n \"Gap\",\n \"Movement\",\n ] as const;\n optionalNumericFieldsWithDefaultZero.forEach((field) => {\n if (!normalizedRow[field] || normalizedRow[field].trim() === \"\") {\n normalizedRow[field] = \"0\";\n }\n });\n\n // Basic format validation (detailed validation happens in conversion)\n rawTradeDataSchema.parse(normalizedRow);\n\n return normalizedRow;\n } catch (error) {\n // Log validation errors to console for debugging\n console.warn(\n `[TradeProcessor] Row ${rowIndex + 2} validation failed:`,\n error instanceof Error ? error.message : error,\n );\n // Return null for invalid rows - they'll be counted as invalid\n return null;\n }\n }\n\n /**\n * Parse a YYYY-MM-DD date string preserving the calendar date.\n *\n * Option Omega exports dates in Eastern time. JavaScript's new Date('YYYY-MM-DD')\n * parses as UTC midnight, which when converted to local time can shift to the\n * previous day (e.g., Dec 11 UTC → Dec 10 7pm EST).\n *\n * This method creates a Date representing midnight local time on the specified\n * calendar date, so Dec 11 in the CSV becomes Dec 11 in the app regardless of timezone.\n */\n private 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 // Create date at midnight local time - this preserves the calendar date\n return new Date(parseInt(year), parseInt(month) - 1, parseInt(day));\n }\n // Fall back to default parsing for other formats\n return new Date(dateStr);\n }\n\n /**\n * Convert validated CSV row to Trade object\n */\n private convertToTrade(rawData: Record<string, string>): Trade {\n try {\n // Parse dates preserving calendar day\n const dateOpened = this.parseDatePreservingCalendarDay(rawData[\"Date Opened\"]);\n if (isNaN(dateOpened.getTime())) {\n throw new Error(`Invalid Date Opened: ${rawData[\"Date Opened\"]}`);\n }\n\n const dateClosed = rawData[\"Date Closed\"]\n ? this.parseDatePreservingCalendarDay(rawData[\"Date Closed\"])\n : undefined;\n if (dateClosed && isNaN(dateClosed.getTime())) {\n throw new Error(`Invalid Date Closed: ${rawData[\"Date Closed\"]}`);\n }\n\n // Normalize strategy name (handle empty strings)\n const strategy = (rawData[\"Strategy\"] || \"\").trim() || \"Unknown\";\n\n // Parse numeric values with error handling and NaN handling\n const parseNumber = (\n value: string | undefined,\n fieldName: string,\n defaultValue?: number,\n ): number => {\n if (!value || value.trim() === \"\" || value.toLowerCase() === \"nan\") {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Missing required numeric field: ${fieldName}`);\n }\n\n // Remove currency symbols and commas\n const cleaned = value.replace(/[$,]/g, \"\").trim();\n const parsed = parseFloat(cleaned);\n\n if (isNaN(parsed)) {\n if (defaultValue !== undefined) return defaultValue;\n throw new Error(`Invalid numeric value for ${fieldName}: ${value}`);\n }\n\n return parsed;\n };\n\n // P/L % is optional: a blank or unparseable cell leaves the trade on its computed value\n // instead of rejecting a row whose required fields all loaded.\n const reportedPlPct = parseNumber(rawData[\"P/L %\"], \"P/L %\", NaN);\n\n const trade: Trade = {\n dateOpened,\n timeOpened: rawData[\"Time Opened\"] || \"00:00:00\",\n openingPrice: parseNumber(rawData[\"Opening Price\"], \"Opening Price\"),\n legs: rawData[\"Legs\"] || \"\",\n premium: parseNumber(rawData[\"Premium\"], \"Premium\"),\n closingPrice: rawData[\"Closing Price\"]\n ? parseNumber(rawData[\"Closing Price\"], \"Closing Price\")\n : undefined,\n dateClosed,\n timeClosed: rawData[\"Time Closed\"] || undefined,\n avgClosingCost: rawData[\"Avg. Closing Cost\"]\n ? parseNumber(rawData[\"Avg. Closing Cost\"], \"Avg. Closing Cost\")\n : undefined,\n reasonForClose: rawData[\"Reason For Close\"] || undefined,\n pl: parseNumber(rawData[\"P/L\"], \"P/L\"),\n plBasis: PlBasis.NetIncludesFees,\n plPct: Number.isFinite(reportedPlPct) ? reportedPlPct : undefined,\n numContracts: Math.round(parseNumber(rawData[\"No. of Contracts\"], \"No. of Contracts\")),\n fundsAtClose: parseNumber(rawData[\"Funds at Close\"], \"Funds at Close\"),\n marginReq: parseNumber(rawData[\"Margin Req.\"], \"Margin Req.\"),\n strategy,\n openingCommissionsFees: parseNumber(\n rawData[\"Opening Commissions + Fees\"],\n \"Opening Commissions\",\n 0,\n ),\n closingCommissionsFees: parseNumber(\n rawData[\"Closing Commissions + Fees\"],\n \"Closing Commissions\",\n 0,\n ),\n openingShortLongRatio: parseNumber(\n rawData[\"Opening Short/Long Ratio\"],\n \"Opening Short/Long Ratio\",\n 0,\n ),\n closingShortLongRatio: rawData[\"Closing Short/Long Ratio\"]\n ? parseNumber(rawData[\"Closing Short/Long Ratio\"], \"Closing Short/Long Ratio\")\n : undefined,\n openingVix: rawData[\"Opening VIX\"]\n ? parseNumber(rawData[\"Opening VIX\"], \"Opening VIX\")\n : undefined,\n closingVix: rawData[\"Closing VIX\"]\n ? parseNumber(rawData[\"Closing VIX\"], \"Closing VIX\")\n : undefined,\n gap: rawData[\"Gap\"] ? parseNumber(rawData[\"Gap\"], \"Gap\") : undefined,\n movement: rawData[\"Movement\"] ? parseNumber(rawData[\"Movement\"], \"Movement\") : undefined,\n maxProfit: rawData[\"Max Profit\"]\n ? parseNumber(rawData[\"Max Profit\"], \"Max Profit\")\n : undefined,\n maxLoss: rawData[\"Max Loss\"] ? parseNumber(rawData[\"Max Loss\"], \"Max Loss\") : undefined,\n };\n\n // Extract custom fields (columns not in KNOWN_TRADE_COLUMNS)\n const customFields: Record<string, number | string> = {};\n for (const [key, value] of Object.entries(rawData)) {\n if (!KNOWN_TRADE_COLUMNS.has(key) && value !== undefined && value.trim() !== \"\") {\n // Auto-detect type: try to parse as number\n const cleaned = value.replace(/[$,%]/g, \"\").trim();\n const parsed = parseFloat(cleaned);\n if (!isNaN(parsed) && isFinite(parsed)) {\n customFields[key] = parsed;\n } else {\n customFields[key] = value.trim();\n }\n }\n }\n\n // Only add customFields if there are any\n if (Object.keys(customFields).length > 0) {\n trade.customFields = customFields;\n }\n\n // Final validation with Zod schema\n const validatedTrade = tradeSchema.parse(trade);\n return validatedTrade;\n } catch (error) {\n throw new Error(\n `Trade conversion failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n /**\n * Process CSV content directly (for testing)\n */\n async processCSVContent(content: string): Promise<TradeProcessingResult> {\n // Create a mock File object for testing\n const blob = new Blob([content], { type: \"text/csv\" });\n const file = new File([blob], \"test.csv\", { type: \"text/csv\" });\n return this.processFile(file);\n }\n}\n"],"mappings":"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","names":[]}