diff --git a/README.md b/README.md index 95cf2cb..8d25e47 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ Each surface has its own `.env.example`. The root `.env.example` is the comprehe | `PLATFORM_JWT_PUBLIC_KEY` or `PLATFORM_JWT_JWKS_URL` | Prod | Platform JWT verification key | | `JWT_SECRET` | Dev | Legacy/local JWT secret when `PLATFORM_AUTH_ENABLED=false` | | `ALPACA_API_KEY` / `ALPACA_API_SECRET` | For trading | Exchange credentials | +| `FMP_API_KEY` | For research/screener | Financial Modeling Prep key required by `/api/research/*` and `/api/screener`; the shared `demo` key is intentionally rejected by the backend | | `OPENAI_API_KEY` | For AI rules | Primary LLM provider | | `ENABLE_TRADING` | — | Set `true` to enable live order execution (default `false`) | | `PAPER_TRADING` | — | Set `true` for paper mode | diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..4fd6cf9 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,128 @@ +# ============================================================================= +# ByteLyst Trading — Backend Environment Configuration +# Copy this file to .env and fill in your values. +# Production: pull secrets from Azure Key Vault (see docs/AZURE_INFRASTRUCTURE.md) +# ============================================================================= + +# --- Server --- +API_PORT=4018 +CORS_ALLOWED_ORIGINS=http://localhost:5173 + +# --- Platform Auth (required for production) --- +# Set PLATFORM_AUTH_ENABLED=true to require platform JWT (RS256) verification. +# For local dev with legacy Supabase JWTs, set false and provide JWT_SECRET. +PLATFORM_AUTH_ENABLED=false +PLATFORM_API_URL=http://localhost:4003/api +PLATFORM_JWT_ISSUER=bytelyst-platform +PLATFORM_JWT_PUBLIC_KEY= +PLATFORM_JWT_JWKS_URL= +JWT_SECRET=change-me-for-local-dev + +# --- Azure Key Vault (optional — skipped if unset) --- +# Set to trigger automatic secret resolution at startup via DefaultAzureCredential. +# Pulls invttrdg-* secrets into the env vars below before the app starts. +AZURE_KEYVAULT_URL= + +# --- Cosmos DB (primary data store) --- +# Pulled from Key Vault in production (invttrdg-cosmos-*). +COSMOS_ENDPOINT=https://cosmos-mywisprai.documents.azure.com:443/ +COSMOS_KEY=your_cosmos_key +COSMOS_DATABASE=invttrdg + +# --- Azure OpenAI (AI Foundry — preferred over direct OpenAI in production) --- +# Pulled from Key Vault in production (invttrdg-azure-openai-*). +# When set, LLM_PROVIDER is auto-detected as 'azure'. +AZURE_OPENAI_ENDPOINT= +AZURE_OPENAI_KEY= +AZURE_OPENAI_DEPLOYMENT=gpt-4o + +# --- Plug-and-Play Configuration --- +PROVIDER=alpaca # Options: 'alpaca' or 'ccxt' + +# Asset Details +SYMBOL=BTC/USD +TIMEFRAME=1Min +POLLING_INTERVAL=60000 + +# --- Alpaca Settings --- +ALPACA_API_KEY=your_key +ALPACA_API_SECRET=your_secret +PAPER_TRADING=true +ASSET_CLASS=crypto # 'crypto' or 'us_equity' + +# --- Research Data (Financial Modeling Prep) --- +# Required for /api/research/* and /api/screener. Free tier: 250 req/day. +# Register at https://financialmodelingprep.com/developer/docs; do not use the +# shared "demo" key outside ad-hoc manual experiments. +FMP_API_KEY=your_fmp_key + +# --- CCXT Settings --- +EXCHANGE=binance +CCXT_API_KEY=your_key +CCXT_API_SECRET=your_secret + +# --- Notifications --- +WEBHOOK_URL=https://discord.com/api/webhooks/your/url + +# --- AI Configuration --- +AI_PROVIDER=perplexity +PERPLEXITY_API_KEY=your_perplexity_key +OPENAI_API_KEY=your_openai_key +GEMINI_API_KEY=your_gemini_key +AI_FALLBACK_LIST=perplexity,openai,gemini +AI_MODEL=sonar +AI_CONFIDENCE_THRESHOLD=70 +AI_CACHE_HOURS=4 + +# --- Features --- +LOW_STRESS_MODE=false +ENABLE_TREND_ALERTS=true +ENABLE_PULSE_ALERTS=true +ENABLE_TRADING=true +TOTAL_CAPITAL=1000 +MAX_OPEN_TRADES=3 + +# Feature flags (opt-out: omit or true = enabled, false = disabled) +ENABLE_BACKTEST=false +BACKTEST_CUSTOMER_ENABLED=false +TAB_MARKETPLACE_ENABLED=true +TAB_MEMBERSHIP_ENABLED=true + +ENABLE_STRICT_CAPITAL_GUARD=true +STRICT_CAPITAL_SLIPPAGE_BUFFER_PCT=1.0 +STRICT_CAPITAL_FEE_BUFFER_PCT=0.15 +STRICT_CAPITAL_MIN_RESERVE_USD=0 + +# --- Alpaca Omnibus Sub-tagging --- +ENABLE_ALPACA_SUBTAG=false +SUBTAG_OMNIBUS_ONLY=true +ALPACA_SUBTAG_ENV=paper +ALPACA_SUBTAG_MAX_LENGTH=48 +ALPACA_SUBTAG_DISABLE_FOR_EXCHANGE= +ALPACA_OMNIBUS_PROFILE_ALLOWLIST= + +# --- Reconciliation EXIT Backfill Safety --- +ENABLE_RECON_EXIT_BACKFILL=true +RECON_EXIT_BACKFILL_DRY_RUN=true +RECON_EXIT_BACKFILL_REQUIRE_PAUSE=true +RECON_EXIT_BACKFILL_DUST_ABS_QTY=0.001 +RECON_EXIT_BACKFILL_DUST_REL_PCT=0.002 +RECON_EXIT_BACKFILL_LOOKBACK_HOURS=72 +RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION=true +RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH=false +RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES=5 + +# --- Reconciliation Parity Watchdog Auto-Resume --- +ENABLE_RECON_WATCHDOG_AUTO_RESUME=true +RECON_WATCHDOG_AUTO_RESUME_MIN_PAUSE_MS=900000 +RECON_WATCHDOG_AUTO_RESUME_CLEAN_CYCLES=2 +RECON_WATCHDOG_AUTO_RESUME_COOLDOWN_MS=1800000 + +# --- Supabase (legacy fallback — Cosmos DB is primary) --- +# Required only if Cosmos DB is not configured or for legacy auth fallback. +SUPABASE_URL=your_supabase_url +SUPABASE_KEY=your_supabase_key + +# --- Product Identity --- +# Auto-set from Key Vault (invttrdg-product-id) in production; default is fine for local dev. +PRODUCT_ID=invttrdg diff --git a/backend/.gitignore b/backend/.gitignore index e9d9a97..d79a55d 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,8 +1,8 @@ node_modules -! .env.example .env .env.* *.env +!.env.example old_env.txt new_env.txt temp_env.txt diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 7e64d49..0e17fe2 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -61,7 +61,7 @@ export const config = { // Research data — Financial Modeling Prep (free tier: 250 req/day) // Register free at https://financialmodelingprep.com/developer/docs - FMP_API_KEY: process.env.FMP_API_KEY || 'demo', + FMP_API_KEY: process.env.FMP_API_KEY || '', // Supabase SUPABASE_URL: process.env.SUPABASE_URL || '', diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index d3e8364..7b12ba8 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -130,6 +130,15 @@ const normalizeNewsSymbolsQuery = (value: unknown): string => { return symbols.join(','); }; +const getConfiguredFmpApiKey = (): string => { + const apiKey = config.FMP_API_KEY.trim(); + if (!apiKey || apiKey.toLowerCase() === 'demo') { + throw new Error('FMP_API_KEY is required for research and screener endpoints'); + } + + return apiKey; +}; + interface TradeAuditEvent { event: string; userId?: string; @@ -2805,7 +2814,7 @@ RULES: try { const symbol = String(req.query.symbol || '').trim().toUpperCase(); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - const apiKey = process.env.FMP_API_KEY || 'demo'; + const apiKey = getConfiguredFmpApiKey(); const url = `https://financialmodelingprep.com/api/v3/profile/${symbol}?apikey=${apiKey}`; const data = await fetchFmpJson(url) as any; res.json(Array.isArray(data) ? data[0] ?? {} : data); @@ -2822,7 +2831,7 @@ RULES: try { const symbol = String(req.query.symbol || '').trim().toUpperCase(); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - const apiKey = process.env.FMP_API_KEY || 'demo'; + const apiKey = getConfiguredFmpApiKey(); const url = `https://financialmodelingprep.com/api/v3/key-metrics/${symbol}?limit=4&apikey=${apiKey}`; const data = await fetchFmpJson(url) as any; res.json(Array.isArray(data) ? data[0] ?? {} : data); @@ -2839,7 +2848,7 @@ RULES: try { const symbol = String(req.query.symbol || '').trim().toUpperCase(); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - const apiKey = process.env.FMP_API_KEY || 'demo'; + const apiKey = getConfiguredFmpApiKey(); const url = `https://financialmodelingprep.com/api/v3/historical/earning_calendar/${symbol}?limit=8&apikey=${apiKey}`; const data = await fetchFmpJson(url) as any; res.json({ earnings: Array.isArray(data) ? data : [] }); @@ -2854,7 +2863,7 @@ RULES: // ── Screener: stock screener from FMP ──────────────────────────────── this.app.get('/api/screener', this.requireAuth, async (req, res) => { try { - const apiKey = process.env.FMP_API_KEY || 'demo'; + const apiKey = getConfiguredFmpApiKey(); const qs = new URLSearchParams(); const sector = String(req.query.sector || '').trim(); if (sector && sector !== 'All') { diff --git a/backend/verifyMarketDataEndpoints.ts b/backend/verifyMarketDataEndpoints.ts index 39353b7..48b689c 100644 --- a/backend/verifyMarketDataEndpoints.ts +++ b/backend/verifyMarketDataEndpoints.ts @@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const apiServerSource = readFileSync(join(__dirname, 'src/services/apiServer.ts'), 'utf8'); +const configSource = readFileSync(join(__dirname, 'src/config/index.ts'), 'utf8'); function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -33,6 +34,10 @@ function assertSourceIncludes(fragment: string, message: string) { assert.ok(apiServerSource.includes(fragment), message); } +function assertConfigIncludes(fragment: string, message: string) { + assert.ok(configSource.includes(fragment), message); +} + function assertSourceMatches(pattern: RegExp, message: string) { assert.match(apiServerSource, pattern, message); } @@ -151,6 +156,18 @@ function testFmpProxyContracts() { "import { fetchFmpJson, FmpFetchError } from './fmpCache.js';", 'FMP routes must use the shared cache helper and typed upstream error', ); + assertConfigIncludes( + "FMP_API_KEY: process.env.FMP_API_KEY || '',", + 'FMP config must not silently default to the shared demo key', + ); + assertSourceIncludes( + "apiKey.toLowerCase() === 'demo'", + 'FMP routes must reject the shared demo key explicitly', + ); + assertSourceIncludes( + 'FMP_API_KEY is required for research and screener endpoints', + 'FMP routes must surface an explicit missing-key error', + ); const fetchFmpCalls = apiServerSource.match(/fetchFmpJson\(url\)/g) ?? []; assert.equal(fetchFmpCalls.length, 4, 'each FMP route must fetch through fetchFmpJson(url)');