opentradex 0.1.3 → 0.1.4

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/src/index.mjs CHANGED
@@ -6,10 +6,14 @@ import process from "node:process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import {
9
+ CHANNEL_OPTIONS,
10
+ DASHBOARD_SURFACE_OPTIONS,
9
11
  INTEGRATION_OPTIONS,
12
+ MCP_TRANSPORT_OPTIONS,
10
13
  MARKET_OPTIONS,
11
14
  PACKAGE_MANAGER_OPTIONS,
12
15
  RUNTIME_OPTIONS,
16
+ TRADINGVIEW_CONNECTOR_OPTIONS,
13
17
  formatOptionLines,
14
18
  getOptionById,
15
19
  keepKnownIds,
@@ -49,6 +53,8 @@ const DEFAULT_ENV = {
49
53
  OPENTRADEX_ENABLED_MARKETS: "kalshi",
50
54
  OPENTRADEX_ENABLED_INTEGRATIONS: "apify,rss",
51
55
  OPENTRADEX_LIVE_EXECUTION_MARKET: "kalshi",
56
+ OPENTRADEX_DASHBOARD_SURFACE: "chat",
57
+ OPENTRADEX_CHANNELS: "command,markets,feeds,risk,execution",
52
58
  KALSHI_API_KEY_ID: "",
53
59
  KALSHI_PRIVATE_KEY_PATH: "",
54
60
  KALSHI_USE_DEMO: "true",
@@ -58,6 +64,12 @@ const DEFAULT_ENV = {
58
64
  TRADINGVIEW_USERNAME: "",
59
65
  TRADINGVIEW_PASSWORD: "",
60
66
  TRADINGVIEW_WATCHLIST: "SPY,QQQ,BTCUSD,NQ1!",
67
+ TRADINGVIEW_CONNECTOR_MODE: "watchlist",
68
+ TRADINGVIEW_MCP_ENABLED: "false",
69
+ TRADINGVIEW_MCP_TRANSPORT: "stdio",
70
+ TRADINGVIEW_MCP_COMMAND: "",
71
+ TRADINGVIEW_MCP_ARGS: "",
72
+ TRADINGVIEW_MCP_URL: "",
61
73
  ROBINHOOD_USERNAME: "",
62
74
  ROBINHOOD_PASSWORD: "",
63
75
  ROBINHOOD_MFA_CODE: "",
@@ -111,6 +123,11 @@ export function writeWorkspaceProfile(workspace, profile) {
111
123
  primaryMarket: profile.primaryMarket,
112
124
  enabledMarkets: uniqueIds(profile.enabledMarkets),
113
125
  integrations: uniqueIds(profile.integrations),
126
+ dashboardSurface: profile.dashboardSurface,
127
+ channels: uniqueIds(profile.channels),
128
+ tradingviewConnectorMode: profile.tradingviewConnectorMode,
129
+ tradingviewMcpEnabled: Boolean(profile.tradingviewMcpEnabled),
130
+ tradingviewMcpTransport: profile.tradingviewMcpTransport,
114
131
  bankroll: Number(profile.bankroll).toFixed(2),
115
132
  interval: Number(profile.interval),
116
133
  installDeps: Boolean(profile.installDeps),
@@ -179,6 +196,8 @@ export function writeEnvFile(workspace, overrides = {}) {
179
196
  "OPENTRADEX_ENABLED_MARKETS",
180
197
  "OPENTRADEX_ENABLED_INTEGRATIONS",
181
198
  "OPENTRADEX_LIVE_EXECUTION_MARKET",
199
+ "OPENTRADEX_DASHBOARD_SURFACE",
200
+ "OPENTRADEX_CHANNELS",
182
201
  "NEWS_PROVIDER",
183
202
  ],
184
203
  },
@@ -214,6 +233,12 @@ export function writeEnvFile(workspace, overrides = {}) {
214
233
  "TRADINGVIEW_USERNAME",
215
234
  "TRADINGVIEW_PASSWORD",
216
235
  "TRADINGVIEW_WATCHLIST",
236
+ "TRADINGVIEW_CONNECTOR_MODE",
237
+ "TRADINGVIEW_MCP_ENABLED",
238
+ "TRADINGVIEW_MCP_TRANSPORT",
239
+ "TRADINGVIEW_MCP_COMMAND",
240
+ "TRADINGVIEW_MCP_ARGS",
241
+ "TRADINGVIEW_MCP_URL",
217
242
  ],
218
243
  },
219
244
  {
@@ -293,6 +318,11 @@ export function collectDoctor(workspaceArg) {
293
318
  const runtime = config.runtime || env.OPENTRADEX_RUNTIME || "claude-code";
294
319
  const packageManager = config.packageManager || env.OPENTRADEX_PACKAGE_MANAGER || "npm";
295
320
  const primaryMarket = config.primaryMarket || env.OPENTRADEX_PRIMARY_MARKET || "kalshi";
321
+ const dashboardSurface = config.dashboardSurface || env.OPENTRADEX_DASHBOARD_SURFACE || DEFAULT_ENV.OPENTRADEX_DASHBOARD_SURFACE;
322
+ const channels = keepKnownIds(
323
+ CHANNEL_OPTIONS,
324
+ config.channels?.length ? config.channels : env.OPENTRADEX_CHANNELS || DEFAULT_ENV.OPENTRADEX_CHANNELS
325
+ );
296
326
  const enabledMarkets = keepKnownIds(
297
327
  MARKET_OPTIONS,
298
328
  config.enabledMarkets?.length ? config.enabledMarkets : env.OPENTRADEX_ENABLED_MARKETS || primaryMarket
@@ -301,6 +331,16 @@ export function collectDoctor(workspaceArg) {
301
331
  INTEGRATION_OPTIONS,
302
332
  config.integrations?.length ? config.integrations : env.OPENTRADEX_ENABLED_INTEGRATIONS || "apify,rss"
303
333
  );
334
+ const tradingviewConnectorMode =
335
+ String(config.tradingviewConnectorMode || env.TRADINGVIEW_CONNECTOR_MODE || DEFAULT_ENV.TRADINGVIEW_CONNECTOR_MODE).toLowerCase() === "mcp"
336
+ ? "mcp"
337
+ : "watchlist";
338
+ const tradingviewMcpEnabled =
339
+ String(config.tradingviewMcpEnabled ?? env.TRADINGVIEW_MCP_ENABLED ?? DEFAULT_ENV.TRADINGVIEW_MCP_ENABLED).toLowerCase() === "true";
340
+ const tradingviewMcpTransport =
341
+ String(config.tradingviewMcpTransport || env.TRADINGVIEW_MCP_TRANSPORT || DEFAULT_ENV.TRADINGVIEW_MCP_TRANSPORT).toLowerCase() === "http"
342
+ ? "http"
343
+ : "stdio";
304
344
 
305
345
  const checks = [
306
346
  check("Node.js", commandWorks("node", ["-v"]), "Install Node.js 22+."),
@@ -354,6 +394,17 @@ export function collectDoctor(workspaceArg) {
354
394
  "Add TRADINGVIEW_WATCHLIST so the agent has symbols to monitor."
355
395
  )
356
396
  );
397
+ if (tradingviewConnectorMode === "mcp" || tradingviewMcpEnabled) {
398
+ checks.push(
399
+ check(
400
+ "TradingView MCP",
401
+ tradingviewMcpTransport === "http" ? Boolean(env.TRADINGVIEW_MCP_URL) : Boolean(env.TRADINGVIEW_MCP_COMMAND),
402
+ tradingviewMcpTransport === "http"
403
+ ? "Set TRADINGVIEW_MCP_URL to your TradingView MCP endpoint."
404
+ : "Set TRADINGVIEW_MCP_COMMAND and optional TRADINGVIEW_MCP_ARGS for your local TradingView MCP server."
405
+ )
406
+ );
407
+ }
357
408
  }
358
409
 
359
410
  if (integrations.includes("apify") || integrations.includes("twitter") || integrations.includes("tiktok")) {
@@ -365,13 +416,28 @@ export function collectDoctor(workspaceArg) {
365
416
  profile: {
366
417
  runtime,
367
418
  packageManager,
419
+ dashboardSurface,
420
+ channels,
368
421
  mode: liveTrading ? "live" : config.mode || env.OPENTRADEX_EXECUTION_MODE || "paper",
369
422
  primaryMarket,
370
423
  enabledMarkets,
371
424
  integrations,
425
+ tradingviewConnectorMode,
426
+ tradingviewMcpEnabled,
427
+ tradingviewMcpTransport,
372
428
  },
373
429
  checks,
374
- notes: buildDoctorNotes({ runtime, primaryMarket, enabledMarkets, liveTrading }),
430
+ notes: buildDoctorNotes({
431
+ runtime,
432
+ primaryMarket,
433
+ enabledMarkets,
434
+ liveTrading,
435
+ dashboardSurface,
436
+ channels,
437
+ tradingviewConnectorMode,
438
+ tradingviewMcpEnabled,
439
+ tradingviewMcpTransport,
440
+ }),
375
441
  };
376
442
  }
377
443
 
@@ -384,6 +450,15 @@ export async function onboard(options = {}) {
384
450
  try {
385
451
  printWelcomeBanner();
386
452
 
453
+ if (interactive) {
454
+ printSecurityNotice();
455
+ const accepted = await confirm(rl, "I understand this is powerful and inherently risky. Continue?", false);
456
+ if (!accepted) {
457
+ return null;
458
+ }
459
+ printSection("1/7", "workspace", "Choose where OpenTradex should create or reuse the local harness.");
460
+ }
461
+
387
462
  const workspace = path.resolve(
388
463
  options.workspace ||
389
464
  (interactive
@@ -396,11 +471,20 @@ export async function onboard(options = {}) {
396
471
  ...saved,
397
472
  ...readWorkspaceProfile(workspace),
398
473
  });
474
+ const workspaceExists = existsSync(path.join(workspace, "main.py"));
475
+
476
+ if (interactive && workspaceExists && !(await confirm(rl, `Reuse existing workspace at ${workspace}`, true))) {
477
+ return null;
478
+ }
479
+
480
+ if (interactive) {
481
+ printSection("2/7", "runtime", "Pick the operator runtime and package flow for this machine.");
482
+ }
399
483
 
400
484
  const runtime = interactive
401
485
  ? await chooseOption(
402
486
  rl,
403
- "Choose your agent runtime",
487
+ "Select operator runtime",
404
488
  RUNTIME_OPTIONS,
405
489
  options.llm || options.runtime || savedProfile.runtime || currentEnv.OPENTRADEX_RUNTIME || "claude-code"
406
490
  )
@@ -409,7 +493,7 @@ export async function onboard(options = {}) {
409
493
  const packageManager = interactive
410
494
  ? await chooseOption(
411
495
  rl,
412
- "Choose your package manager",
496
+ "Select package manager",
413
497
  PACKAGE_MANAGER_OPTIONS,
414
498
  options.packageManager || savedProfile.packageManager || currentEnv.OPENTRADEX_PACKAGE_MANAGER || "npm"
415
499
  )
@@ -419,10 +503,14 @@ export async function onboard(options = {}) {
419
503
  "npm"
420
504
  ).id;
421
505
 
506
+ if (interactive) {
507
+ printSection("3/7", "market rails", "Pick the execution rail first, then enable any additional discovery or watchlist rails.");
508
+ }
509
+
422
510
  const primaryMarket = interactive
423
511
  ? await chooseOption(
424
512
  rl,
425
- "Choose your primary market rail",
513
+ "Select primary market",
426
514
  MARKET_OPTIONS,
427
515
  options.primaryMarket || options.market || savedProfile.primaryMarket || currentEnv.OPENTRADEX_PRIMARY_MARKET || "kalshi"
428
516
  )
@@ -442,7 +530,7 @@ export async function onboard(options = {}) {
442
530
  const extraMarkets = interactive
443
531
  ? await chooseMany(
444
532
  rl,
445
- "Enable extra market rails",
533
+ "Enable additional market rails",
446
534
  MARKET_OPTIONS.filter((item) => item.id !== primaryMarket),
447
535
  defaultMarkets
448
536
  )
@@ -450,15 +538,53 @@ export async function onboard(options = {}) {
450
538
 
451
539
  const enabledMarkets = uniqueIds([primaryMarket, ...extraMarkets]);
452
540
 
541
+ const availableChannels = enabledMarkets.includes("tradingview")
542
+ ? CHANNEL_OPTIONS
543
+ : CHANNEL_OPTIONS.filter((item) => item.id !== "tradingview");
544
+
545
+ if (interactive) {
546
+ printSection("4/7", "operator channels", "Choose the dashboard surface and the messaging lanes your operator cockpit should keep online.");
547
+ }
548
+
549
+ const dashboardSurface = interactive
550
+ ? await chooseOption(
551
+ rl,
552
+ "Select dashboard surface",
553
+ DASHBOARD_SURFACE_OPTIONS,
554
+ options.dashboardSurface || savedProfile.dashboardSurface || currentEnv.OPENTRADEX_DASHBOARD_SURFACE || DEFAULT_ENV.OPENTRADEX_DASHBOARD_SURFACE
555
+ )
556
+ : getOptionById(
557
+ DASHBOARD_SURFACE_OPTIONS,
558
+ options.dashboardSurface || savedProfile.dashboardSurface || currentEnv.OPENTRADEX_DASHBOARD_SURFACE,
559
+ DEFAULT_ENV.OPENTRADEX_DASHBOARD_SURFACE
560
+ ).id;
561
+
562
+ const defaultChannels = keepKnownIds(
563
+ availableChannels,
564
+ options.channels || savedProfile.channels || currentEnv.OPENTRADEX_CHANNELS || DEFAULT_ENV.OPENTRADEX_CHANNELS
565
+ );
566
+
567
+ const channels = interactive
568
+ ? await chooseMany(rl, "Enable operator channels", availableChannels, defaultChannels)
569
+ : defaultChannels;
570
+
453
571
  const defaultIntegrations = keepKnownIds(
454
572
  INTEGRATION_OPTIONS,
455
573
  options.integrations || savedProfile.integrations || currentEnv.OPENTRADEX_ENABLED_INTEGRATIONS || "apify,rss"
456
574
  );
457
575
 
576
+ if (interactive) {
577
+ printSection("5/7", "data feeds", "Enable only the context feeds you want. Optional integrations stay optional.");
578
+ }
579
+
458
580
  const integrations = interactive
459
581
  ? await chooseMany(rl, "Enable optional data integrations", INTEGRATION_OPTIONS, defaultIntegrations)
460
582
  : defaultIntegrations;
461
583
 
584
+ if (interactive) {
585
+ printSection("6/7", "guardrails", "Set trading mode, bankroll, and loop interval before anything gets close to execution.");
586
+ }
587
+
462
588
  const mode =
463
589
  options.live ? "live" : options.paper ? "paper" : interactive ? await chooseMode(rl, savedProfile.mode || "paper") : savedProfile.mode || "paper";
464
590
 
@@ -472,8 +598,9 @@ export async function onboard(options = {}) {
472
598
  ? await ask(rl, "Loop interval in seconds", String(savedProfile.interval || DEFAULT_ENV.CYCLE_INTERVAL))
473
599
  : savedProfile.interval || DEFAULT_ENV.CYCLE_INTERVAL);
474
600
 
475
- const installDeps =
476
- options.install ?? (options.skipInstall ? false : interactive ? await confirm(rl, "Install Python and web dependencies now", true) : true);
601
+ if (interactive) {
602
+ printSection("7/7", "credentials", "Enter only the keys and watchlists you need. Blank values stay blank unless you enable live Kalshi.");
603
+ }
477
604
 
478
605
  const apifyToken =
479
606
  options.apifyToken ??
@@ -519,6 +646,80 @@ export async function onboard(options = {}) {
519
646
  : currentEnv.TRADINGVIEW_WATCHLIST || DEFAULT_ENV.TRADINGVIEW_WATCHLIST)
520
647
  : currentEnv.TRADINGVIEW_WATCHLIST || DEFAULT_ENV.TRADINGVIEW_WATCHLIST;
521
648
 
649
+ const tradingviewConnectorMode =
650
+ enabledMarkets.includes("tradingview")
651
+ ? interactive
652
+ ? await chooseOption(
653
+ rl,
654
+ "TradingView connector mode",
655
+ TRADINGVIEW_CONNECTOR_OPTIONS,
656
+ options.tradingviewConnectorMode ||
657
+ savedProfile.tradingviewConnectorMode ||
658
+ currentEnv.TRADINGVIEW_CONNECTOR_MODE ||
659
+ DEFAULT_ENV.TRADINGVIEW_CONNECTOR_MODE
660
+ )
661
+ : getOptionById(
662
+ TRADINGVIEW_CONNECTOR_OPTIONS,
663
+ options.tradingviewConnectorMode ||
664
+ savedProfile.tradingviewConnectorMode ||
665
+ currentEnv.TRADINGVIEW_CONNECTOR_MODE,
666
+ DEFAULT_ENV.TRADINGVIEW_CONNECTOR_MODE
667
+ ).id
668
+ : currentEnv.TRADINGVIEW_CONNECTOR_MODE || DEFAULT_ENV.TRADINGVIEW_CONNECTOR_MODE;
669
+
670
+ const tradingviewMcpTransport =
671
+ enabledMarkets.includes("tradingview") && tradingviewConnectorMode === "mcp"
672
+ ? interactive
673
+ ? await chooseOption(
674
+ rl,
675
+ "TradingView MCP transport",
676
+ MCP_TRANSPORT_OPTIONS,
677
+ options.tradingviewMcpTransport ||
678
+ savedProfile.tradingviewMcpTransport ||
679
+ currentEnv.TRADINGVIEW_MCP_TRANSPORT ||
680
+ DEFAULT_ENV.TRADINGVIEW_MCP_TRANSPORT
681
+ )
682
+ : getOptionById(
683
+ MCP_TRANSPORT_OPTIONS,
684
+ options.tradingviewMcpTransport ||
685
+ savedProfile.tradingviewMcpTransport ||
686
+ currentEnv.TRADINGVIEW_MCP_TRANSPORT,
687
+ DEFAULT_ENV.TRADINGVIEW_MCP_TRANSPORT
688
+ ).id
689
+ : currentEnv.TRADINGVIEW_MCP_TRANSPORT || DEFAULT_ENV.TRADINGVIEW_MCP_TRANSPORT;
690
+
691
+ const tradingviewMcpEnabled =
692
+ enabledMarkets.includes("tradingview") && tradingviewConnectorMode === "mcp"
693
+ ? options.tradingviewMcpEnabled ??
694
+ (interactive
695
+ ? await confirm(rl, "Enable TradingView MCP for this workspace", true)
696
+ : String(savedProfile.tradingviewMcpEnabled ?? currentEnv.TRADINGVIEW_MCP_ENABLED ?? "true").toLowerCase() === "true")
697
+ : false;
698
+
699
+ const tradingviewMcpCommand =
700
+ tradingviewMcpEnabled && tradingviewMcpTransport === "stdio"
701
+ ? options.tradingviewMcpCommand ??
702
+ (interactive
703
+ ? await ask(rl, "TradingView MCP command", currentEnv.TRADINGVIEW_MCP_COMMAND || "")
704
+ : currentEnv.TRADINGVIEW_MCP_COMMAND || "")
705
+ : currentEnv.TRADINGVIEW_MCP_COMMAND || "";
706
+
707
+ const tradingviewMcpArgs =
708
+ tradingviewMcpEnabled && tradingviewMcpTransport === "stdio"
709
+ ? options.tradingviewMcpArgs ??
710
+ (interactive
711
+ ? await ask(rl, "TradingView MCP args (optional)", currentEnv.TRADINGVIEW_MCP_ARGS || "")
712
+ : currentEnv.TRADINGVIEW_MCP_ARGS || "")
713
+ : currentEnv.TRADINGVIEW_MCP_ARGS || "";
714
+
715
+ const tradingviewMcpUrl =
716
+ tradingviewMcpEnabled && tradingviewMcpTransport === "http"
717
+ ? options.tradingviewMcpUrl ??
718
+ (interactive
719
+ ? await ask(rl, "TradingView MCP URL", currentEnv.TRADINGVIEW_MCP_URL || "")
720
+ : currentEnv.TRADINGVIEW_MCP_URL || "")
721
+ : currentEnv.TRADINGVIEW_MCP_URL || "";
722
+
522
723
  const robinhoodUsername =
523
724
  enabledMarkets.includes("robinhood")
524
725
  ? options.robinhoodUsername ??
@@ -543,16 +744,29 @@ export async function onboard(options = {}) {
543
744
  : currentEnv.GROWW_ACCESS_TOKEN || "")
544
745
  : currentEnv.GROWW_ACCESS_TOKEN || "";
545
746
 
546
- const workspaceExists = existsSync(path.join(workspace, "main.py"));
547
- if (interactive && workspaceExists && !(await confirm(rl, `Reuse existing workspace at ${workspace}`, true))) {
548
- throw new Error("Onboarding cancelled.");
549
- }
550
-
551
747
  if (mode === "live" && primaryMarket !== "kalshi") {
552
748
  notes.push("Live execution is currently wired through Kalshi. This workspace has been kept in paper mode while the extra rails stay enabled.");
553
749
  }
554
750
 
751
+ if (dashboardSurface === "chat") {
752
+ notes.push("Dashboard chat cockpit is enabled with operator channels for command, markets, feeds, risk, and execution.");
753
+ }
754
+
755
+ if (enabledMarkets.includes("tradingview") && tradingviewConnectorMode !== "mcp") {
756
+ notes.push("TradingView is configured in watchlist mode. Add an MCP connector later if you want richer chart and symbol context.");
757
+ }
758
+
759
+ if (tradingviewMcpEnabled && tradingviewMcpTransport === "stdio" && !tradingviewMcpCommand) {
760
+ notes.push("TradingView MCP was enabled without a command. The dashboard will show the connector as incomplete until you fill it in.");
761
+ }
762
+
763
+ if (tradingviewMcpEnabled && tradingviewMcpTransport === "http" && !tradingviewMcpUrl) {
764
+ notes.push("TradingView MCP was enabled without a URL. The dashboard will show the connector as incomplete until you fill it in.");
765
+ }
766
+
555
767
  const effectiveMode = mode === "live" && primaryMarket === "kalshi" ? "live" : "paper";
768
+ const installDeps =
769
+ options.install ?? (options.skipInstall ? false : interactive ? await confirm(rl, "Install Python and web dependencies now", true) : true);
556
770
 
557
771
  ensureWorkspaceFiles(workspace, { force: Boolean(options.force) });
558
772
 
@@ -564,6 +778,8 @@ export async function onboard(options = {}) {
564
778
  OPENTRADEX_ENABLED_MARKETS: enabledMarkets.join(","),
565
779
  OPENTRADEX_ENABLED_INTEGRATIONS: integrations.join(","),
566
780
  OPENTRADEX_LIVE_EXECUTION_MARKET: "kalshi",
781
+ OPENTRADEX_DASHBOARD_SURFACE: dashboardSurface,
782
+ OPENTRADEX_CHANNELS: channels.join(","),
567
783
  NEWS_PROVIDER: integrations.join(","),
568
784
  APIFY_API_TOKEN: apifyToken,
569
785
  BANKROLL: Number(bankroll).toFixed(2),
@@ -575,6 +791,12 @@ export async function onboard(options = {}) {
575
791
  POLYMARKET_WALLET_ADDRESS: polymarketWalletAddress,
576
792
  POLYMARKET_PRIVATE_KEY: polymarketPrivateKey,
577
793
  TRADINGVIEW_WATCHLIST: tradingviewWatchlist,
794
+ TRADINGVIEW_CONNECTOR_MODE: tradingviewConnectorMode,
795
+ TRADINGVIEW_MCP_ENABLED: tradingviewMcpEnabled ? "true" : "false",
796
+ TRADINGVIEW_MCP_TRANSPORT: tradingviewMcpTransport,
797
+ TRADINGVIEW_MCP_COMMAND: tradingviewMcpCommand,
798
+ TRADINGVIEW_MCP_ARGS: tradingviewMcpArgs,
799
+ TRADINGVIEW_MCP_URL: tradingviewMcpUrl,
578
800
  ROBINHOOD_USERNAME: robinhoodUsername,
579
801
  ROBINHOOD_PASSWORD: robinhoodPassword,
580
802
  GROWW_ACCESS_TOKEN: growwAccessToken,
@@ -588,6 +810,11 @@ export async function onboard(options = {}) {
588
810
  primaryMarket,
589
811
  enabledMarkets,
590
812
  integrations,
813
+ dashboardSurface,
814
+ channels,
815
+ tradingviewConnectorMode,
816
+ tradingviewMcpEnabled,
817
+ tradingviewMcpTransport,
591
818
  bankroll,
592
819
  interval,
593
820
  installDeps,
@@ -603,14 +830,25 @@ export async function onboard(options = {}) {
603
830
  primaryMarket,
604
831
  enabledMarkets,
605
832
  integrations,
833
+ dashboardSurface,
834
+ channels,
835
+ tradingviewConnectorMode,
836
+ tradingviewMcpEnabled,
837
+ tradingviewMcpTransport,
606
838
  bankroll: Number(bankroll).toFixed(2),
607
839
  interval: Number(interval),
608
840
  installedAt: new Date().toISOString(),
609
841
  });
610
842
 
611
843
  if (installDeps) {
844
+ if (interactive) {
845
+ printSection("BOOT", "local launch", "Installing the requested Python and dashboard dependencies.");
846
+ }
612
847
  await runCommand("python", ["-m", "pip", "install", "-r", "requirements.txt"], { cwd: workspace });
613
848
  await runPackageInstall(packageManager, path.join(workspace, "web"));
849
+ notes.push("Local dependencies were installed for Python and the dashboard.");
850
+ } else {
851
+ notes.push("Dependency install was skipped. You can still boot later with `pip install -r requirements.txt` and a dashboard package install.");
614
852
  }
615
853
 
616
854
  return {
@@ -671,6 +909,8 @@ export async function runDashboard(options = {}) {
671
909
  await runPackageInstall(packageManager, webDir);
672
910
  }
673
911
 
912
+ printDashboardBoot({ workspace, packageManager, profile });
913
+
674
914
  await runPackageScript(packageManager, "dev", webDir);
675
915
  }
676
916
 
@@ -680,6 +920,8 @@ export function formatDoctorReport(report) {
680
920
  `workspace: ${report.workspace}`,
681
921
  `runtime: ${report.profile.runtime}`,
682
922
  `package manager: ${report.profile.packageManager}`,
923
+ `dashboard: ${report.profile.dashboardSurface}`,
924
+ `channels: ${labelsForIds(CHANNEL_OPTIONS, report.profile.channels).join(", ") || "none"}`,
683
925
  `mode: ${report.profile.mode}`,
684
926
  `primary market: ${labelsForIds(MARKET_OPTIONS, [report.profile.primaryMarket]).join(", ")}`,
685
927
  `market rails: ${labelsForIds(MARKET_OPTIONS, report.profile.enabledMarkets).join(", ") || "none"}`,
@@ -711,12 +953,18 @@ export function formatProviderMatrix() {
711
953
  "Agent runtimes:",
712
954
  ...formatOptionLines(RUNTIME_OPTIONS).map((line) => ` ${line}`),
713
955
  "",
956
+ "Dashboard surfaces:",
957
+ ...formatOptionLines(DASHBOARD_SURFACE_OPTIONS).map((line) => ` ${line}`),
958
+ "",
714
959
  "Market rails:",
715
960
  ...formatOptionLines(MARKET_OPTIONS).map((line) => ` ${line}`),
716
961
  "",
717
962
  "Data integrations:",
718
963
  ...formatOptionLines(INTEGRATION_OPTIONS).map((line) => ` ${line}`),
719
964
  "",
965
+ "Operator channels:",
966
+ ...formatOptionLines(CHANNEL_OPTIONS).map((line) => ` ${line}`),
967
+ "",
720
968
  "Package managers:",
721
969
  ...formatOptionLines(PACKAGE_MANAGER_OPTIONS).map((line) => ` ${line}`),
722
970
  ];
@@ -817,12 +1065,12 @@ async function chooseMode(rl, fallback) {
817
1065
 
818
1066
  async function chooseOption(rl, label, options, fallbackId) {
819
1067
  const fallback = getOptionById(options, fallbackId, options[0].id);
820
- console.log(`${label}:`);
1068
+ printOptionList(label);
821
1069
  for (const line of formatOptionLines(options)) {
822
1070
  console.log(` ${line}`);
823
1071
  }
824
1072
 
825
- const answer = (await rl.question(`Enter number or id [${fallback.id}]: `)).trim().toLowerCase();
1073
+ const answer = (await rl.question(` Enter number or id [${fallback.id}]: `)).trim().toLowerCase();
826
1074
  if (!answer) {
827
1075
  return fallback.id;
828
1076
  }
@@ -837,13 +1085,13 @@ async function chooseOption(rl, label, options, fallbackId) {
837
1085
 
838
1086
  async function chooseMany(rl, label, options, fallbackIds = []) {
839
1087
  const fallback = keepKnownIds(options, fallbackIds);
840
- console.log(`${label}:`);
1088
+ printOptionList(label);
841
1089
  for (const line of formatOptionLines(options)) {
842
1090
  console.log(` ${line}`);
843
1091
  }
844
1092
 
845
1093
  const fallbackText = fallback.join(",") || "none";
846
- const answer = (await rl.question(`Enter comma-separated ids or numbers [${fallbackText}]: `)).trim().toLowerCase();
1094
+ const answer = (await rl.question(` Enter comma-separated ids or numbers [${fallbackText}]: `)).trim().toLowerCase();
847
1095
  if (!answer) {
848
1096
  return fallback;
849
1097
  }
@@ -866,28 +1114,180 @@ async function chooseMany(rl, label, options, fallbackIds = []) {
866
1114
  function printWelcomeBanner() {
867
1115
  console.log([
868
1116
  "",
869
- "Welcome to OpenTradex",
870
- "Our implementation. Your strategy.",
1117
+ " ___ ____ _____ _ _ _____ ____ _ ____ _______ __",
1118
+ " / _ \\| _ \\| ____| \\ | |_ _| _ \\ / \\ | _ \\| ____\\ \\/ /",
1119
+ "| | | | |_) | _| | \\| | | | | |_) |/ _ \\ | | | | _| \\ / ",
1120
+ "| |_| | __/| |___| |\\ | | | | _ </ ___ \\| |_| | |___ / \\ ",
1121
+ " \\___/|_| |_____|_| \\_| |_| |_| \\_\\_/ \\_\\____/|_____/_/\\_\\",
1122
+ "",
1123
+ " OpenTradex onboarding",
1124
+ " Our implementation. Your strategy.",
1125
+ "",
1126
+ ].join("\n"));
1127
+ }
1128
+
1129
+ function printSecurityNotice() {
1130
+ printPanel("Security", [
1131
+ "OpenTradex can read local files, install dependencies, and route trading workflows.",
1132
+ "Treat it like a capable local operator. Use least-privilege credentials and keep secrets out of the repo.",
1133
+ "Do not enable live trading until your paper workflow is stable and your risk limits are set.",
1134
+ "Live execution is currently supported on Kalshi. Other rails stay in research, discovery, watchlist, or paper mode.",
1135
+ "",
1136
+ "Recommended routine:",
1137
+ "opentradex doctor",
1138
+ "opentradex providers",
1139
+ ]);
1140
+ }
1141
+
1142
+ function printSection(step, title, description) {
1143
+ console.log([
1144
+ "",
1145
+ `[${step}] ${title.toUpperCase()}`,
1146
+ "-".repeat(72),
1147
+ description,
1148
+ "",
1149
+ ].join("\n"));
1150
+ }
1151
+
1152
+ function printOptionList(label) {
1153
+ console.log([
871
1154
  "",
1155
+ label,
1156
+ "-".repeat(72),
872
1157
  ].join("\n"));
873
1158
  }
874
1159
 
1160
+ function printDashboardBoot({ workspace, packageManager, profile }) {
1161
+ printPanel("Local harness", [
1162
+ `workspace: ${workspace}`,
1163
+ `runtime: ${profile.runtime}`,
1164
+ `package flow: ${packageManager}`,
1165
+ `dashboard: ${profile.dashboardSurface || DEFAULT_ENV.OPENTRADEX_DASHBOARD_SURFACE}`,
1166
+ `primary rail: ${profile.primaryMarket}`,
1167
+ `additional rails: ${profile.enabledMarkets.join(", ") || "none"}`,
1168
+ `channels: ${profile.channels?.join(", ") || "none"}`,
1169
+ "",
1170
+ "When the dev server is ready, open the dashboard and warm boot the local harness.",
1171
+ "Suggested local URL: http://localhost:3000/dashboard",
1172
+ ]);
1173
+ }
1174
+
1175
+ function printPanel(title, lines, width = 76) {
1176
+ const innerWidth = Math.max(32, width - 4);
1177
+ const border = `+${"-".repeat(innerWidth + 2)}+`;
1178
+ const output = [border];
1179
+
1180
+ for (const line of wrapText(title, innerWidth)) {
1181
+ output.push(`| ${padRight(line, innerWidth)} |`);
1182
+ }
1183
+
1184
+ output.push(`| ${"-".repeat(innerWidth)} |`);
1185
+
1186
+ for (const sourceLine of lines) {
1187
+ if (!sourceLine) {
1188
+ output.push(`| ${" ".repeat(innerWidth)} |`);
1189
+ continue;
1190
+ }
1191
+
1192
+ for (const line of wrapText(sourceLine, innerWidth)) {
1193
+ output.push(`| ${padRight(line, innerWidth)} |`);
1194
+ }
1195
+ }
1196
+
1197
+ output.push(border);
1198
+ console.log(`\n${output.join("\n")}\n`);
1199
+ }
1200
+
1201
+ function wrapText(text, width) {
1202
+ const normalized = String(text);
1203
+ if (normalized.length <= width) {
1204
+ return [normalized];
1205
+ }
1206
+
1207
+ const words = normalized.split(/\s+/).filter(Boolean);
1208
+ if (words.length === 0) {
1209
+ return [""];
1210
+ }
1211
+
1212
+ const lines = [];
1213
+ let current = "";
1214
+
1215
+ const flush = () => {
1216
+ if (current) {
1217
+ lines.push(current);
1218
+ current = "";
1219
+ }
1220
+ };
1221
+
1222
+ for (const word of words) {
1223
+ if (word.length > width) {
1224
+ flush();
1225
+ for (let index = 0; index < word.length; index += width) {
1226
+ lines.push(word.slice(index, index + width));
1227
+ }
1228
+ continue;
1229
+ }
1230
+
1231
+ if (!current) {
1232
+ current = word;
1233
+ continue;
1234
+ }
1235
+
1236
+ if (`${current} ${word}`.length <= width) {
1237
+ current = `${current} ${word}`;
1238
+ continue;
1239
+ }
1240
+
1241
+ flush();
1242
+ current = word;
1243
+ }
1244
+
1245
+ flush();
1246
+ return lines;
1247
+ }
1248
+
1249
+ function padRight(value, width) {
1250
+ return String(value).padEnd(width, " ");
1251
+ }
1252
+
875
1253
  function normalizeConfig(config = {}) {
876
1254
  return {
877
1255
  workspace: config.workspace,
878
1256
  runtime: config.runtime || config.profile?.runtime || DEFAULT_ENV.OPENTRADEX_RUNTIME,
879
1257
  packageManager: config.packageManager || config.profile?.packageManager || DEFAULT_ENV.OPENTRADEX_PACKAGE_MANAGER,
1258
+ dashboardSurface: config.dashboardSurface || config.profile?.dashboardSurface || DEFAULT_ENV.OPENTRADEX_DASHBOARD_SURFACE,
1259
+ channels: keepKnownIds(CHANNEL_OPTIONS, config.channels || config.profile?.channels || DEFAULT_ENV.OPENTRADEX_CHANNELS),
880
1260
  mode: config.mode || config.profile?.mode || DEFAULT_ENV.OPENTRADEX_EXECUTION_MODE,
881
1261
  primaryMarket: config.primaryMarket || config.profile?.primaryMarket || DEFAULT_ENV.OPENTRADEX_PRIMARY_MARKET,
882
1262
  enabledMarkets: keepKnownIds(MARKET_OPTIONS, config.enabledMarkets || config.profile?.enabledMarkets || DEFAULT_ENV.OPENTRADEX_ENABLED_MARKETS),
883
1263
  integrations: keepKnownIds(INTEGRATION_OPTIONS, config.integrations || config.profile?.integrations || DEFAULT_ENV.OPENTRADEX_ENABLED_INTEGRATIONS),
1264
+ tradingviewConnectorMode:
1265
+ String(config.tradingviewConnectorMode || config.profile?.tradingviewConnectorMode || DEFAULT_ENV.TRADINGVIEW_CONNECTOR_MODE).toLowerCase() === "mcp"
1266
+ ? "mcp"
1267
+ : "watchlist",
1268
+ tradingviewMcpEnabled:
1269
+ String(config.tradingviewMcpEnabled ?? config.profile?.tradingviewMcpEnabled ?? DEFAULT_ENV.TRADINGVIEW_MCP_ENABLED).toLowerCase() === "true",
1270
+ tradingviewMcpTransport:
1271
+ String(config.tradingviewMcpTransport || config.profile?.tradingviewMcpTransport || DEFAULT_ENV.TRADINGVIEW_MCP_TRANSPORT).toLowerCase() === "http"
1272
+ ? "http"
1273
+ : "stdio",
884
1274
  bankroll: config.bankroll || config.profile?.bankroll || DEFAULT_ENV.BANKROLL,
885
1275
  interval: config.interval || config.profile?.interval || DEFAULT_ENV.CYCLE_INTERVAL,
886
1276
  installDeps: config.installDeps ?? config.profile?.installDeps ?? false,
887
1277
  };
888
1278
  }
889
1279
 
890
- function buildDoctorNotes({ runtime, primaryMarket, enabledMarkets, liveTrading }) {
1280
+ function buildDoctorNotes({
1281
+ runtime,
1282
+ primaryMarket,
1283
+ enabledMarkets,
1284
+ liveTrading,
1285
+ dashboardSurface,
1286
+ channels,
1287
+ tradingviewConnectorMode,
1288
+ tradingviewMcpEnabled,
1289
+ tradingviewMcpTransport,
1290
+ }) {
891
1291
  const notes = [];
892
1292
 
893
1293
  if (runtime !== "claude-code") {
@@ -906,6 +1306,20 @@ function buildDoctorNotes({ runtime, primaryMarket, enabledMarkets, liveTrading
906
1306
  notes.push("TradingView, Robinhood, and Groww are configured as watchlist/profile rails so you can add credentials only when you really need them.");
907
1307
  }
908
1308
 
1309
+ if (dashboardSurface === "chat") {
1310
+ notes.push(`Dashboard chat cockpit is enabled with channels: ${channels.join(", ") || "none"}.`);
1311
+ }
1312
+
1313
+ if (enabledMarkets.includes("tradingview") && tradingviewConnectorMode === "mcp") {
1314
+ notes.push(`TradingView is set to MCP mode over ${tradingviewMcpTransport}. The dashboard will surface connector status, but Claude Code still needs the MCP server available locally.`);
1315
+ } else if (enabledMarkets.includes("tradingview")) {
1316
+ notes.push("TradingView is currently a watchlist/context rail. Enable MCP later if you want richer chart tooling.");
1317
+ }
1318
+
1319
+ if (tradingviewMcpEnabled && tradingviewConnectorMode === "mcp") {
1320
+ notes.push("TradingView MCP is marked enabled in the workspace profile. Fill in the command or URL if the doctor still shows it as incomplete.");
1321
+ }
1322
+
909
1323
  return notes;
910
1324
  }
911
1325