Skip to content

feat(paper-trading): add paper trading feature and related scripts#81

Open
ericosmic wants to merge 12 commits into
Open-Dev-Society:mainfrom
ericosmic:openstock-paper-trading
Open

feat(paper-trading): add paper trading feature and related scripts#81
ericosmic wants to merge 12 commits into
Open-Dev-Society:mainfrom
ericosmic:openstock-paper-trading

Conversation

@ericosmic

@ericosmic ericosmic commented May 29, 2026

Copy link
Copy Markdown

Added a complete paper trading system, including account management and trade execution functionalities. Introduced multiple helper scripts for portfolio analysis, stock research, and automated trade execution. Configured dependency build options to optimize performance.

  • Added paper trading page and dashboard components
  • Implemented AI-driven automated simulated trading functionality
  • Added MongoDB data models for trade record management
  • Implemented automatic price fetching and caching mechanisms
  • Added portfolio analysis and risk monitoring features
  • Configured npm dependency build optimizations

Summary by CodeRabbit

  • New Features

    • Paper Trading dashboard: multi-account support, account switcher, pending orders list, full buy/sell controls (market/limit/stop/market-on-open), real-time positions, P&L and trade history, trading-period and reset controls
    • AI-powered auto-trading panel: strategy presets, saveable config, enable toggle, manual run and run results
    • Market-hours detection with pre-market/regular-hour awareness
  • Style

    • Added slide-down animation for UI elements
  • Chores

    • Navigation entry for Paper Trading; workspace/build restrictions, new runtime dependency, and updated ignore rules

Review Change Stack

Added a complete paper trading system, including account management and trade execution functionalities.
Introduced multiple helper scripts for portfolio analysis, stock research, and automated trade execution.
Configured dependency build options to optimize performance.

- Added paper trading page and dashboard components
- Implemented AI-driven automated simulated trading functionality
- Added MongoDB data models for trade record management
- Implemented automatic price fetching and caching mechanisms
- Added portfolio analysis and risk monitoring features
- Configured npm dependency build optimizations
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

@ericosmic is attempting to deploy a commit to the ravixalgorithm's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a complete paper-trading feature: multi-account simulation, pending orders, AI-driven trade cycles, market-hours utilities, Finnhub filesystem caching, dashboard and UI components, and workspace/build configuration.

Changes

Paper Trading System

Layer / File(s) Summary
Data Models & Schemas
database/models/paper-trading.model.ts
PaperTradingAccount, PaperTrade, and AITradingConfig Mongoose schemas/types with indexes and exported model handles.
Market Hours & Scheduling Utilities
lib/utils/market-hours.ts
Dynamic US market holiday generation, ET DST offset, and exports: isMarketOpen(), isPreMarket(), getMarketStatus().
Finnhub API Caching Infrastructure
lib/actions/finnhub.actions.ts
Filesystem-backed MD5 JSON cache for requests, timeout-enforced fetch (doFetch), stale-cache fallback for quotes, and optional HTTP proxy via undici.
Account Management & Basics
lib/actions/paper-trading.actions.ts
Server actions for listing, creating, deleting, switching accounts, default-account creation/backfill, and native-MongoDB account CRUD.
Trading Simulation & Order Execution
lib/actions/paper-trading.actions.ts
buyStock/sellStock, getPositions, getTradeHistory, balance/holdings validation, trade record writes, and trading-period management.
Pending Order Management
lib/actions/pending-orders.actions.ts
Create/cancel/fetch pending orders, execution engine for LIMIT/STOP/MARKET_ON_OPEN semantics, and processAllPendingOrders for cron execution.
AI Trading System
lib/actions/ai-trading.actions.ts
AI config CRUD (getAIConfig, saveAIConfig, toggleAITrading) and runAITradeCycle(accountId, force=false) that calls an LLM, parses decisions, executes trades, and enforces gating.
Paper Trading Page & Dashboard Component
app/(root)/paper-trading/page.tsx, components/paper-trading/PaperTradingDashboard.tsx
Server-authenticated page that renders PaperTradingDashboard, which manages data refresh, buy/sell forms, holdings, pending orders, trade history, period/reset flows, and embeds AITradingPanel.
Account Switcher Component
components/paper-trading/AccountSwitcher.tsx
Client dropdown and dialog for switching, creating, and deleting accounts with validation and protections.
AI Trading Panel Component
components/paper-trading/AITradingPanel.tsx
Client form for LLM endpoint/model/key, strategy presets, risk params, system prompt editing, Save/Run controls, and result display.
Pending Orders List Component
components/paper-trading/PendingOrdersList.tsx
Client table showing pending/failed orders with per-order cancel action and periodic refresh.
Configuration & Navigation Wiring
.npmrc, package.json, pnpm-workspace.yaml, lib/constants.ts, app/globals.css, .gitignore
Adds undici dependency, restricts native builds in .npmrc/pnpm workspace, adds navigation entry for Paper Trading, defines slideDown animation, and ignores _scripts/.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🐰 I’m a rabbit in the market light,

Tapping keys to trade by night,
Pending orders, AI in tow,
Mock returns that ebb and flow,
Hopping through charts with delight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changeset: a complete paper trading feature system with related infrastructure and scripts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (5)
lib/actions/finnhub.actions.ts-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a guaranteed-writable temp dir instead of process.env.HOME.

