/** * TradeSense AI — Authoritative Institutional API Client & Adapters (Milestone 23 Master) * Maps 1-to-1 with openapi/tradesense-openapi.json. * Strict zero-credential leakage; in-memory session handling. */ class TradeSenseAPIClient { constructor(baseUrl = '') { this.baseUrl = baseUrl; this.accessToken = null; this.tenantId = 'default_tenant'; this.onUnauthorizedCallback = null; } setToken(token) { this.accessToken = token; } setTenant(tenantId) { this.tenantId = tenantId; } onUnauthorized(callback) { this.onUnauthorizedCallback = callback; } async request(endpoint, options = {}) { const headers = options.headers || {}; if (this.accessToken) { headers['Authorization'] = `Bearer ${this.accessToken}`; } headers['X-Request-ID'] = `req_${Math.random().toString(36).substring(2, 10)}`; const config = { ...options, headers, }; try { const response = await fetch(`${this.baseUrl}${endpoint}`, config); if (response.status === 401) { if (this.onUnauthorizedCallback) { this.onUnauthorizedCallback(); } const errData = await response.json().catch(() => ({})); throw new Error(errData.detail || 'Authentication required or session expired.'); } if (!response.ok) { const errData = await response.json().catch(() => ({})); throw new Error(errData.detail || errData.error?.message || `HTTP ${response.status}: Request failed.`); } const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } return await response.text(); } catch (err) { console.error(`API Error [${endpoint}]:`, err); throw err; } } // ── Authentication ──────────────────────────────────────────────────────── async login(username, password, tenantId = 'default_tenant') { const query = new URLSearchParams({ username, password, tenant_id: tenantId }); const res = await this.request(`/api/v1/auth/login?${query.toString()}`, { method: 'POST', }); this.setToken(res.access_token); this.setTenant(res.tenant_id); return res; } async logout() { try { await this.request('/api/v1/auth/logout', { method: 'POST' }); } finally { this.accessToken = null; } } // ── Research Plane: Runs, Specs, Evidence ───────────────────────────────── async listResearchRuns(experimentId = null, limit = 50, offset = 0) { const params = new URLSearchParams({ limit, offset }); if (experimentId) params.append('experiment_id', experimentId); return await this.request(`/api/v1/research/runs?${params.toString()}`); } async getResearchRun(runId) { return await this.request(`/api/v1/research/runs/${encodeURIComponent(runId)}`); } async submitResearchRun(config, experimentId, strategyId, idempotencyKey) { const params = new URLSearchParams({ experiment_id: experimentId, strategy_id: strategyId }); return await this.request(`/api/v1/research/runs?${params.toString()}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey || `idem_${Date.now()}`, }, body: JSON.stringify(config), }); } async getEvidencePackage(runId) { return await this.request(`/api/v1/research/runs/${encodeURIComponent(runId)}/evidence_package`); } async reproduceRun(runId) { return await this.request(`/api/v1/research/runs/${encodeURIComponent(runId)}/reproduce`, { method: 'POST', }); } async getStockResearch(ticker, horizonSessions = 21) { const params = new URLSearchParams({ horizon_sessions: horizonSessions }); return await this.request(`/api/v1/research/${encodeURIComponent(ticker)}?${params.toString()}`); } // ── Module 05: Asynchronous Compute Job Infrastructure ────────────────── async submitResearchJob(ticker, horizonSessions = 21, priority = 1, idempotencyKey = null) { return await this.request('/api/v1/jobs/research', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticker, horizon_sessions: horizonSessions, priority, idempotency_key: idempotencyKey, }), }); } async getJobStatus(jobId) { return await this.request(`/api/v1/jobs/${encodeURIComponent(jobId)}`); } async cancelJob(jobId) { return await this.request(`/api/v1/jobs/${encodeURIComponent(jobId)}/cancel`, { method: 'POST', }); } async getJobResult(jobId) { return await this.request(`/api/v1/jobs/${encodeURIComponent(jobId)}/result`); } async listJobs(status = null, limit = 50, offset = 0) { const params = new URLSearchParams({ limit, offset }); if (status) params.append('status', status); return await this.request(`/api/v1/jobs?${params.toString()}`); } async getJobEvents(jobId) { return await this.request(`/api/v1/jobs/${encodeURIComponent(jobId)}/events`); } // ── Module 06: Independent Quant Validation & Reproducibility ──────────── async submitValidationJob(ticker, horizonSessions = 21, depth = 'FAST', randomSeed = 42) { return await this.request('/api/v1/jobs/validation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ticker, horizon_sessions: horizonSessions, depth, random_seed: randomSeed, }), }); } async getValidationRun(validationId) { return await this.request(`/api/v1/validation/${encodeURIComponent(validationId)}`); } async getValidationReport(validationId) { return await this.request(`/api/v1/validation/${encodeURIComponent(validationId)}/report`); } async getValidationCertificate(validationId) { return await this.request(`/api/v1/validation/${encodeURIComponent(validationId)}/certificate`); } async replayValidation(validationId) { return await this.request(`/api/v1/validation/${encodeURIComponent(validationId)}/replay`, { method: 'POST', }); } // ── Governance & Control Benchmarks (Module 07 Lifecycle) ───────────────── async preRegisterResearch(researchId, protocolData) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/pre-register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(protocolData), }); } async startGovernanceResearch(researchId, version = 1) { const params = new URLSearchParams({ version }); return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/start?${params.toString()}`, { method: 'POST', }); } async submitResearchForReview(researchId, reviewData) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/submit-review`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(reviewData), }); } async approveGovernanceDecision(researchId, approvalData) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/approve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(approvalData), }); } async rejectGovernanceDecision(researchId, rejectData) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(rejectData), }); } async revokeGovernanceApproval(researchId, revokeData) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/revoke`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(revokeData), }); } async archiveGovernanceResearch(researchId, archiveData) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/archive`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(archiveData), }); } async getGovernanceState(researchId, version = 1) { const params = new URLSearchParams({ version }); return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}?${params.toString()}`); } async getGovernanceHistory(researchId) { return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/history`); } async getGovernanceEvidence(researchId, version = 1) { const params = new URLSearchParams({ version }); return await this.request(`/api/v1/governance/${encodeURIComponent(researchId)}/evidence?${params.toString()}`); } async exportAuditPackage(researchId, version = 1) { const params = new URLSearchParams({ version }); return await this.request(`/api/v1/audit/export/${encodeURIComponent(researchId)}?${params.toString()}`); } async listGovernanceDecisions(experimentId = null, limit = 50, offset = 0) { const params = new URLSearchParams({ limit, offset }); if (experimentId) params.append('experiment_id', experimentId); return await this.request(`/api/v1/governance/decisions?${params.toString()}`); } async submitPeerReview(decisionId, verdict, comments) { return await this.request(`/api/v1/governance/decisions/${encodeURIComponent(decisionId)}/peer_review`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ verdict, comments }), }); } async getFamily1Archive() { return await this.request('/api/v1/governance/family_1_archive'); } // ── Cryptographic Audit Ledger ──────────────────────────────────────────── async listAuditEvents(limit = 100, offset = 0) { const params = new URLSearchParams({ limit, offset }); return await this.request(`/api/v1/audit/events?${params.toString()}`); } async verifyAuditLedger() { return await this.request('/api/v1/audit/verify'); } // ── Indian Market Endpoints (Live Ticker, Quotes, Charts, SEBI Friction) ─ async getLiveMarketTicker() { return await this.request('/api/v1/market/live_ticker'); } async getStockQuote(symbol, exchange = 'NSE') { const params = new URLSearchParams({ exchange }); return await this.request(`/api/v1/market/quote/${encodeURIComponent(symbol)}?${params.toString()}`); } async getStockChartBars(symbol, exchange = 'NSE', period = '1d', interval = '5m') { const params = new URLSearchParams({ exchange, period, interval }); return await this.request(`/api/v1/market/chart/${encodeURIComponent(symbol)}?${params.toString()}`); } async getCompanyOverview(exchange = 'NSE', refresh = false) { const params = new URLSearchParams({ exchange, refresh: refresh ? 'true' : 'false' }); return await this.request(`/api/v1/market/company_overview?${params.toString()}`); } async calculateStatutoryCosts(buyPrice, sellPrice, quantity = 10, segment = 'EQUITY_DELIVERY') { const params = new URLSearchParams({ buy_price: buyPrice, sell_price: sellPrice, quantity, segment }); return await this.request(`/api/v1/statutory/costs?${params.toString()}`); } async searchStocks(query, exchange = 'ALL', limit = 10) { const params = new URLSearchParams({ q: query, exchange, limit }); return await this.request(`/api/v1/market/search?${params.toString()}`); } // ── Operations & Telemetry ──────────────────────────────────────────────── async getHealth() { return await this.request('/api/v1/health'); } async getVersion() { return await this.request('/api/v1/version'); } async getTenantUsage() { return await this.request('/api/v1/observability/usage'); } async getObservabilityMetrics() { return await this.request('/api/v1/observability/metrics'); } } // Instantiate global singleton client window.TradeSenseAPI = new TradeSenseAPIClient(); // ============================================================================== // Domain Normalization Adapters (Decouples UI from raw backend JSON formats) // ============================================================================== window.TradeSenseAdapters = { /** * Normalizes raw backend ResearchRun into a structured client model with first-class human identity */ normalizeResearchRun(rawRun) { if (!rawRun) return null; const runId = rawRun.run_id || 'run_unknown'; const config = rawRun.configuration || {}; const custom = config.custom_parameters || {}; const prov = rawRun.provenance || {}; // 1. Determine human-readable research name and sanitize universe to Indian equities const rawUniverse = (config.universe_tickers && config.universe_tickers.length > 0) ? config.universe_tickers : ['RELIANCE', 'TCS']; // Sanitize any legacy foreign symbols to Indian equities const indianTickerMap = { 'AAPL': 'RELIANCE', 'MSFT': 'TCS', 'GOOG': 'INFY', 'AMZN': 'HDFCBANK', 'NVDA': 'TATAMOTORS', 'META': 'ICICIBANK', 'TSLA': 'JSWSTEEL', }; const universeList = rawUniverse.map(tk => indianTickerMap[tk.toUpperCase()] || tk.toUpperCase()); let researchName = custom.research_name || config.research_name || null; let isFallbackName = false; if (!researchName || researchName.includes('AAPL') || researchName.includes('MSFT')) { if (rawRun.experiment_id && rawRun.experiment_id.includes(' ') && !rawRun.experiment_id.includes('AAPL')) { researchName = rawRun.experiment_id; } else if (rawRun.experiment_id && !rawRun.experiment_id.startsWith('exp_') && !rawRun.experiment_id.includes('AAPL')) { researchName = rawRun.experiment_id.replace(/_/g, ' ').toUpperCase(); } else { // Deterministic Indian market research name const universeStr = universeList.join(', '); const horizon = config.holding_period_sessions || 21; const strat = (rawRun.strategy_id || 'Quantitative Strategy').replace(/^strat_/, '').replace(/_/g, ' '); const stratClean = strat.toLowerCase().includes('01') ? 'Alpha Factor Momentum' : (strat.charAt(0).toUpperCase() + strat.slice(1)); researchName = `${universeStr} ${stratClean} — ${horizon} Sessions`; isFallbackName = true; } } // 2. Extract hypothesis & methodology const hypothesis = custom.hypothesis || 'Investigate empirical predictability and factor contamination under Newey-West HAC covariance correction on NSE Indian equities.'; const methodology = custom.methodology || 'Conformal Prediction & Purged K-Fold Cross-Validation'; return { runId, researchName, isFallbackName, experimentId: rawRun.experiment_id || 'exp_general', strategyId: rawRun.strategy_id || 'strat_general', tenantId: rawRun.tenant_id || 'default_tenant', workflowState: rawRun.workflow_state || 'PRE_REGISTERED', status: rawRun.status || 'QUEUED', universe: universeList, horizonSessions: config.holding_period_sessions || 21, randomSeed: config.random_seed || 42, hypothesis, methodology, configuration: config, provenance: prov, createdAt: rawRun.created_at || '2026-09-02T09:00:00Z', }; }, /** * Normalizes raw backend stock research response into an authoritative DecisionEvidenceModel */ normalizeDecisionBrief(raw) { if (!raw) return null; if (window.TradeSenseDecisionEngine && window.TradeSenseDecisionEngine.buildDecisionEvidenceModel) { return window.TradeSenseDecisionEngine.buildDecisionEvidenceModel(raw); } return null; }, /** * Clipboard helper with toast feedback */ copyToClipboard(text, label = 'Identifier') { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(() => { if (window.AppState && window.AppState.showToast) { window.AppState.showToast(`✓ Copied ${label} to clipboard!`); } }).catch(() => this.fallbackCopy(text, label)); } else { this.fallbackCopy(text, label); } }, fallbackCopy(text, label) { const input = document.createElement('input'); input.value = text; document.body.appendChild(input); input.select(); document.execCommand('copy'); document.body.removeChild(input); if (window.AppState && window.AppState.showToast) { window.AppState.showToast(`✓ Copied ${label} to clipboard!`); } } };