tradesenseAi / static /js /api_client.js
Niranjan-Murugarasu024
feat: English retail guidance and Indian market research registry
19e07b9
Raw History Blame Contribute Delete
17.2 kB
/**
* 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!`);
}
}
};