HOME is unset on Windows (falls back to /tmp, which isn't a valid path there) and is frequently read-only in serverless/containerized deploys where only the OS temp dir is writable. Deriving the cache dir from os.tmpdir() avoids these failure modes.

🛠️ Proposed fix
+import * as os from 'os';
 ...
-const FINNHUB_CACHE_DIR = path.join(process.env.HOME || '/tmp', '.hermes', 'cache', 'finnhub');
+const FINNHUB_CACHE_DIR = path.join(os.tmpdir(), '.hermes', 'cache', 'finnhub');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/finnhub.actions.ts` at line 12, Replace the current
FINNHUB_CACHE_DIR construction which uses process.env.HOME with a
guaranteed-writable temp directory by using os.tmpdir(); update the symbol
FINNHUB_CACHE_DIR to join path.join(os.tmpdir(), '.hermes', 'cache', 'finnhub')
(importing os if missing) so the cache lives under the OS temp directory instead
of HOME.
_scripts/sector_analysis.js-52-56 (1)

52-56: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

q.dp can be undefined even when q.c is set → q.dp.toFixed(2) throws.

The guard only checks q.c. If Finnhub returns a quote without dp, Line 56's q.dp.toFixed(2) throws TypeError, aborting the whole loop. Default dp before use.

🛠️ Proposed fix
     if (q && q.c) {
-      const chg = q.dp >= 0 ? '+' : '';
-      const emoji = q.dp > 2 ? '🔥' : q.dp > 0 ? '📈' : q.dp > -2 ? '📉' : '💀';
+      const dp = typeof q.dp === 'number' ? q.dp : 0;
+      const chg = dp >= 0 ? '+' : '';
+      const emoji = dp > 2 ? '🔥' : dp > 0 ? '📈' : dp > -2 ? '📉' : '💀';
       const mv = pp?.marketCapitalization ? '$' + (pp.marketCapitalization / 1e9).toFixed(1) + 'B' : '';
-      console.log(emoji + ' ' + s.sym.padEnd(6) + s.name.padEnd(22) + '$' + String(q.c).padStart(8) + '  ' + chg + q.dp.toFixed(2) + '%  ' + mv);
+      console.log(emoji + ' ' + s.sym.padEnd(6) + s.name.padEnd(22) + '$' + String(q.c).padStart(8) + '  ' + chg + dp.toFixed(2) + '%  ' + mv);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/sector_analysis.js` around lines 52 - 56, The issue is that q.dp may
be undefined causing q.dp.toFixed(2) to throw; fix by normalizing dp before use:
create a local numeric dp (e.g. const dp = typeof q.dp === 'number' ? q.dp : 0)
and then use dp for the emoji selection, chg calculation and formatting instead
of q.dp; update references in the block that compute emoji, chg and the toFixed
output to use this dp variable so the loop won't throw when dp is missing.
_scripts/check_positions.js-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

process.env.HOME is undefined on Windows.

path.join(process.env.HOME, ...) throws TypeError when HOME is unset (Windows uses USERPROFILE). Prefer os.homedir().

🛠️ Proposed fix
+const os = require('os');
-const cacheDir = path.join(process.env.HOME, '.hermes', 'cache', 'finnhub');
+const cacheDir = path.join(os.homedir(), '.hermes', 'cache', 'finnhub');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/check_positions.js` at line 12, Replace use of process.env.HOME with
the cross-platform API os.homedir(): add a require/import for the os module
(const os = require('os')) and change the cacheDir initialization (the const
cacheDir = path.join(...)) to use os.homedir() instead of process.env.HOME so it
works on Windows and when HOME is unset.
_scripts/execute_trades.js-10-10 (1)

10-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

.env is read relative to the current working directory, not the script.

Unlike the sibling scripts (which use path.join(__dirname, '..', '.env')), this reads '.env' from process.cwd(), so the token resolves to '' unless the script is run from the repo root. Make the path script-relative for consistency.

🛠️ Proposed fix
-const token = fs.readFileSync('.env', 'utf8').match(/NEXT_PUBLIC_FINNHUB_API_KEY=(\S+)/)?.[1] || '';
+const token = fs.readFileSync(path.join(__dirname, '..', '.env'), 'utf8').match(/NEXT_PUBLIC_FINNHUB_API_KEY=(\S+)/)?.[1] || '';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/execute_trades.js` at line 10, The token is read from '.env'
relative to process.cwd() which breaks when the script is executed outside the
repo root; update the read to use a script-relative path by constructing the env
file path with __dirname (e.g. path.join(__dirname, '..', '.env')) and pass that
into fs.readFileSync where the token is defined (the const token declaration) so
the script consistently finds the repo .env regardless of current working
directory.
_scripts/check_positions.js-1-5 (1)

1-5: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix ESLint failures from CommonJS require() in _scripts/*.js

@typescript-eslint/no-require-imports is enforced and fails lint on _scripts/check_positions.js (and the other _scripts/*.js files) due to require() usage; _scripts/ isn’t ignored in eslint.config.mjs (it only ignores node_modules/**, .next/**, out/**, build/**, next-env.d.ts). Convert these scripts to ESM import or exclude _scripts/ / disable @typescript-eslint/no-require-imports for that folder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/check_positions.js` around lines 1 - 5, The CommonJS requires (e.g.,
const { MongoClient } = require('mongodb'), const undici = require('undici'),
const fs = require('fs'), const path = require('path'), const crypto =
require('crypto')) must be converted to ESM imports to satisfy
`@typescript-eslint/no-require-imports` for files under _scripts; replace them
with import statements like import { MongoClient } from 'mongodb'; import * as
undici from 'undici' (or import specific named exports you use); import fs from
'fs'; import path from 'path'; import crypto from 'crypto'; and ensure the
project treats these scripts as ESM (add "type":"module" to package.json or
rename the scripts to .mjs) so Node will accept the import syntax.
🧹 Nitpick comments (6)
lib/actions/paper-trading.actions.ts (1)

153-157: ⚡ Quick win

Resolve the no-explicit-any lint errors flagged across this file.

ESLint reports @typescript-eslint/no-explicit-any as errors on the (account as any) casts here (and at Lines 179-188, 222, 225, 408, 460-462). Since the rule is at error level it will block CI. The casts stem from findById(...).lean() losing the document type — typing the lean result (e.g. PaperTradingAccount) removes most of them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/paper-trading.actions.ts` around lines 153 - 157, The code is
using (account as any) casts because Mongoose .lean() lost the type; fix by
properly typing the lean result (e.g. call .lean<PaperTradingAccount | null>()
or assign the result to a variable typed as PaperTradingAccount | null) when
fetching the document in the functions that use account (look for usages of
findById(...).lean() and the local variable account in
lib/actions/paper-trading.actions.ts), then remove all (account as any) casts
and access properties directly (tradingPeriod, startDate, endDate,
customPeriodDays, name, and the other usages at the later locations referenced
in the review). Ensure other occurrences (lines ~179-188, 222, 225, 408,
460-462) are similarly updated by replacing explicit any casts with the correct
typed lean result or by narrowing the type before mutation.
lib/actions/finnhub.actions.ts (1)

47-53: ⚡ Quick win

Synchronous filesystem I/O blocks the event loop.

Inside a 'use server' action, fs.existsSync/readFileSync (here and at Lines 66-67, 148-149) and mkdirSync/writeFileSync (Lines 60-61) run synchronously and stall all concurrent requests handled by the same Node process. Consider switching to fs.promises (await fs.promises.readFile, etc.) since the surrounding functions are already async.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/finnhub.actions.ts` around lines 47 - 53, Replace all synchronous
fs calls (fs.existsSync, fs.readFileSync, fs.mkdirSync, fs.writeFileSync) with
their async Promise counterparts to avoid blocking the event loop: use await
fs.promises.readFile to read cacheFile and JSON.parse the result, or await
fs.promises.stat/access to check existence, and use await
fs.promises.mkdir(cacheDir, { recursive: true }) and await fs.promises.writeFile
for writes; update the try/catch around the cache logic that uses cacheFile and
revalidateSeconds to await these operations and preserve existing error handling
and return types, and make the equivalent changes for the other sync usages in
this module (the blocks that currently call fs.existsSync/readFileSync and
mkdirSync/writeFileSync).
_scripts/check_positions.js (2)

8-10: ⚡ Quick win

Hardcoded proxy makes the script fail wherever no proxy runs on 127.0.0.1:7890.

All requests are routed through a fixed local ProxyAgent; on any machine without that proxy, fetch will error out. Consider deriving the proxy from an env var (e.g. HTTPS_PROXY) and falling back to a direct dispatcher when unset.

♻️ Suggested change
-const PROXY = 'http://127.0.0.1:7890';
-const dispatcher = new undici.ProxyAgent(PROXY);
-const fetch = (url) => undici.fetch(url, { dispatcher });
+const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || '';
+const dispatcher = PROXY ? new undici.ProxyAgent(PROXY) : undefined;
+const fetch = (url) => undici.fetch(url, dispatcher ? { dispatcher } : undefined);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/check_positions.js` around lines 8 - 10, The script currently
hardcodes PROXY and always creates dispatcher = new undici.ProxyAgent(PROXY) and
a wrapped fetch that passes that dispatcher, which breaks on machines without a
local proxy; change it to read the proxy URL from an environment variable (e.g.
process.env.HTTPS_PROXY || process.env.https_proxy), only construct new
undici.ProxyAgent(proxy) when that env var is set, and make the fetch wrapper
(the fetch function defined in this file) conditionally include the dispatcher
option (omit the dispatcher or pass undefined when no proxy is configured) so
requests use a direct connection when no proxy is present; update references to
PROXY, dispatcher, ProxyAgent and fetch accordingly.

15-26: ⚡ Quick win

getQuote doesn't check the HTTP response and caches error payloads.

If Finnhub returns a non-200 (e.g. rate limit / auth error), r.json() yields an error object with no c, so price silently becomes 0, and that bad payload is cached on disk for 60s. Validate the response before caching.

♻️ Suggested guard
   const r = await fetch('https://finnhub.io/api/v1/quote?symbol=' + sym + '&token=' + token);
+  if (!r.ok) throw new Error(`Finnhub ${r.status} for ${sym}`);
   const d = await r.json();
+  if (typeof d.c !== 'number') throw new Error(`Invalid quote for ${sym}: ${JSON.stringify(d)}`);
   fs.writeFileSync(cf, JSON.stringify({ ts: Date.now(), data: d }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/check_positions.js` around lines 15 - 26, getQuote currently calls
r.json() and caches the payload without checking HTTP status, so non-200 error
bodies get written to cf; change getQuote to validate the fetch response (check
r.ok or r.status) before writing the cache file (cf) or returning data: if the
response is not ok, read the response body or text for diagnostics but do NOT
write to the cache and either throw an error or return a clear error value;
ensure you only JSON.parse/fs.writeFileSync the successful payload (d) and
preserve existing timeout logic that uses Date.now() and token/cacheDir
variables.
components/paper-trading/AccountSwitcher.tsx (1)

14-21: 💤 Low value

Unused prop onAccountListChange in component signature.

The prop onAccountListChange is defined in Props interface but never used in the component. Either remove it from the interface or implement the callback where appropriate (e.g., after create/delete operations).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/paper-trading/AccountSwitcher.tsx` around lines 14 - 21, Props
defines onAccountListChange but AccountSwitcher never uses it; remove
onAccountListChange from the Props interface and the AccountSwitcher parameter
list if you don't need the callback, or if you do need it, wire it up by adding
the callback parameter to the function signature (AccountSwitcher) and invoking
onAccountListChange() after account create/delete flows (where createAccount,
deleteAccount or similar handlers are implemented) so the parent is notified;
update any call sites that render <AccountSwitcher ... /> to pass the prop only
if kept.
lib/actions/ai-trading.actions.ts (1)

249-265: ⚖️ Poor tradeoff

Fragile price extraction via regex on localized message strings.

Parsing price from the success message using regex (/\$([\d.]+)/g) is brittle. If the message format changes or contains other dollar amounts, extraction may fail or return wrong values. The executed trade record will have price: 0 and incorrect total.

Consider returning structured data from buyStock/sellStock (e.g., { success, price, total }) rather than parsing natural language messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/ai-trading.actions.ts` around lines 249 - 265, The price
extraction from result.message using regex in the buyStock/sellStock call sites
is brittle; modify the buyStock and sellStock functions to return structured
data (e.g., { success, price, total, message }) and update the usage in
ai-trading.actions (where buyStock, sellStock are called and executed.push is
built) to read price and total directly from the returned object (use
result.price and result.total), removing the regex parsing; if you cannot change
the underlying functions immediately, add a robust fallback: attempt to read
result.price/result.total first, then only as a last resort parse result.message
with a stricter pattern and handle parse failures by flagging an error instead
of defaulting to 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@_scripts/execute_trades.js`:
- Around line 70-89: This script currently unconditionally inserts each BUY
(inside the for loop using trades.insertOne) and then updates the account
balance with accounts.updateOne, causing duplicate buys and double-debits when
rerun; fix by generating a deterministic batchId (or orderBatchId) for this run,
add/require a unique index on the trades collection keyed by batchId (and
symbol/shares if needed), check for an existing marker via trades.findOne({
batchId }) before inserting, and if not present perform all inserts and the
balance update inside a single MongoDB transaction (use a session) so
trades.insertOne and accounts.updateOne (referencing USER_ID, totalCost and
acc._id) are atomic and idempotent.

In `@components/paper-trading/AccountSwitcher.tsx`:
- Around line 59-74: handleDelete has a race where you filter the stale accounts
array after awaiting loadAccounts(); change it to use the fresh list returned by
loadAccounts (or modify loadAccounts to return the new accounts) instead of
referencing the old accounts variable: after calling await loadAccounts(),
capture its returned array (from loadAccounts) and compute remaining =
returnedAccounts.filter(a => a._id !== accountId), then call
onAccountChange(remaining[0]._id) if any; reference functions/variables:
handleDelete, loadAccounts, deleteAccount, accounts, onAccountChange.

In `@components/paper-trading/AITradingPanel.tsx`:
- Around line 120-128: handleToggle is calling toggleAITrading with userId as
the first arg but the server action toggleAITrading(accountId, enabled, userId?)
expects accountId first; update the call inside handleToggle to pass the correct
accountId (e.g., accountId or config.accountId) as the first argument and pass
userId only as the optional third argument if required, then keep the rest of
the logic (setConfig and setMessage) unchanged so the correct AI config document
is updated.
- Around line 130-145: In AITradingPanel update the handleRun function to pass
the account ID to the server action: replace the incorrect userId argument with
the accountId (or the prop/state variable that holds the account identifier)
when calling runAITradeCycle; ensure the correct identifier name used in this
component is referenced so runAITradeCycle(accountId) can load the AI config and
portfolio correctly.

In `@database/models/paper-trading.model.ts`:
- Around line 95-111: The AIConfigSchema currently persists apiKey as plaintext
(field apiKey in AIConfigSchema), so change storage to avoid plaintext: either
replace apiKey with a secret reference (e.g., secretRef) that holds an opaque ID
to a secrets manager, or implement application-level encryption via a Mongoose
pre-save hook on AIConfigSchema that encrypts apiKey with a KMS-derived key and
decrypts only when explicitly needed; additionally ensure apiKey is excluded
from outgoing documents by adding schema toJSON/toObject transforms (or use
select: false) so apiKey is never returned to clients, and update any codepaths
that read/write apiKey to use the secretRef or explicit decrypt helper.

In `@lib/actions/ai-trading.actions.ts`:
- Around line 61-75: In AITradingPanel, the call uses toggleAITrading(userId,
newEnabled) but the function expects accountId as the first argument; update the
caller in the AITradingPanel component to pass the correct account identifier
(e.g., change toggleAITrading(userId, newEnabled) to toggleAITrading(accountId,
newEnabled)), ensuring the accountId variable is available/derived in that scope
and not accidentally using userId.
- Around line 118-126: The caller in AITradingPanel incorrectly passes userId to
runAITradeCycle (which expects an accountId) causing config lookup to fail;
locate the call to runAITradeCycle in AITradingPanel.tsx and change the argument
from userId to the correct accountId value (or derive accountId from the current
account object/props) and update any related typing if necessary so
runAITradeCycle(accountId: string) receives the actual accountId rather than the
userId.
- Around line 196-226: The undici LLM call in this block lacks a timeout and
uses a hardcoded fallback proxy; update the logic so that proxy usage is
conditional (only create ProxyAgent/dispatcher when process.env.HTTPS_PROXY is
set and do NOT default to 'http://127.0.0.1:7890') and add an abort timeout
around undici.fetch (use AbortController/abort signal and a clearable
setTimeout) to ensure the request is cancelled after a sensible timeout; update
references in this block (proxyUrl, ProxyAgent, dispatcher, undici.fetch, and
the try/catch) so dispatcher is omitted from the fetch options when no proxy is
configured and the fetch call is aborted on timeout to surface an error to the
existing catch.

In `@lib/actions/finnhub.actions.ts`:
- Around line 58-73: The current try around doFetch<T>(url, timeoutMs) also
performs fs.mkdirSync(FINNHUB_CACHE_DIR, { recursive: true }) and
fs.writeFileSync(cacheFile, ...), so any write error causes the catch to run and
rethrows even though the fetch succeeded; change this so the filesystem
operations are best-effort: after obtaining const data (from doFetch)
immediately return data to callers and/or move the mkdirSync/writeFileSync into
their own nested try/catch that swallows/logs errors (do not rethrow) so that
getCompanyProfile/getNews/searchStocks (and functions using FINNHUB_CACHE_DIR,
cacheFile, now) always return the fetched data even if cache writes fail.
- Around line 144-156: The stale-cache fallback never runs because token and url
are declared inside the try and thus may be referenced out of scope, and
fetchJSON is called with revalidateSeconds === 0 so it never writes a cache for
the fallback to read; fix getQuote by hoisting the Finnhub auth token and the
constructed url (the variables referenced in getQuote) into the outer scope
before the try/catch (so token/url are available in the fallback block and error
handlers) and call fetchJSON(url, SHORT_TTL) with a small non-zero
revalidateSeconds (e.g., 30) so fetchJSON will persist the quote to
FINNHUB_CACHE_DIR and allow the existing stale-cache lookup (which checks
FINNHUB_CACHE_DIR and url md5) to succeed; also ensure the inner catch does not
reference scoped vars and only swallows read errors, preserving the intended
fallback return behavior in getQuote.
- Around line 87-105: In doFetch, replace the require('undici') usage with a
dynamic import (await import('undici')) to match other modules, change the
actualFetch typing from a custom signature to typeof globalThis.fetch and remove
the init?: any, and avoid creating a new ProxyAgent on every call by
caching/reusing a single ProxyAgent/dispatcher per proxyUrl (or a module-scoped
map keyed by proxyUrl) so you can reuse it and call its close() when
appropriate; update the code paths that set actualFetch to use the cached
dispatcher (new undici.ProxyAgent only when no cached agent exists) and ensure
types reference ProxyAgent/dispatcher from the imported undici module.

In `@lib/actions/paper-trading.actions.ts`:
- Around line 343-382: The sell flow has a TOCTOU race: holdings are read via
db.collection('papertrades').aggregate and later a SELL insert (and
PaperAccount.updateOne) happens outside an atomic context; fix by doing the
holdings check, balance update (PaperAccount.updateOne) and trade insert
(db.collection('papertrades').insertOne) inside a single MongoDB
transaction/session so the holdings are re-read and validated within the same
transaction and concurrent sells cannot both pass. Concretely, start a client
session (session = client.startSession()), run session.withTransaction(async ()
=> { recompute holdings using
db.collection('papertrades').aggregate(...).toArray({ session }), if held <
shares throw; await PaperAccount.updateOne(..., { $inc: { balance: total } }, {
session }); await db.collection('papertrades').insertOne({...}, { session }) }),
and ensure all DB calls (aggregate, updateOne, insertOne) include the session
param and that you abort/handle errors from the transaction.
- Around line 275-298: The buyStock and sellStock flows currently use the native
driver (db.collection('papertrades').insertOne(...)) which bypasses Mongoose
schema validation and allows non-positive or non-integer shares; add an explicit
guard at the top of both buyStock and sellStock to validate shares is an integer
>= 1 (reject otherwise with a clear error like 'shares must be a positive
integer'), and ensure this check runs before any price/held/balance calculations
or the native insert; reference the buyStock and sellStock functions and the
Trade schema constraint (shares: { min: 1 }) when adding the validation.
- Around line 300-318: The balance check and separate decrement are racy;
replace the two-step check-then-update with an atomic conditional update on
PaperAccount (e.g., use updateOne or findOneAndUpdate with filter {_id:
accountId, balance: {$gte: total}} and $inc: {balance: -total}) and verify the
update result (modifiedCount or returned value) succeeded before calling
db.collection('papertrades').insertOne; if the atomic update failed, return the
insufficient-funds error; keep accountId, total, roundedPrice, symbol and the
trade insert logic intact but only run the insert after the successful
conditional update.
- Around line 205-243: The aggregation and P/L math treat totalCost as the cost
of remaining shares which is wrong after partial sells; update the aggregation
to separately sum buyShares, sellShares, buyCost (sum of BUY totals) and
sellProceeds (sum of SELL totals) instead of only totalCost/totalProceeds,
compute shares = buyShares - sellShares, compute costPerBuyShare = buyCost /
buyShares (guard buyShares > 0), derive remainingCost = costPerBuyShare *
shares, then set avgCost = remainingCost / shares, pl = marketValue -
remainingCost, and plPercent based on remainingCost; change references in the
mapping (pos.totalCost, pos.shares, avgCost, pl, plPercent) to use the new
fields (buyShares, sellShares, buyCost, remainingCost) so partial sells produce
correct unrealized P/L.

In `@pnpm-workspace.yaml`:
- Around line 1-6: Replace the placeholder strings under allowBuilds with
boolean values and remove the unsupported onlyBuiltDependencies setting: change
allowBuilds to a map like "allowBuilds:\n  protobufjs: true\n  sharp: false\n 
unrs-resolver: false" (or set true/false per package policy you want) so each
package matcher is mapped to a boolean, delete the onlyBuiltDependencies line
entirely, and keep nodeLinker: hoisted as-is; update the allowBuilds entries for
protobufjs, sharp and unrs-resolver accordingly.

---

Minor comments:
In `@_scripts/check_positions.js`:
- Line 12: Replace use of process.env.HOME with the cross-platform API
os.homedir(): add a require/import for the os module (const os = require('os'))
and change the cacheDir initialization (the const cacheDir = path.join(...)) to
use os.homedir() instead of process.env.HOME so it works on Windows and when
HOME is unset.
- Around line 1-5: The CommonJS requires (e.g., const { MongoClient } =
require('mongodb'), const undici = require('undici'), const fs = require('fs'),
const path = require('path'), const crypto = require('crypto')) must be
converted to ESM imports to satisfy `@typescript-eslint/no-require-imports` for
files under _scripts; replace them with import statements like import {
MongoClient } from 'mongodb'; import * as undici from 'undici' (or import
specific named exports you use); import fs from 'fs'; import path from 'path';
import crypto from 'crypto'; and ensure the project treats these scripts as ESM
(add "type":"module" to package.json or rename the scripts to .mjs) so Node will
accept the import syntax.

In `@_scripts/execute_trades.js`:
- Line 10: The token is read from '.env' relative to process.cwd() which breaks
when the script is executed outside the repo root; update the read to use a
script-relative path by constructing the env file path with __dirname (e.g.
path.join(__dirname, '..', '.env')) and pass that into fs.readFileSync where the
token is defined (the const token declaration) so the script consistently finds
the repo .env regardless of current working directory.

In `@_scripts/sector_analysis.js`:
- Around line 52-56: The issue is that q.dp may be undefined causing
q.dp.toFixed(2) to throw; fix by normalizing dp before use: create a local
numeric dp (e.g. const dp = typeof q.dp === 'number' ? q.dp : 0) and then use dp
for the emoji selection, chg calculation and formatting instead of q.dp; update
references in the block that compute emoji, chg and the toFixed output to use
this dp variable so the loop won't throw when dp is missing.

In `@lib/actions/finnhub.actions.ts`:
- Line 12: Replace the current FINNHUB_CACHE_DIR construction which uses
process.env.HOME with a guaranteed-writable temp directory by using os.tmpdir();
update the symbol FINNHUB_CACHE_DIR to join path.join(os.tmpdir(), '.hermes',
'cache', 'finnhub') (importing os if missing) so the cache lives under the OS
temp directory instead of HOME.

---

Nitpick comments:
In `@_scripts/check_positions.js`:
- Around line 8-10: The script currently hardcodes PROXY and always creates
dispatcher = new undici.ProxyAgent(PROXY) and a wrapped fetch that passes that
dispatcher, which breaks on machines without a local proxy; change it to read
the proxy URL from an environment variable (e.g. process.env.HTTPS_PROXY ||
process.env.https_proxy), only construct new undici.ProxyAgent(proxy) when that
env var is set, and make the fetch wrapper (the fetch function defined in this
file) conditionally include the dispatcher option (omit the dispatcher or pass
undefined when no proxy is configured) so requests use a direct connection when
no proxy is present; update references to PROXY, dispatcher, ProxyAgent and
fetch accordingly.
- Around line 15-26: getQuote currently calls r.json() and caches the payload
without checking HTTP status, so non-200 error bodies get written to cf; change
getQuote to validate the fetch response (check r.ok or r.status) before writing
the cache file (cf) or returning data: if the response is not ok, read the
response body or text for diagnostics but do NOT write to the cache and either
throw an error or return a clear error value; ensure you only
JSON.parse/fs.writeFileSync the successful payload (d) and preserve existing
timeout logic that uses Date.now() and token/cacheDir variables.

In `@components/paper-trading/AccountSwitcher.tsx`:
- Around line 14-21: Props defines onAccountListChange but AccountSwitcher never
uses it; remove onAccountListChange from the Props interface and the
AccountSwitcher parameter list if you don't need the callback, or if you do need
it, wire it up by adding the callback parameter to the function signature
(AccountSwitcher) and invoking onAccountListChange() after account create/delete
flows (where createAccount, deleteAccount or similar handlers are implemented)
so the parent is notified; update any call sites that render <AccountSwitcher
... /> to pass the prop only if kept.

In `@lib/actions/ai-trading.actions.ts`:
- Around line 249-265: The price extraction from result.message using regex in
the buyStock/sellStock call sites is brittle; modify the buyStock and sellStock
functions to return structured data (e.g., { success, price, total, message })
and update the usage in ai-trading.actions (where buyStock, sellStock are called
and executed.push is built) to read price and total directly from the returned
object (use result.price and result.total), removing the regex parsing; if you
cannot change the underlying functions immediately, add a robust fallback:
attempt to read result.price/result.total first, then only as a last resort
parse result.message with a stricter pattern and handle parse failures by
flagging an error instead of defaulting to 0.

In `@lib/actions/finnhub.actions.ts`:
- Around line 47-53: Replace all synchronous fs calls (fs.existsSync,
fs.readFileSync, fs.mkdirSync, fs.writeFileSync) with their async Promise
counterparts to avoid blocking the event loop: use await fs.promises.readFile to
read cacheFile and JSON.parse the result, or await fs.promises.stat/access to
check existence, and use await fs.promises.mkdir(cacheDir, { recursive: true })
and await fs.promises.writeFile for writes; update the try/catch around the
cache logic that uses cacheFile and revalidateSeconds to await these operations
and preserve existing error handling and return types, and make the equivalent
changes for the other sync usages in this module (the blocks that currently call
fs.existsSync/readFileSync and mkdirSync/writeFileSync).

In `@lib/actions/paper-trading.actions.ts`:
- Around line 153-157: The code is using (account as any) casts because Mongoose
.lean() lost the type; fix by properly typing the lean result (e.g. call
.lean<PaperTradingAccount | null>() or assign the result to a variable typed as
PaperTradingAccount | null) when fetching the document in the functions that use
account (look for usages of findById(...).lean() and the local variable account
in lib/actions/paper-trading.actions.ts), then remove all (account as any) casts
and access properties directly (tradingPeriod, startDate, endDate,
customPeriodDays, name, and the other usages at the later locations referenced
in the review). Ensure other occurrences (lines ~179-188, 222, 225, 408,
460-462) are similarly updated by replacing explicit any casts with the correct
typed lean result or by narrowing the type before mutation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 551973cd-a201-4470-ab5f-4daf86449dbc

📥 Commits

Reviewing files that changed from the base of the PR and between 8326133 and de0b615.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (18)
  • .npmrc
  • _scripts/check_positions.js
  • _scripts/execute_trades.js
  • _scripts/research_candidates.js
  • _scripts/round2_trades.js
  • _scripts/round3_trades.js
  • _scripts/sector_analysis.js
  • app/(root)/paper-trading/page.tsx
  • components/paper-trading/AITradingPanel.tsx
  • components/paper-trading/AccountSwitcher.tsx
  • components/paper-trading/PaperTradingDashboard.tsx
  • database/models/paper-trading.model.ts
  • lib/actions/ai-trading.actions.ts
  • lib/actions/finnhub.actions.ts
  • lib/actions/paper-trading.actions.ts
  • lib/constants.ts
  • package.json
  • pnpm-workspace.yaml

Comment thread _scripts/execute_trades.js Outdated
Comment on lines +70 to +89
for (const o of orders) {
console.log('BUY ' + o.shares + ' x ' + o.sym + ' @ $' + o.price.toFixed(2) + ' = $' + o.cost.toFixed(2));
await trades.insertOne({
userId: USER_ID,
symbol: o.sym,
company: o.company,
type: 'BUY',
shares: o.shares,
price: o.price,
total: o.cost,
timestamp: new Date(),
});
}

// Update balance
const newBalance = +(acc.balance - totalCost).toFixed(2);
await accounts.updateOne(
{ _id: acc._id },
{ $set: { balance: newBalance, updatedAt: new Date() } }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Trade execution is not idempotent — re-running double-buys and corrupts the balance.

Each run unconditionally inserts new BUY documents and subtracts totalCost from balance. Running the script twice (a likely occurrence for a manual script) silently duplicates positions and double-debits cash, with no way to detect or roll back. Consider a guard such as a deterministic order/batch id (unique index on papertrades) or a "round already executed" check before inserting, and ideally perform the inserts + balance update inside a single MongoDB transaction so a mid-run failure doesn't leave trades recorded without the balance updated (or vice versa).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/execute_trades.js` around lines 70 - 89, This script currently
unconditionally inserts each BUY (inside the for loop using trades.insertOne)
and then updates the account balance with accounts.updateOne, causing duplicate
buys and double-debits when rerun; fix by generating a deterministic batchId (or
orderBatchId) for this run, add/require a unique index on the trades collection
keyed by batchId (and symbol/shares if needed), check for an existing marker via
trades.findOne({ batchId }) before inserting, and if not present perform all
inserts and the balance update inside a single MongoDB transaction (use a
session) so trades.insertOne and accounts.updateOne (referencing USER_ID,
totalCost and acc._id) are atomic and idempotent.

Comment thread components/paper-trading/AccountSwitcher.tsx
Comment thread components/paper-trading/AITradingPanel.tsx
Comment thread components/paper-trading/AITradingPanel.tsx Outdated
Comment on lines +95 to +111
const AIConfigSchema = new Schema<AITradingConfig>(
{
accountId: { type: String, required: true, unique: true, index: true },
userId: { type: String, required: true, index: true },
enabled: { type: Boolean, default: false },
apiEndpoint: { type: String, default: 'https://api.openai.com/v1/chat/completions' },
apiKey: { type: String, default: '' },
model: { type: String, default: 'gpt-4o' },
systemPrompt: { type: String, default: '' },
strategy: { type: String, enum: ['aggressive', 'moderate', 'conservative', 'custom'], default: 'moderate' },
maxPositionPct: { type: Number, default: 25, min: 1, max: 100 },
stopLossPct: { type: Number, default: -10, max: 0 },
tradingIntervalMin: { type: Number, default: 60, min: 5 },
lastTradeAt: { type: Date, default: null },
},
{ timestamps: true }
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

API keys are persisted in plaintext.

apiKey is stored as a raw String. Any DB dump, backup, or log of these documents leaks user-provided LLM credentials. Consider encrypting at rest (e.g. application-level envelope encryption) or storing only a reference to a secrets manager, and ensure this field is never returned to the client unredacted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@database/models/paper-trading.model.ts` around lines 95 - 111, The
AIConfigSchema currently persists apiKey as plaintext (field apiKey in
AIConfigSchema), so change storage to avoid plaintext: either replace apiKey
with a secret reference (e.g., secretRef) that holds an opaque ID to a secrets
manager, or implement application-level encryption via a Mongoose pre-save hook
on AIConfigSchema that encrypts apiKey with a KMS-derived key and decrypts only
when explicitly needed; additionally ensure apiKey is excluded from outgoing
documents by adding schema toJSON/toObject transforms (or use select: false) so
apiKey is never returned to clients, and update any codepaths that read/write
apiKey to use the secretRef or explicit decrypt helper.

Comment thread lib/actions/paper-trading.actions.ts
Comment thread lib/actions/paper-trading.actions.ts
Comment on lines +300 to +318
if (total > account.balance) {
return { success: false, error: `余额不足。需要 $${total.toFixed(2)},可用 $${account.balance.toFixed(2)}` };
}

// Update balance
await PaperAccount.updateOne({ _id: accountId }, { $inc: { balance: -total } });

// Record trade
await db.collection('papertrades').insertOne({
accountId,
userId: userId || account.userId,
symbol: symbol.toUpperCase(),
company: company || symbol,
type: 'BUY',
shares,
price: roundedPrice,
total,
timestamp: new Date(),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Race condition: balance check and decrement are not atomic.

total > account.balance is evaluated against a snapshot read at Line 288, then the balance is decremented separately at Line 305. Two concurrent buys (very plausible with AI auto-trading firing trades) can both pass the check and overdraw the account into a negative balance. Make the debit conditional and atomic, and verify it applied before recording the trade.

🔒 Atomic conditional debit
-        if (total > account.balance) {
-            return { success: false, error: `余额不足。需要 $${total.toFixed(2)},可用 $${account.balance.toFixed(2)}` };
-        }
-
-        // Update balance
-        await PaperAccount.updateOne({ _id: accountId }, { $inc: { balance: -total } });
+        // Atomic check-and-debit to avoid overdraw under concurrency
+        const debit = await PaperAccount.updateOne(
+            { _id: accountId, balance: { $gte: total } },
+            { $inc: { balance: -total } }
+        );
+        if (debit.modifiedCount === 0) {
+            return { success: false, error: `余额不足。需要 $${total.toFixed(2)},可用 $${account.balance.toFixed(2)}` };
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/paper-trading.actions.ts` around lines 300 - 318, The balance
check and separate decrement are racy; replace the two-step check-then-update
with an atomic conditional update on PaperAccount (e.g., use updateOne or
findOneAndUpdate with filter {_id: accountId, balance: {$gte: total}} and $inc:
{balance: -total}) and verify the update result (modifiedCount or returned
value) succeeded before calling db.collection('papertrades').insertOne; if the
atomic update failed, return the insufficient-funds error; keep accountId,
total, roundedPrice, symbol and the trade insert logic intact but only run the
insert after the successful conditional update.

Comment on lines +343 to +382
const holding = await db.collection('papertrades').aggregate([
{ $match: { accountId, symbol: symbol.toUpperCase() } },
{
$group: {
_id: '$symbol',
shares: { $sum: { $cond: [{ $eq: ['$type', 'BUY'] }, '$shares', { $multiply: ['$shares', -1] }] } },
company: { $first: '$company' },
}
},
]).toArray();

const held = holding[0]?.shares || 0;
if (held < shares) {
return { success: false, error: `持仓不足。持有 ${held} 股,尝试卖出 ${shares} 股` };
}

// Get current price
const { getQuote } = await import('@/lib/actions/finnhub.actions');
const quote = await getQuote(symbol);
const price = quote?.c || 0;
if (!price) return { success: false, error: `无法获取 ${symbol} 的实时价格` };

const roundedPrice = Math.round(price * 100) / 100;
const total = Math.round(shares * roundedPrice * 100) / 100;

// Update balance
await PaperAccount.updateOne({ _id: accountId }, { $inc: { balance: total } });

// Record trade
await db.collection('papertrades').insertOne({
accountId,
userId: userId || account.userId,
symbol: symbol.toUpperCase(),
company: holding[0]?.company || symbol,
type: 'SELL',
shares,
price: roundedPrice,
total,
timestamp: new Date(),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Same TOCTOU exposure on sells — holdings check and trade insert are not atomic.

The held-shares aggregation (Lines 343-357) and the SELL insert (Line 372) are independent steps, so concurrent sells of the same symbol can both pass the held < shares guard and oversell the position, leaving net holdings negative. Once the shares validation above is in place, consider serializing per-account trades or re-checking net holdings inside a transaction so positions cannot go negative.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/paper-trading.actions.ts` around lines 343 - 382, The sell flow
has a TOCTOU race: holdings are read via db.collection('papertrades').aggregate
and later a SELL insert (and PaperAccount.updateOne) happens outside an atomic
context; fix by doing the holdings check, balance update
(PaperAccount.updateOne) and trade insert
(db.collection('papertrades').insertOne) inside a single MongoDB
transaction/session so the holdings are re-read and validated within the same
transaction and concurrent sells cannot both pass. Concretely, start a client
session (session = client.startSession()), run session.withTransaction(async ()
=> { recompute holdings using
db.collection('papertrades').aggregate(...).toArray({ session }), if held <
shares throw; await PaperAccount.updateOne(..., { $inc: { balance: total } }, {
session }); await db.collection('papertrades').insertOne({...}, { session }) }),
and ensure all DB calls (aggregate, updateOne, insertOne) include the session
param and that you abort/handle errors from the transaction.

Comment thread pnpm-workspace.yaml
Comment on lines +1 to +6
allowBuilds:
protobufjs: set this to true or false
sharp: set this to true or false
unrs-resolver: set this to true or false
nodeLinker: hoisted
onlyBuiltDependencies: protobufjs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification: inspect current YAML value types/shape.
python - <<'PY'
import yaml, pathlib
p = pathlib.Path("pnpm-workspace.yaml")
data = yaml.safe_load(p.read_text())
print("allowBuilds:", type(data.get("allowBuilds")).__name__, data.get("allowBuilds"))
print("onlyBuiltDependencies:", type(data.get("onlyBuiltDependencies")).__name__, data.get("onlyBuiltDependencies"))
PY

Repository: Open-Dev-Society/OpenStock

Length of output: 254


🌐 Web query:

According to current pnpm documentation, what are the valid keys and value shapes for allowBuildsandonlyBuiltDependenciesinpnpm-workspace.yaml?

💡 Result:

In current pnpm documentation (v10.x/v11.x docs), pnpm-workspace.yaml supports: 1) allowBuilds Valid keys (how entries are written): - The keys are “package matchers” (package name patterns) such as esbuild or nx@21.6.4 || 21.6.5 and version-disjunction forms; you use matcher expressions as YAML map keys (example given: nx@21.6.4 || 21.6.5: true, and plain package names like core-js: false). [1][2] Valid value shape: - allowBuilds is a map: each matcher key maps to a boolean true (explicitly allow script execution) or false (explicitly disallow). [1] 2) onlyBuiltDependencies - The current pnpm documentation states that allowBuilds replaces onlyBuiltDependencies (and other related settings) and that those removed settings have been replaced by allowBuilds. Therefore onlyBuiltDependencies is no longer supported as a current/valid key in pnpm-workspace.yaml. [1] So, for “current pnpm documentation” validity: - Use allowBuilds with a YAML map from package matcher -> boolean. [1] - Do not use onlyBuiltDependencies; it has been replaced by allowBuilds in current docs. [1]

Citations:


Fix pnpm-workspace.yaml build policy schema (invalid allowBuilds values + unsupported onlyBuiltDependencies)

  • allowBuilds currently uses placeholder strings ("set this to true or false") but pnpm expects a map of package matcher -> boolean (true/false).
  • onlyBuiltDependencies is no longer a supported workspace setting in current pnpm; remove it and encode those restrictions in allowBuilds.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpm-workspace.yaml` around lines 1 - 6, Replace the placeholder strings
under allowBuilds with boolean values and remove the unsupported
onlyBuiltDependencies setting: change allowBuilds to a map like "allowBuilds:\n 
protobufjs: true\n  sharp: false\n  unrs-resolver: false" (or set true/false per
package policy you want) so each package matcher is mapped to a boolean, delete
the onlyBuiltDependencies line entirely, and keep nodeLinker: hoisted as-is;
update the allowBuilds entries for protobufjs, sharp and unrs-resolver
accordingly.

ericosmic added 3 commits May 30, 2026 10:21
…ments

P&L Calculation Fix
- Fixed weighted average cost method in getPositions/getAccountOverview
- Changed from totalCost/remainingShares to totalBuyCost/totalBuyShares * remainingShares
- Fixes incorrect avgCost and P&L after partial sells

US Market Hours System (lib/utils/market-hours.ts)
- NYSE/NASDAQ regular hours: Mon-Fri 9:30AM-4PM ET
- Auto DST detection (EDT/EST)
- 2026 US holiday calendar (10 holidays)
- getMarketStatus() returns open/closed/pre/post labels
- All trades restricted: market orders auto-convert to MARKET_ON_OPEN when closed

Pending Orders System
- New MongoDB collection: pendingorders
- Order types: LIMIT, STOP, MARKET_ON_OPEN
- Balance/holdings validation before order creation
- cron processes every 10min during market hours via auto-trading-monitor
- UI: PendingOrdersList component with cancel support

Dashboard UI Improvements
- Buy form: Market/Limit/Stop toggle with inline price inputs
- Sell panel slides out below the stock row (inline toggle)
- Market status indicator in buy form and sell panel headers
- Inline messages per operation section
- Slide-down animation (animate-slideDown)

AI Trading Fixes & Enhancements
- Fixed bug: handleToggle/handleRun passed userId instead of accountId
- Manual Run button disabled when market closed
- Market Open/Closed indicator in header
- Interval check: respects tradingIntervalMin from AI config
- Manual Run passes force=true (bypasses interval check)

Cron Updates
- auto-trading-monitor: 4 fixed times -> */10 21-3 * * 1-5
- Pending order processing merged into auto-trading-monitor

Cleanup
- Removed 5 stale one-time scripts
- Fixed process-pending-orders.js .env file path
- Script outputs only when orders execute (watchdog pattern)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/paper-trading/AITradingPanel.tsx (1)

385-394: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent market-closed check on inner "Run AI Trade" button.

The header Run button (line 214) disables itself when the market is closed:

disabled={running || (marketStatus !== null && !marketStatus.isOpen)}

But this expanded-panel button only checks running, allowing users to trigger AI trading when the market is closed.

Proposed fix
 {config?.enabled && (
     <button
         onClick={handleRun}
-        disabled={running}
+        disabled={running || (marketStatus !== null && !marketStatus.isOpen)}
         className="flex-1 bg-teal-600 hover:bg-teal-700 disabled:opacity-50 text-white py-2.5 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
     >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/paper-trading/AITradingPanel.tsx` around lines 385 - 394, The
inner "Run AI Trade" button in AITradingPanel allows running when the market is
closed because it only checks running; update its disabled condition to match
the header button by combining running with the marketStatus check (use the same
expression: running || (marketStatus !== null && !marketStatus.isOpen)) so the
button that calls handleRun is disabled when the market is closed; locate the
JSX for the expanded-panel button (the one rendering Play/Loader and using
config?.enabled and onClick={handleRun}) and replace its disabled prop
accordingly to ensure consistent behavior.
🧹 Nitpick comments (2)
lib/actions/pending-orders.actions.ts (1)

62-63: 💤 Low value

Redundant dynamic import of connectToDatabase.

connectToDatabase is already imported at the top of the file (line 3). This dynamic re-import is unnecessary.

♻️ Remove redundant import
-        const { connectToDatabase } = await import('`@/database/mongoose`');
-        await connectToDatabase();
+        // connectToDatabase already called via getPendingOrdersCollection()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/actions/pending-orders.actions.ts` around lines 62 - 63, The code
dynamically re-imports connectToDatabase even though it's already statically
imported at the top of pending-orders.actions.ts; remove the redundant lines
"const { connectToDatabase } = await import('`@/database/mongoose`');" and "await
connectToDatabase();" and instead call the existing connectToDatabase() directly
where needed (e.g., inside the function that currently performs the dynamic
import) to avoid duplicate imports and unnecessary await/import overhead.
_scripts/process-pending-orders.js (1)

25-52: ⚖️ Poor tradeoff

Market hours logic is duplicated from market-hours.ts.

This script reimplements market hours checking with a simplified DST approximation (month >= 3 && month <= 11) that differs from the main implementation. Changes to holiday lists or DST rules must be updated in both places.

Consider importing the shared utility or extracting to a shared CommonJS module if this script needs to remain standalone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@_scripts/process-pending-orders.js` around lines 25 - 52, The isMarketOpen
function duplicates market-hours logic and uses a simplified DST rule; replace
this reimplementation by importing and using the centralized market hours
utility (e.g., the exported function from market-hours.ts) or move the shared
logic into a CommonJS module that both scripts require; update this file to call
that shared function instead of the local isMarketOpen, remove the local
holiday/DST code, and ensure the import/require references the exact exported
symbol name from market-hours (or the new shared module) so DST rules and
holiday lists remain single-source-of-truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@_scripts/process-pending-orders.sh`:
- Around line 1-5: Replace the hardcoded absolute path used in the cd command in
_scripts/process-pending-orders.sh (currently "cd /Volumes/ks500/OpenStock")
with a portable lookup: compute the project root relative to the script location
(using the script's directory via dirname "$0") or fall back to an environment
variable like PROJECT_ROOT, then cd into that root before running exec node
_scripts/process-pending-orders.js so the script works across machines, CI and
production.

In `@app/globals.css`:
- Line 135: Keyframe name uses camelCase "slideDown" which violates kebab-case
linting; rename the keyframe to "slide-down" and update its reference in the CSS
variable --animate-slide-down (and the corresponding `@keyframes` declaration) so
both the animation variable and the `@keyframes` identifier match the new
kebab-case name (ensure you update any other occurrences of "slideDown" in the
file).

In `@components/paper-trading/PendingOrdersList.tsx`:
- Around line 36-42: The switch in function orderTypeLabel
(PendingOrdersList.tsx) can fall through and return undefined for unknown
PendingOrderType values; update orderTypeLabel to include a default case that
returns a safe descriptive string (e.g., `Unknown order type: ${o.orderType}` or
a localized "Unknown") so the function always returns a string and the Condition
column never renders empty, ensuring you reference the existing cases 'LIMIT',
'STOP', and 'MARKET_ON_OPEN' when adding the default.

In `@lib/actions/pending-orders.actions.ts`:
- Around line 275-292: The function processAllPendingOrders never increments the
failed counter; update the for-loop that calls attemptExecuteOrder(order._id) so
that when ok is false you increment failed, and also wrap the await
attemptExecuteOrder call in a try/catch so any thrown errors increment failed
and do not stop processing remaining orders; reference the failed variable, the
for (const order of orders) loop, and attemptExecuteOrder to locate where to add
the increment and error handling.
- Around line 195-269: attemptExecuteOrder suffers a TOCTOU race: it reads a
PENDING order then executes buyStock/sellStock allowing concurrent workers to
double-execute; replace the initial read+check with an atomic claim using
getPendingOrdersCollection().findOneAndUpdate({ _id: new
mongoose.Types.ObjectId(orderId), status: 'PENDING' }, { $set: { status:
'EXECUTING', claimedAt: new Date() } }, { returnDocument: 'after' }) and if that
returns null, return false; proceed using the returned document (use its _id) to
call buyStock/sellStock and then update to EXECUTED or FAILED (and include
failReason) — also ensure to revert or mark FAILED if execution throws,
referencing function attemptExecuteOrder and helpers buyStock/sellStock.

---

Outside diff comments:
In `@components/paper-trading/AITradingPanel.tsx`:
- Around line 385-394: The inner "Run AI Trade" button in AITradingPanel allows
running when the market is closed because it only checks running; update its
disabled condition to match the header button by combining running with the
marketStatus check (use the same expression: running || (marketStatus !== null
&& !marketStatus.isOpen)) so the button that calls handleRun is disabled when
the market is closed; locate the JSX for the expanded-panel button (the one
rendering Play/Loader and using config?.enabled and onClick={handleRun}) and
replace its disabled prop accordingly to ensure consistent behavior.

---

Nitpick comments:
In `@_scripts/process-pending-orders.js`:
- Around line 25-52: The isMarketOpen function duplicates market-hours logic and
uses a simplified DST rule; replace this reimplementation by importing and using
the centralized market hours utility (e.g., the exported function from
market-hours.ts) or move the shared logic into a CommonJS module that both
scripts require; update this file to call that shared function instead of the
local isMarketOpen, remove the local holiday/DST code, and ensure the
import/require references the exact exported symbol name from market-hours (or
the new shared module) so DST rules and holiday lists remain
single-source-of-truth.

In `@lib/actions/pending-orders.actions.ts`:
- Around line 62-63: The code dynamically re-imports connectToDatabase even
though it's already statically imported at the top of pending-orders.actions.ts;
remove the redundant lines "const { connectToDatabase } = await
import('`@/database/mongoose`');" and "await connectToDatabase();" and instead
call the existing connectToDatabase() directly where needed (e.g., inside the
function that currently performs the dynamic import) to avoid duplicate imports
and unnecessary await/import overhead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97a4c347-63d0-49d5-bded-86c661d41d9f

📥 Commits

Reviewing files that changed from the base of the PR and between de0b615 and d82aaf2.

📒 Files selected for processing (11)
  • _scripts/check_positions.js
  • _scripts/process-pending-orders.js
  • _scripts/process-pending-orders.sh
  • app/globals.css
  • components/paper-trading/AITradingPanel.tsx
  • components/paper-trading/PaperTradingDashboard.tsx
  • components/paper-trading/PendingOrdersList.tsx
  • lib/actions/ai-trading.actions.ts
  • lib/actions/paper-trading.actions.ts
  • lib/actions/pending-orders.actions.ts
  • lib/utils/market-hours.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/actions/paper-trading.actions.ts

Comment thread _scripts/process-pending-orders.js Outdated
Comment thread _scripts/process-pending-orders.sh Outdated
Comment thread app/globals.css Outdated
Comment thread components/paper-trading/PendingOrdersList.tsx
Comment thread lib/actions/pending-orders.actions.ts
Comment thread lib/actions/pending-orders.actions.ts
- Replace static 2026 holiday array with runtime calculation for any year
- Gauss algorithm for Easter Sunday → Good Friday
- Weekend observance rules (Sat→Fri, Sun→Mon)
- Works automatically for 2026, 2027, and beyond
- Also updated standalone process-pending-orders.js to match

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/utils/market-hours.ts (1)

217-219: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Weekend nextOpen message can be incorrect on Monday holidays

Line 218 always says Monday 9:30 AM ET. That is wrong on long weekends where Monday is a market holiday (e.g., Presidents’ Day), where next open is Tuesday.

Minimal safe fix
-        return { isOpen: false, label: 'Market Closed — Weekend', nextOpen: 'Monday 9:30 AM ET' };
+        return { isOpen: false, label: 'Market Closed — Weekend', nextOpen: 'Next trading day 9:30 AM ET' };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/utils/market-hours.ts` around lines 217 - 219, The weekend branch
currently returns a hard-coded 'Monday 9:30 AM ET' which fails on Monday market
holidays; update the branch in lib/utils/market-hours.ts (where et.dayOfWeek is
checked and the returned object includes nextOpen) to compute the next trading
day instead of hard-coding Monday: iterate from the following day until a date
that is not a weekend and not a market holiday (use the existing holiday check
helper, e.g., isMarketHoliday or the holidays list), then format and return that
weekday name with "9:30 AM ET" as nextOpen (keep isOpen: false and the label).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/utils/market-hours.ts`:
- Around line 17-23: nthWeekday is incorrectly adding an extra week when the
month's 1st is the target weekday because of the conditional "if (offset === 0
&& n > 1) offset = 7"; remove that conditional and let offset = (weekday - day +
7) % 7 stand as-is, keep the date calculation date = 1 + offset + (n - 1) * 7,
and optionally validate that n >= 1 to avoid invalid inputs; update the function
nthWeekday to eliminate the special-case offset adjustment so nth occurrences
are computed correctly.

---

Outside diff comments:
In `@lib/utils/market-hours.ts`:
- Around line 217-219: The weekend branch currently returns a hard-coded 'Monday
9:30 AM ET' which fails on Monday market holidays; update the branch in
lib/utils/market-hours.ts (where et.dayOfWeek is checked and the returned object
includes nextOpen) to compute the next trading day instead of hard-coding
Monday: iterate from the following day until a date that is not a weekend and
not a market holiday (use the existing holiday check helper, e.g.,
isMarketHoliday or the holidays list), then format and return that weekday name
with "9:30 AM ET" as nextOpen (keep isOpen: false and the label).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c2b26de3-b20a-4059-9e58-993961684500

📥 Commits

Reviewing files that changed from the base of the PR and between e13a19e and 7f4d33c.

📒 Files selected for processing (1)
  • lib/utils/market-hours.ts

Comment thread lib/utils/market-hours.ts
ericosmic and others added 3 commits May 30, 2026 11:12
- fix nthWeekday bug when 1st of month matches target weekday
- add 30s timeout to LLM API call in runAITradeCycle
- fix failed counter not being incremented in processAllPendingOrders
- add TOCTOU protection to attemptExecuteOrder (atomic claim via findOneAndUpdate)
- add default case to PendingOrdersList order type switch
- rename slideDown keyframe to kebab-case slide-down
- AccountSwitcher: stale React state after account deletion (used fresh API call
  instead of filtering stale accounts state)
- finnhub.actions fetchJSON: cache write failure no longer discards successful
  data (wrapped in non-fatal try/catch)
- finnhub.actions getQuote: token variable moved out of try block so stale cache
  fallback in catch block actually works (was ReferenceError)
- paper-trading.actions buyStock/sellStock: added share count validation
  (reject non-integer, 0, or negative shares)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a full paper-trading feature set (accounts, simulated trades, pending orders, and an AI-assisted auto-trading panel), plus supporting utilities (market-hours detection, Finnhub fetching/caching) and a new navigation entry.

Changes:

  • Added paper trading dashboard UI with multi-account support, manual trading, pending orders, and history/positions views.
  • Implemented server-side paper trading actions + MongoDB models (accounts, trades, AI config) and a pending-order execution engine.
  • Added market-hours utilities and enhanced Finnhub fetching with timeout/proxy support and a local file cache.

Reviewed changes

Copilot reviewed 16 out of 18 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Adds pnpm workspace config (currently invalid/misplaced content).
package.json Adds undici dependency for proxy-capable fetch usage.
lib/utils/market-hours.ts Introduces US market-hours/holiday helpers used by trading logic/UI.
lib/constants.ts Adds “Paper Trading” to navigation items.
lib/actions/pending-orders.actions.ts Implements pending orders CRUD + execution processing (contains critical execution-state bugs).
lib/actions/paper-trading.actions.ts Implements paper accounts, simulated buy/sell, overview, positions, history, reset/period logic.
lib/actions/finnhub.actions.ts Adds file-based caching + timeout/proxy support for Finnhub calls.
lib/actions/ai-trading.actions.ts Implements AI config CRUD and a trade-cycle runner that calls an LLM + executes simulated trades.
database/models/paper-trading.model.ts Adds Mongoose schemas for paper accounts, trades, and AI trading config.
components/paper-trading/PendingOrdersList.tsx UI to display/cancel pending orders.
components/paper-trading/PaperTradingDashboard.tsx Main paper trading dashboard UI (buy/sell forms, holdings, pending orders, history, setup/reset).
components/paper-trading/AITradingPanel.tsx UI for configuring and manually running AI trading.
components/paper-trading/AccountSwitcher.tsx UI for switching/creating/deleting paper accounts.
app/globals.css Adds slide-down keyframes / animation variable for UI transitions.
app/(root)/paper-trading/page.tsx Adds the paper trading page and loads/creates the active account.
.npmrc Configures onlyBuiltDependencies list.
.gitignore Ignores /_scripts/ helper scripts directory.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pnpm-workspace.yaml
Comment on lines +1 to +6
allowBuilds:
protobufjs: set this to true or false
sharp: set this to true or false
unrs-resolver: set this to true or false
nodeLinker: hoisted
onlyBuiltDependencies: protobufjs
Comment thread lib/actions/pending-orders.actions.ts Outdated
Comment on lines +199 to +206
// Atomically claim the order (prevents TOCTOU race when two processes try to execute the same order)
const order = await coll.findOneAndUpdate(
{ _id: new mongoose.Types.ObjectId(orderId), status: 'PENDING' },
{ $set: { status: 'PROCESSING', processingStartedAt: new Date() } },
{ returnDocument: 'before' }
);
if (!order) return false; // Already claimed or doesn't exist

Comment thread lib/actions/pending-orders.actions.ts Outdated
Comment on lines +207 to +214
// Market must be open to execute
if (!isMarketOpen()) return false;

// Get current price
const { getQuote } = await import('@/lib/actions/finnhub.actions');
const quote = await getQuote(order.symbol);
const currentPrice = quote?.c || 0;
if (!currentPrice) return false;
Comment thread lib/actions/pending-orders.actions.ts Outdated
break;
}

if (!shouldExecute) return false;
Comment on lines +58 to +66
const coll = await getPendingOrdersCollection();
const sym = symbol.toUpperCase();

// ── Balance / Holdings Check ──
const { connectToDatabase } = await import('@/database/mongoose');
await connectToDatabase();
const db = mongoose.connection.db;
if (!db) return { success: false, error: 'Database connection failed' };

Comment thread lib/actions/ai-trading.actions.ts Outdated
Comment on lines +220 to +224
const proxyUrl = process.env.HTTPS_PROXY || 'http://127.0.0.1:7890';
const { ProxyAgent } = await import('undici');
const undici = await import('undici');

const dispatcher = new ProxyAgent(proxyUrl);
Comment thread lib/actions/ai-trading.actions.ts
Comment thread lib/actions/ai-trading.actions.ts
Comment thread lib/actions/ai-trading.actions.ts Outdated
Comment thread lib/actions/finnhub.actions.ts Outdated
ericosmic added 3 commits June 2, 2026 00:48
- Add scheduled AI trading execution through checkAITrading
- Support API key configuration and connection testing
- Enable automatic activation after successful API validation
- Add DeepSeek reasoning model compatibility
- Support proxy configuration for outbound network access
- Add symbol-based filtering to trade history
- Replace manual execution with background automation
- Enhance error handling and user notifications
@ericosmic

Copy link
Copy Markdown
Author

Hi! It seems the Vercel check is blocked waiting for authorization. Could you please help review and trigger the deployment check? Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants