diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..76a8641 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Copy to .env (untracked) at the repository root. +# IPADS Responses API key for the web watchlist agent; blank uses demo data. +IPADS_API_KEY= +# Proxy URL for the live monitor's market-data fetches (deploy/start-monitor.sh). +QUANT_MARKET_PROXY= diff --git a/.gitignore b/.gitignore index 3a67b32..c465801 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ data/*_unavailable.json logs/ memory/ .tmp_finance/ + +# Web UI state and precomputed backtests (web/src/server/paths.ts) +data/web/ +data/precomputed/ diff --git a/bridge.py b/bridge.py index 66c4484..fbb8803 100644 --- a/bridge.py +++ b/bridge.py @@ -1,5 +1,5 @@ """ -Bridge script for stock-agent integration. +Bridge between the Python engine and the web API (web/src/server/strategyService.ts). Outputs JSON to stdout; all logs go to stderr. @@ -1186,7 +1186,7 @@ def cmd_live(args: argparse.Namespace) -> None: # --------------------------------------------------------------------------- def main() -> None: - parser = argparse.ArgumentParser(description="Quant bridge for stock-agent") + parser = argparse.ArgumentParser(description="Quant bridge for the web API") subparsers = parser.add_subparsers(dest="command") # list diff --git a/precompute_backtest.py b/precompute_backtest.py index 2eb737d..167c625 100644 --- a/precompute_backtest.py +++ b/precompute_backtest.py @@ -326,7 +326,7 @@ def main(): parser = argparse.ArgumentParser(description="Pre-compute backtest results") parser.add_argument( "--output", default=None, - help="Output directory (default: ../stock-agent/data/precomputed)", + help="Output directory (default: data/precomputed, read by the web UI)", ) parser.add_argument( "--years", type=int, default=10, @@ -335,7 +335,7 @@ def main(): args = parser.parse_args() output_dir = args.output or os.path.join( - os.path.dirname(os.path.abspath(__file__)), "..", "stock-agent", "data", "precomputed" + os.path.dirname(os.path.abspath(__file__)), "data", "precomputed" ) os.makedirs(output_dir, exist_ok=True) diff --git a/pyproject.toml b/pyproject.toml index 5818f4b..0d643e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "quant" +name = "quant-trading" version = "0.1.0" description = "Add your description here" readme = "README.md" diff --git a/uv.lock b/uv.lock index e86a37d..6d9f272 100644 --- a/uv.lock +++ b/uv.lock @@ -761,7 +761,7 @@ wheels = [ ] [[package]] -name = "quant" +name = "quant-trading" version = "0.1.0" source = { virtual = "." } dependencies = [ diff --git a/web/.env.example b/web/.env.example deleted file mode 100644 index 9a6377c..0000000 --- a/web/.env.example +++ /dev/null @@ -1 +0,0 @@ -IPADS_API_KEY= diff --git a/web/package-lock.json b/web/package-lock.json index 65b6e16..25afb13 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "personal-stock-agent", + "name": "quant-trading-web", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "personal-stock-agent", + "name": "quant-trading-web", "version": "0.1.0", "dependencies": { "@vitejs/plugin-react": "^5.0.0", diff --git a/web/package.json b/web/package.json index d055849..39cb8ea 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "name": "personal-stock-agent", + "name": "quant-trading-web", "version": "0.1.0", "private": true, "type": "module", diff --git a/web/src/server/index.ts b/web/src/server/index.ts index b83701e..9594b6e 100644 --- a/web/src/server/index.ts +++ b/web/src/server/index.ts @@ -1,4 +1,4 @@ -import "dotenv/config"; +import { PRECOMPUTED_DIR, WEB_DATA_DIR } from "./paths"; import cors from "cors"; import express from "express"; import { existsSync, readFileSync } from "node:fs"; @@ -15,11 +15,10 @@ import { listStrategies, runBacktest, getCachedResult, getLiveStrategies } from const port = Number(process.env.PORT ?? 8787); const host = process.env.HOST ?? "127.0.0.1"; -const dataDir = process.env.STOCK_AGENT_DATA_DIR ?? join(process.cwd(), "data"); -const watchlistStore = new WatchlistStore(join(dataDir, "watchlist.json")); +const watchlistStore = new WatchlistStore(join(WEB_DATA_DIR, "watchlist.json")); const dataProvider = createProvider(loadIpadsConfig().apiKey); -const stockDataCache = new StockDataCache(join(dataDir, "cache"), dataProvider); +const stockDataCache = new StockDataCache(join(WEB_DATA_DIR, "cache"), dataProvider); const app = express(); let cachedReportUpdatedAt = 0; @@ -157,7 +156,7 @@ app.get("/api/strategies", async (_request, response, next) => { }); function loadPrecomputedFile() { - const precomputedPath = join(dataDir, "precomputed", "backtest_results.json"); + const precomputedPath = join(PRECOMPUTED_DIR, "backtest_results.json"); if (!existsSync(precomputedPath)) { throw new Error("Pre-computed results not available. Run precompute_backtest.py first."); } diff --git a/web/src/server/paths.ts b/web/src/server/paths.ts new file mode 100644 index 0000000..9cf0b2b --- /dev/null +++ b/web/src/server/paths.ts @@ -0,0 +1,14 @@ +import { config } from "dotenv"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +// web/src/server/ → repository root, which also holds bridge.py and the Python engine. +export const REPO_ROOT = resolve(fileURLToPath(new URL("../../..", import.meta.url))); + +// Secrets such as IPADS_API_KEY live in the single untracked repo-root .env. +config({ path: join(REPO_ROOT, ".env"), quiet: true }); + +// Web-only state: watchlist, per-stock cache and backtest result cache. +export const WEB_DATA_DIR = join(REPO_ROOT, "data", "web"); +// Written by precompute_backtest.py, read by the strategy page. +export const PRECOMPUTED_DIR = join(REPO_ROOT, "data", "precomputed"); diff --git a/web/src/server/strategyService.ts b/web/src/server/strategyService.ts index bc206e3..76b410f 100644 --- a/web/src/server/strategyService.ts +++ b/web/src/server/strategyService.ts @@ -1,11 +1,11 @@ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, unlinkSync } from "node:fs"; -import { join, resolve } from "node:path"; +import { join } from "node:path"; +import { REPO_ROOT, WEB_DATA_DIR } from "./paths"; import type { BacktestConfig, BacktestResult, LiveStrategyState, StrategyInfo } from "../shared/strategyTypes.js"; -const QUANT_DIR = resolve(process.env.QUANT_PROJECT_DIR || join(process.cwd(), "../quant")); -const CACHE_DIR = join(process.env.STOCK_AGENT_DATA_DIR || join(process.cwd(), "data"), "strategy_cache"); +const CACHE_DIR = join(WEB_DATA_DIR, "strategy_cache"); const TIMEOUT_MS = Number(process.env.QUANT_BACKTEST_TIMEOUT_MS) || 180_000; const CACHE_TTL_MS = 24 * 60 * 60 * 1000; const MAX_CACHE_FILES = 50; @@ -37,7 +37,7 @@ function stableStringify(value: unknown): string { function runBridge(args: string[]): Promise { return new Promise((resolve, reject) => { const proc = spawn("uv", ["run", "python", "bridge.py", ...args], { - cwd: QUANT_DIR, + cwd: REPO_ROOT, env: { ...process.env, PYTHONUNBUFFERED: "1" }, });