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.
- package/README.md +208 -23
- package/dist/{chunk-TKUG3FOR.js → chunk-BH6SQOWF.js} +23 -5
- package/dist/chunk-BH6SQOWF.js.map +1 -0
- package/dist/{chunk-3KDAY4J4.js → chunk-D4AWELQU.js} +8 -9
- package/{server/chunk-35ZZUXY7.js.map → dist/chunk-D4AWELQU.js.map} +1 -1
- package/dist/{chunk-XANRFYQT.js → chunk-HQQNVMYF.js} +6340 -5569
- package/dist/chunk-HQQNVMYF.js.map +1 -0
- package/dist/{chunk-5SZL2WXB.js → chunk-IW75IC6I.js} +5 -32
- package/dist/chunk-IW75IC6I.js.map +1 -0
- package/dist/chunk-XF3F3S7D.js +5674 -0
- package/dist/chunk-XF3F3S7D.js.map +1 -0
- package/dist/chunk-YQMQ3PI4.js +5833 -0
- package/dist/chunk-YQMQ3PI4.js.map +1 -0
- package/dist/{chunk-Q4ZODPP6.js → chunk-ZSVYX5PP.js} +6 -23
- package/dist/chunk-ZSVYX5PP.js.map +1 -0
- package/dist/daily-log-processor-T2QMVGBA.js +9 -0
- package/dist/daily-log-processor-ZFTGQDOD.js +10 -0
- package/dist/{daily-logs-store-3FVXJCAS.js → daily-logs-store-LRZBF2UA.js} +3 -5
- package/dist/{daily-logs-store-X4IAX7S5.js → daily-logs-store-UNX5DAT7.js} +3 -5
- package/dist/market/provenance/index.d.ts +5 -18
- package/dist/market/provenance/index.js +566 -18905
- package/dist/market/provenance/index.js.map +1 -1
- package/dist/{sync-DVSXIIUZ.js → sync-I2S5TFQS.js} +7 -8
- package/dist/test-exports.js +4011 -712
- package/dist/test-exports.js.map +1 -1
- package/dist/trade-processor-6QSOAWJN.js +9 -0
- package/dist/trade-processor-VJ3XUDTH.js +10 -0
- package/dist/{trades-store-HDB2Q3EC.js → trades-store-O4ETTDGU.js} +3 -5
- package/dist/{trades-store-5SLXLMGH.js → trades-store-P6QZQMAD.js} +3 -5
- package/package.json +6 -6
- package/server/{chunk-4AGMJIIZ.js → chunk-24O66TM5.js} +6 -23
- package/server/chunk-24O66TM5.js.map +1 -0
- package/server/{chunk-N2RXXORM.js → chunk-2E3K452Q.js} +23 -5
- package/server/chunk-2E3K452Q.js.map +1 -0
- package/server/chunk-3XKPTACI.js +6035 -0
- package/server/chunk-3XKPTACI.js.map +1 -0
- package/server/{chunk-35ZZUXY7.js → chunk-76OEJGQX.js} +8 -9
- package/{dist/chunk-3KDAY4J4.js.map → server/chunk-76OEJGQX.js.map} +1 -1
- package/server/{chunk-I6FEESXT.js → chunk-JI2QLM6W.js} +25413 -24241
- package/server/chunk-JI2QLM6W.js.map +1 -0
- package/server/chunk-MCT6A2WW.js +593 -0
- package/server/chunk-MCT6A2WW.js.map +1 -0
- package/server/{chunk-YKGDAXGE.js → chunk-NLYLE5C6.js} +5 -32
- package/server/chunk-NLYLE5C6.js.map +1 -0
- package/server/cli.d.ts +2 -0
- package/server/cli.js +62 -0
- package/server/cli.js.map +1 -0
- package/server/daily-log-processor-NXSUFX32.js +11 -0
- package/server/{daily-logs-store-GK6B3X2V.js → daily-logs-store-EMWH6MR4.js} +3 -5
- package/server/index.d.ts +17 -7
- package/server/index.js +4579 -1771
- package/server/index.js.map +1 -1
- package/server/setup-4EJSSDL4.js +9 -0
- package/server/{sync-MW333YEK.js → sync-NNHKLGNZ.js} +7 -8
- package/server/trade-processor-NYDJ3VMH.js +11 -0
- package/server/{trades-store-QH6Z3OYG.js → trades-store-3QYMUVA5.js} +3 -5
- package/src/cli.ts +63 -0
- package/src/db/connection.ts +5 -6
- package/src/db/data-root.ts +1 -1
- package/src/index.ts +45 -20
- package/src/market/provenance/content-object-store.ts +6 -46
- package/src/market/provenance/durable-directory.ts +92 -0
- package/src/market/provenance/index.ts +1 -0
- package/src/market/provenance/partition-commit-store.ts +26 -62
- package/src/market/provenance/xnys-calendar-data.ts +117 -0
- package/src/market/provenance/xnys-session-calendar.ts +35 -138
- package/src/market/published-rates.ts +52 -0
- package/src/market/stores/duckdb-quote-store.ts +11 -3
- package/src/market/stores/duckdb-spot-store.ts +2 -2
- package/src/market/stores/parquet-quote-store.ts +11 -3
- package/src/market/stores/parquet-spot-store.ts +7 -2
- package/src/market/stores/quote-store.ts +8 -1
- package/src/market/stores/spot-sql.ts +20 -2
- package/src/market/stores/spot-store.ts +9 -3
- package/src/plugins.ts +6 -0
- package/src/prompts.ts +393 -0
- package/src/setup.ts +708 -0
- package/src/sync/block-sync.ts +2 -1
- package/src/test-exports.ts +8 -1
- package/src/tools/analysis.ts +16 -56
- package/src/tools/blocks/analysis.ts +25 -243
- package/src/tools/blocks/comparison.ts +8 -4
- package/src/tools/blocks/core.ts +61 -32
- package/src/tools/blocks/paired-comparison.ts +7 -20
- package/src/tools/blocks/similarity.ts +13 -161
- package/src/tools/blocks.ts +1 -1
- package/src/tools/edge-decay.ts +1 -0
- package/src/tools/exit-analysis.ts +6 -1
- package/src/tools/greeks-attribution.ts +11 -14
- package/src/tools/imports.ts +58 -42
- package/src/tools/middleware/strict-arguments.ts +67 -0
- package/src/tools/performance.ts +46 -694
- package/src/tools/replay.ts +36 -6
- package/src/tools/reports/fields.ts +7 -39
- package/src/tools/reports/helpers.ts +16 -40
- package/src/tools/reports/predictive.ts +12 -122
- package/src/tools/reports.ts +4 -3
- package/src/tools/sql.ts +8 -3
- package/src/utils/block-loader.ts +254 -122
- package/src/utils/csv-discovery.ts +1 -3
- package/src/utils/data-availability.ts +2 -3
- package/src/utils/greeks-decomposition.ts +90 -53
- package/src/utils/market-enricher.ts +93 -115
- package/src/utils/option-quote-greeks.ts +10 -6
- package/src/utils/providers/massive.ts +2 -3
- package/src/utils/providers/thetadata/backfill.ts +12 -20
- package/src/utils/quote-parquet-projection.ts +13 -7
- package/src/utils/schema-metadata.ts +3 -2
- package/src/utils/starting-capital.ts +38 -0
- package/src/utils/trade-replay.ts +44 -0
- package/dist/chunk-4VWUMBAZ.js +0 -859
- package/dist/chunk-4VWUMBAZ.js.map +0 -1
- package/dist/chunk-5SZL2WXB.js.map +0 -1
- package/dist/chunk-AH7VIHDT.js +0 -329
- package/dist/chunk-AH7VIHDT.js.map +0 -1
- package/dist/chunk-COSLRCST.js +0 -3374
- package/dist/chunk-COSLRCST.js.map +0 -1
- package/dist/chunk-DJRGJXYP.js +0 -453
- package/dist/chunk-DJRGJXYP.js.map +0 -1
- package/dist/chunk-P3IETL3K.js +0 -93
- package/dist/chunk-P3IETL3K.js.map +0 -1
- package/dist/chunk-PS2XIVFN.js +0 -359
- package/dist/chunk-PS2XIVFN.js.map +0 -1
- package/dist/chunk-Q4ZODPP6.js.map +0 -1
- package/dist/chunk-TEJVVTIM.js +0 -912
- package/dist/chunk-TEJVVTIM.js.map +0 -1
- package/dist/chunk-TKUG3FOR.js.map +0 -1
- package/dist/chunk-TZRDK5VL.js +0 -5626
- package/dist/chunk-TZRDK5VL.js.map +0 -1
- package/dist/chunk-XANRFYQT.js.map +0 -1
- package/dist/daily-log-processor-DMJA7D2X.js +0 -10
- package/dist/daily-log-processor-IBZMMS72.js +0 -9
- package/dist/market-provider-FWKON2ZG.js +0 -17
- package/dist/trade-processor-DQMN7JZF.js +0 -10
- package/dist/trade-processor-EWPJXBZZ.js +0 -9
- package/server/chunk-4AGMJIIZ.js.map +0 -1
- package/server/chunk-GKB4EQFU.js +0 -5317
- package/server/chunk-GKB4EQFU.js.map +0 -1
- package/server/chunk-I6FEESXT.js.map +0 -1
- package/server/chunk-N2RXXORM.js.map +0 -1
- package/server/chunk-RY37QPQJ.js +0 -3127
- package/server/chunk-RY37QPQJ.js.map +0 -1
- package/server/chunk-YKGDAXGE.js.map +0 -1
- package/server/chunk-YQZDWXAU.js +0 -938
- package/server/chunk-YQZDWXAU.js.map +0 -1
- package/server/daily-log-processor-XNMWJNJ6.js +0 -11
- package/server/market-provider-I6JMSBIK.js +0 -18
- package/server/trade-processor-TO27NFQW.js +0 -11
- package/server/trades-store-QH6Z3OYG.js.map +0 -1
- package/src/tools/blocks/stress-scenarios.ts +0 -72
- /package/dist/{daily-log-processor-DMJA7D2X.js.map → daily-log-processor-T2QMVGBA.js.map} +0 -0
- /package/dist/{daily-log-processor-IBZMMS72.js.map → daily-log-processor-ZFTGQDOD.js.map} +0 -0
- /package/dist/{daily-logs-store-3FVXJCAS.js.map → daily-logs-store-LRZBF2UA.js.map} +0 -0
- /package/dist/{daily-logs-store-X4IAX7S5.js.map → daily-logs-store-UNX5DAT7.js.map} +0 -0
- /package/dist/{market-provider-FWKON2ZG.js.map → sync-I2S5TFQS.js.map} +0 -0
- /package/dist/{sync-DVSXIIUZ.js.map → trade-processor-6QSOAWJN.js.map} +0 -0
- /package/dist/{trade-processor-DQMN7JZF.js.map → trade-processor-VJ3XUDTH.js.map} +0 -0
- /package/dist/{trade-processor-EWPJXBZZ.js.map → trades-store-O4ETTDGU.js.map} +0 -0
- /package/dist/{trades-store-5SLXLMGH.js.map → trades-store-P6QZQMAD.js.map} +0 -0
- /package/{dist/trades-store-HDB2Q3EC.js.map → server/daily-log-processor-NXSUFX32.js.map} +0 -0
- /package/server/{daily-log-processor-XNMWJNJ6.js.map → daily-logs-store-EMWH6MR4.js.map} +0 -0
- /package/server/{daily-logs-store-GK6B3X2V.js.map → setup-4EJSSDL4.js.map} +0 -0
- /package/server/{market-provider-I6JMSBIK.js.map → sync-NNHKLGNZ.js.map} +0 -0
- /package/server/{sync-MW333YEK.js.map → trade-processor-NYDJ3VMH.js.map} +0 -0
- /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/
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
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 [
|
|
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 (
|
|
301
|
-
|
|
302
|
-
-
|
|
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
|
|
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](
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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":[]}
|