refactor: wire web to the in-repo engine and shared data dir
- web/src/server/paths.ts resolves the repo root from its own location, loads the single repo-root .env, and defines data/web/ (watchlist, stock cache, backtest cache) and data/precomputed/. - The API spawns bridge.py at the repo root instead of ../quant. - precompute_backtest.py writes to data/precomputed/ instead of ../stock-agent/data/precomputed. - Rename the project to quant-trading (web package quant-trading-web). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.env.example
Normal file
5
.env.example
Normal file
@@ -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=
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -55,3 +55,7 @@ data/*_unavailable.json
|
|||||||
logs/
|
logs/
|
||||||
memory/
|
memory/
|
||||||
.tmp_finance/
|
.tmp_finance/
|
||||||
|
|
||||||
|
# Web UI state and precomputed backtests (web/src/server/paths.ts)
|
||||||
|
data/web/
|
||||||
|
data/precomputed/
|
||||||
|
|||||||
@@ -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.
|
Outputs JSON to stdout; all logs go to stderr.
|
||||||
|
|
||||||
@@ -1186,7 +1186,7 @@ def cmd_live(args: argparse.Namespace) -> None:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def main() -> 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")
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
|
||||||
# list
|
# list
|
||||||
|
|||||||
@@ -326,7 +326,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description="Pre-compute backtest results")
|
parser = argparse.ArgumentParser(description="Pre-compute backtest results")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output", default=None,
|
"--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(
|
parser.add_argument(
|
||||||
"--years", type=int, default=10,
|
"--years", type=int, default=10,
|
||||||
@@ -335,7 +335,7 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
output_dir = args.output or os.path.join(
|
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)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "quant"
|
name = "quant-trading"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@@ -761,7 +761,7 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quant"
|
name = "quant-trading"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
IPADS_API_KEY=
|
|
||||||
4
web/package-lock.json
generated
4
web/package-lock.json
generated
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "personal-stock-agent",
|
"name": "quant-trading-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "personal-stock-agent",
|
"name": "quant-trading-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitejs/plugin-react": "^5.0.0",
|
"@vitejs/plugin-react": "^5.0.0",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "personal-stock-agent",
|
"name": "quant-trading-web",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import "dotenv/config";
|
import { PRECOMPUTED_DIR, WEB_DATA_DIR } from "./paths";
|
||||||
import cors from "cors";
|
import cors from "cors";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
@@ -15,11 +15,10 @@ import { listStrategies, runBacktest, getCachedResult, getLiveStrategies } from
|
|||||||
|
|
||||||
const port = Number(process.env.PORT ?? 8787);
|
const port = Number(process.env.PORT ?? 8787);
|
||||||
const host = process.env.HOST ?? "127.0.0.1";
|
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 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();
|
const app = express();
|
||||||
|
|
||||||
let cachedReportUpdatedAt = 0;
|
let cachedReportUpdatedAt = 0;
|
||||||
@@ -157,7 +156,7 @@ app.get("/api/strategies", async (_request, response, next) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadPrecomputedFile() {
|
function loadPrecomputedFile() {
|
||||||
const precomputedPath = join(dataDir, "precomputed", "backtest_results.json");
|
const precomputedPath = join(PRECOMPUTED_DIR, "backtest_results.json");
|
||||||
if (!existsSync(precomputedPath)) {
|
if (!existsSync(precomputedPath)) {
|
||||||
throw new Error("Pre-computed results not available. Run precompute_backtest.py first.");
|
throw new Error("Pre-computed results not available. Run precompute_backtest.py first.");
|
||||||
}
|
}
|
||||||
|
|||||||
14
web/src/server/paths.ts
Normal file
14
web/src/server/paths.ts
Normal file
@@ -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");
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
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";
|
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(WEB_DATA_DIR, "strategy_cache");
|
||||||
const CACHE_DIR = join(process.env.STOCK_AGENT_DATA_DIR || join(process.cwd(), "data"), "strategy_cache");
|
|
||||||
const TIMEOUT_MS = Number(process.env.QUANT_BACKTEST_TIMEOUT_MS) || 180_000;
|
const TIMEOUT_MS = Number(process.env.QUANT_BACKTEST_TIMEOUT_MS) || 180_000;
|
||||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||||
const MAX_CACHE_FILES = 50;
|
const MAX_CACHE_FILES = 50;
|
||||||
@@ -37,7 +37,7 @@ function stableStringify(value: unknown): string {
|
|||||||
function runBridge(args: string[]): Promise<unknown> {
|
function runBridge(args: string[]): Promise<unknown> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const proc = spawn("uv", ["run", "python", "bridge.py", ...args], {
|
const proc = spawn("uv", ["run", "python", "bridge.py", ...args], {
|
||||||
cwd: QUANT_DIR,
|
cwd: REPO_ROOT,
|
||||||
env: { ...process.env, PYTHONUNBUFFERED: "1" },
|
env: { ...process.env, PYTHONUNBUFFERED: "1" },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user