Sign in
Create account
Workspace
Demo portfolio
Credits: —
Sign out

What's your idea?

Describe a strategy, paste Python signal code, or upload a research paper. We'll test it across assets and tell you if it holds up.

Describe
Python code
Research paper
YouTube
1 credit · tests across 10 tickers
No strategies in demo yet.
Add a passing result from the workspace.
// before this script tag, or set it via your CDN/hosting environment. const STEPS = ['t00','t01','t02','t03','t04','t05','t06','t07','t08']; const STEP_NAMES = { 't00':'Data Quality','t01':'IS Grid Search','t02':'IS Holdout', 't03':'IS Sharpe ≥ 1.0','t04':'IS MCPT','t05':'Transaction Costs', 't06':'Walk-Forward PF','t07':'Multi-Window OOS','t08':'WF MCPT', }; // ── State ───────────────────────────────────────────────────────── let auth = { token: null, userId: null, email: null, credits: 0 }; let convs = []; let activeConvId = null; let inputType = 'text'; let currentView = 'workspace'; let polling = null; let uploadedText = null; let uploadedFile = null; // ── Boot ────────────────────────────────────────────────────────── (function boot() { const saved = localStorage.getItem('qa_token'); if (saved) { auth.token = saved; verifyToken().then(ok => { if (ok) showApp(); else showAuth(); }); } else { showAuth(); } setupAuthUI(); setupAppUI(); })(); // ── Auth ────────────────────────────────────────────────────────── function showAuth() { document.getElementById('auth-overlay').style.display = 'flex'; document.getElementById('app').classList.remove('visible'); } function showApp() { document.getElementById('auth-overlay').style.display = 'none'; document.getElementById('app').classList.add('visible'); updateCreditsDisplay(); loadConversations(); pingAPI(); } async function verifyToken() { try { const r = await apiFetch('GET', '/api/auth/me'); if (r.ok) { const d = await r.json(); auth.userId = d.user_id; auth.email = d.email; auth.credits = d.credits; return true; } } catch(_) {} localStorage.removeItem('qa_token'); auth.token = null; return false; } function setupAuthUI() { // Tab switching document.querySelectorAll('.auth-tab').forEach(t => { t.addEventListener('click', () => { document.querySelectorAll('.auth-tab').forEach(x => x.classList.remove('active')); t.classList.add('active'); document.getElementById('auth-login').style.display = t.dataset.tab === 'login' ? 'block' : 'none'; document.getElementById('auth-register').style.display = t.dataset.tab === 'register' ? 'block' : 'none'; }); }); // Login document.getElementById('login-btn').addEventListener('click', doLogin); document.getElementById('login-pw').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); }); // Register document.getElementById('reg-btn').addEventListener('click', doRegister); document.getElementById('reg-pw').addEventListener('keydown', e => { if (e.key === 'Enter') doRegister(); }); } async function doLogin() { const email = document.getElementById('login-email').value.trim(); const pw = document.getElementById('login-pw').value; const errEl = document.getElementById('login-err'); errEl.textContent = ''; const btn = document.getElementById('login-btn'); btn.disabled = true; btn.textContent = 'Signing in...'; try { const r = await fetch(`${API}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password: pw }), }); const d = await r.json(); if (!r.ok) { errEl.textContent = d.detail || 'Login failed'; return; } auth.token = d.token; auth.userId = d.user_id; auth.email = d.email; auth.credits = d.credits; localStorage.setItem('qa_token', d.token); showApp(); } catch(_) { errEl.textContent = 'Cannot reach server'; } finally { btn.disabled = false; btn.textContent = 'Sign in'; } } async function doRegister() { const email = document.getElementById('reg-email').value.trim(); const pw = document.getElementById('reg-pw').value; const errEl = document.getElementById('reg-err'); errEl.textContent = ''; const btn = document.getElementById('reg-btn'); btn.disabled = true; btn.textContent = 'Creating...'; try { const r = await fetch(`${API}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password: pw }), }); const d = await r.json(); if (!r.ok) { errEl.textContent = d.detail || 'Registration failed'; return; } auth.token = d.token; auth.userId = d.user_id; auth.email = d.email; auth.credits = d.credits; localStorage.setItem('qa_token', d.token); showApp(); } catch(_) { errEl.textContent = 'Cannot reach server'; } finally { btn.disabled = false; btn.textContent = 'Create account'; } } // ── App UI setup ────────────────────────────────────────────────── function setupAppUI() { // Header nav document.querySelectorAll('.hnav-btn').forEach(b => { b.addEventListener('click', () => switchView(b.dataset.view)); }); // Sign out document.getElementById('signout-btn').addEventListener('click', () => { localStorage.removeItem('qa_token'); auth.token = null; showAuth(); }); // New conversation document.getElementById('new-conv-btn').addEventListener('click', createConversation); // Input type tabs document.querySelectorAll('.itype-tab').forEach(t => { t.addEventListener('click', () => switchInputType(t.dataset.itype)); }); // Send button document.getElementById('send-btn').addEventListener('click', doRun); document.getElementById('main-input').addEventListener('keydown', e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) doRun(); }); // Auto-resize textarea const ta = document.getElementById('main-input'); ta.addEventListener('input', () => { ta.style.height = 'auto'; ta.style.height = Math.min(ta.scrollHeight, 180) + 'px'; }); // Paper upload const zone = document.getElementById('upload-zone'); const fi = document.getElementById('file-input'); zone.addEventListener('click', () => fi.click()); zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragging'); }); zone.addEventListener('dragleave', () => zone.classList.remove('dragging')); zone.addEventListener('drop', e => { e.preventDefault(); zone.classList.remove('dragging'); handleFile(e.dataTransfer.files[0]); }); fi.addEventListener('change', () => handleFile(fi.files[0])); document.getElementById('paper-send-btn').addEventListener('click', doPaperRun); document.getElementById('yt-send-btn').addEventListener('click', doYouTubeRun); } function switchView(view) { currentView = view; document.querySelectorAll('.hnav-btn').forEach(b => b.classList.toggle('active', b.dataset.view === view)); document.getElementById('view-workspace').style.display = view === 'workspace' ? 'flex' : 'none'; const dv = document.getElementById('view-demo'); if (view === 'demo') { dv.style.display = 'flex'; loadDemoPortfolio(); } else dv.style.display = 'none'; } function switchInputType(type) { inputType = type; document.querySelectorAll('.itype-tab').forEach(t => t.classList.toggle('active', t.dataset.itype === type)); document.getElementById('input-text-area').style.display = (type === 'text' || type === 'code') ? 'block' : 'none'; document.getElementById('input-paper-area').style.display = type === 'paper' ? 'block' : 'none'; document.getElementById('input-youtube-area').style.display = type === 'youtube' ? 'block' : 'none'; const ta = document.getElementById('main-input'); if (type === 'code') { ta.classList.add('code-mode'); ta.placeholder = 'def signal_fn(ohlc, param):\n # ohlc: DataFrame with open/high/low/close/volume\n # Return pd.Series: +1.0 long, -1.0 short, 0.0 flat\n ...'; set('input-hint', '1 credit · tests across 10 tickers'); } else { ta.classList.remove('code-mode'); ta.placeholder = 'e.g. Buy when the 20-day RSI crosses above 35 after being below 30. Exit after 5 days or when RSI hits 65.'; set('input-hint', '1 credit · tests across 10 tickers'); } } // ── Conversations ───────────────────────────────────────────────── async function loadConversations() { try { const r = await apiFetch('GET', '/api/conversations'); if (!r.ok) return; convs = await r.json(); renderConvList(); if (convs.length && !activeConvId) { selectConv(convs[0].id); } } catch(_) {} } function renderConvList() { const el = document.getElementById('conv-list'); if (!convs.length) { el.innerHTML = '
No ideas yet. Start one below.
'; return; } el.innerHTML = convs.map(c => `
${esc(c.title)}
${c.n_passes} pass · ${c.n_messages} msg
`).join(''); el.querySelectorAll('.conv-item').forEach(el => { el.addEventListener('click', () => selectConv(Number(el.dataset.id))); }); } async function createConversation() { try { const r = await apiFetch('POST', '/api/conversations', {}); if (!r.ok) return; const c = await r.json(); convs.unshift(c); renderConvList(); selectConv(c.id, true); } catch(_) {} } async function selectConv(id, fresh = false) { activeConvId = id; renderConvList(); if (fresh) { clearThread(); return; } try { const r = await apiFetch('GET', `/api/conversations/${id}`); if (!r.ok) return; const conv = await r.json(); renderThread(conv); } catch(_) {} } // ── Thread rendering ────────────────────────────────────────────── function clearThread() { const thread = document.getElementById('thread'); thread.innerHTML = `

What's your idea?

Describe a strategy, paste Python signal code, or upload a research paper.

`; } function renderThread(conv) { const thread = document.getElementById('thread'); thread.innerHTML = ''; for (const msg of conv.messages) { if (msg.role === 'user') { appendUserMessage(msg.content, msg.input_type); if (msg.results && msg.results.length) { appendResults(msg.results, msg.id); } } } if (!conv.messages.length) { clearThread(); } else { thread.scrollTop = thread.scrollHeight; } } function appendUserMessage(content, inputType) { const el = document.createElement('div'); el.className = 'msg-user'; const isCode = inputType === 'code'; const label = inputType === 'paper' ? 'Paper' : inputType === 'code' ? 'Code' : 'Idea'; el.innerHTML = `
${label}
${esc(content.slice(0, 300))}${content.length > 300 ? '…' : ''}
`; document.getElementById('thread').appendChild(el); hideEmpty(); } function appendResults(results, messageId) { const thread = document.getElementById('thread'); const passes = results.filter(r => r.overall_pass); const fails = results.filter(r => !r.overall_pass); const allTickers = results.map(r => r.ticker); const section = document.createElement('div'); section.className = 'results-section'; section.dataset.messageId = messageId; const tickerTags = allTickers.map(t => { const pass = results.find(r => r.ticker === t && r.overall_pass); return `
${esc(t)}
`; }).join(''); section.innerHTML = `
Results ${passes.length} of ${results.length} passed
${tickerTags}
`; thread.appendChild(section); const cardsEl = section.querySelector(`#cards-${messageId}`); for (const r of [...passes, ...fails]) { cardsEl.appendChild(buildCard(r)); } } function buildCard(r) { const passed = r.overall_pass; const failStep = r.step_failed; const failIdx = failStep ? STEPS.indexOf(failStep) : -1; const segs = STEPS.map((_, i) => { if (passed) return 'pass'; if (failIdx < 0) return ''; if (i < failIdx) return 'pass'; if (i === failIdx) return 'fail'; return ''; }); const pipeHtml = segs.map(cls => `
`).join(''); const pipeNote = passed ? 'all 9 steps' : (STEP_NAMES[failStep] || failStep || ''); const param = r.best_param || ''; let bottom; if (passed) { bottom = `
${r.is_sharpe != null ? metricHtml(r.is_sharpe.toFixed(3), 'Sharpe') : ''} ${r.wf_pf != null ? metricHtml(r.wf_pf.toFixed(3), 'WF PF') : ''} ${r.trades_per_year != null ? metricHtml(r.trades_per_year.toFixed(1), 'Trades/yr') : ''}
`; } else { const stepN = failIdx >= 0 ? `Step ${failIdx + 1}` : ''; bottom = `
Eliminated at ${stepN} — ${esc(STEP_NAMES[failStep] || failStep || '')}
`; } const el = document.createElement('div'); el.className = `result-card ${passed ? 'pass' : 'fail'}`; el.innerHTML = `
${esc(r.strategy_name || 'Strategy')}
${param ? `
param: ${esc(param)}
` : ''}
${esc(r.ticker)}
${passed ? `
Pass
` : ''}
${pipeHtml}
${esc(pipeNote)}
${bottom}`; if (passed) { const btn = el.querySelector('.add-demo-btn'); btn.addEventListener('click', () => addToDemo(r.id, btn)); } return el; } function metricHtml(val, label) { return `
${val}
${label}
`; } // ── Run ─────────────────────────────────────────────────────────── async function doRun() { const content = document.getElementById('main-input').value.trim(); if (!content) { document.getElementById('main-input').focus(); return; } if (!activeConvId) await createConversation(); await submitMessage(content, inputType); } async function doPaperRun() { if (!uploadedText) return; const extra = document.getElementById('paper-extra').value.trim(); const content = uploadedText + (extra ? '\n\nAdditional context: ' + extra : ''); if (!activeConvId) await createConversation(); await submitMessage(content, 'paper'); } async function doYouTubeRun() { const url = document.getElementById('yt-url-input').value.trim(); if (!url) return; const btn = document.getElementById('yt-send-btn'); const status = document.getElementById('yt-status-line'); btn.disabled = true; btn.textContent = 'Fetching...'; status.innerHTML = 'Extracting transcript…'; try { const r = await apiFetch('POST', '/api/upload/youtube', { url }); if (!r.ok) { const e = await r.json().catch(() => ({ detail: 'Failed to fetch transcript' })); status.innerHTML = `${esc(e.detail)}`; return; } const d = await r.json(); status.innerHTML = `Got transcript · ${d.chars} chars · ~${Math.round(d.duration_s/60)} min video`; if (!activeConvId) await createConversation(); await submitMessage(d.text, 'paper'); } finally { btn.disabled = false; btn.textContent = 'Run'; } } async function submitMessage(content, itype) { setBusy(true); appendUserMessage(content, itype); try { const r = await apiFetch('POST', `/api/conversations/${activeConvId}/message`, { content, input_type: itype, n_tickers: 10, }); if (!r.ok) { const e = await r.json().catch(() => ({ detail: 'Failed' })); appendError(e.detail || 'Request failed'); setBusy(false); return; } const d = await r.json(); auth.credits = Math.max(0, auth.credits - d.credits_used); updateCreditsDisplay(); // Show progress document.getElementById('progress-strip').style.display = 'block'; set('progress-label', `Testing across ${d.tickers.join(', ')}`); // Poll for results let done = false; let seen = new Set(); const section = document.createElement('div'); section.className = 'results-section'; section.innerHTML = `
Testing...
${d.tickers.map(t => `
${esc(t)}
`).join('')}
`; document.getElementById('thread').appendChild(section); document.getElementById('thread').scrollTop = 99999; let passCount = 0; polling = setInterval(async () => { try { const pr = await apiFetch('GET', `/api/conversations/${activeConvId}/results/${d.message_id}`); if (!pr.ok) return; const pd = await pr.json(); const results = pd.results || []; for (const res of results) { if (!seen.has(res.id)) { seen.add(res.id); const cardsEl = document.getElementById(`cards-${d.message_id}`); if (cardsEl) cardsEl.appendChild(buildCard(res)); if (res.overall_pass) passCount++; // Update ticker tag const ttId = `tt-${d.message_id}-${res.ticker.replace(/[^a-zA-Z0-9]/g,'-')}`; const ttEl = document.getElementById(ttId); if (ttEl) ttEl.className = `ticker-tag ${res.overall_pass ? 'pass' : 'fail'}`; // Update counts set(`rc-${d.message_id}`, `${passCount} of ${seen.size} passed`); } } // Update progress const pct = d.total > 0 ? (seen.size / d.total * 100) : 0; document.getElementById('progress-fill').style.width = pct + '%'; set('progress-label', `${seen.size} / ${d.total} tested · ${passCount} passed`); if (seen.size >= d.total) { done = true; clearInterval(polling); polling = null; setBusy(false); set(`rl-${d.message_id}`, 'Results'); document.getElementById('progress-strip').style.display = 'none'; loadConversations(); } } catch(_) {} }, 1200); } catch(e) { appendError('Cannot reach API. Start the server: uvicorn api.main:app --reload --port 8000'); setBusy(false); } document.getElementById('main-input').value = ''; } // ── Paper upload ────────────────────────────────────────────────── async function handleFile(file) { if (!file || !file.name.endsWith('.pdf')) return; const zone = document.getElementById('upload-zone'); zone.classList.add('loaded'); set('upload-label', `Uploading ${file.name}...`); uploadedFile = file; try { const formData = new FormData(); formData.append('file', file); const r = await fetch(`${API}/api/upload/paper`, { method: 'POST', headers: { 'Authorization': `Bearer ${auth.token}` }, body: formData, }); if (!r.ok) { const e = await r.json().catch(() => ({ detail: 'Upload failed' })); set('upload-label', e.detail || 'Upload failed'); zone.classList.remove('loaded'); return; } const d = await r.json(); uploadedText = d.text; set('upload-label', `${file.name} — ${d.chars.toLocaleString()} characters extracted`); document.getElementById('paper-send-btn').disabled = false; } catch(_) { set('upload-label', 'Upload failed — server not reachable'); zone.classList.remove('loaded'); } } // ── Demo portfolio ──────────────────────────────────────────────── async function addToDemo(resultId, btn) { btn.disabled = true; btn.textContent = 'Adding...'; try { const r = await apiFetch('POST', '/api/demo/add', { strategy_result_id: resultId }); if (!r.ok) { const e = await r.json().catch(() => ({ detail: 'Failed' })); btn.textContent = e.detail || 'Failed'; btn.disabled = false; return; } btn.classList.add('added'); btn.textContent = '✓ Added'; auth.credits = Math.max(0, auth.credits - 1); updateCreditsDisplay(); } catch(_) { btn.textContent = 'Error'; btn.disabled = false; } } async function loadDemoPortfolio() { try { const r = await apiFetch('GET', '/api/demo/portfolio'); if (!r.ok) return; const entries = await r.json(); renderDemoPortfolio(entries); } catch(_) {} } function renderDemoPortfolio(entries) { const el = document.getElementById('demo-content'); if (!entries.length) { el.innerHTML = '
No strategies in demo yet.
Add a passing result from the workspace.
'; return; } el.innerHTML = entries.map(e => { const pnl = e.cumulative_pnl; const pnlStr = pnl != null ? (pnl >= 0 ? '+' : '') + pnl.toFixed(2) + '%' : '—'; const pnlCls = pnl != null ? (pnl >= 0 ? 'positive' : 'negative') : ''; const dd = e.max_drawdown != null ? '-' + e.max_drawdown.toFixed(2) + '%' : '—'; const wr = e.win_rate != null ? e.win_rate.toFixed(0) + '%' : '—'; const sig = e.last_signal; const sigStr = sig === 1 ? 'LONG' : sig === -1 ? 'SHORT' : sig === 0 ? 'FLAT' : '—'; const sigCls = sig === 1 ? 'long' : sig === -1 ? 'short' : 'flat'; return `
${esc(e.strategy_name)}
${esc(e.ticker)}
${pnlStr}
P&L
${dd}
Max DD
${wr}
Win rate
${sigStr}
Signal
`; }).join(''); // Click to expand detail el.querySelectorAll('.portfolio-card').forEach(card => { card.addEventListener('click', () => loadDemoDetail(Number(card.dataset.pid))); }); } async function loadDemoDetail(pid) { try { const r = await apiFetch('GET', `/api/demo/portfolio/${pid}`); if (!r.ok) return; const d = await r.json(); renderDemoDetail(d); } catch(_) {} } function renderDemoDetail(d) { const el = document.getElementById('demo-content'); const s = d.stats; const pnlStr = s.cumulative_pnl != null ? (s.cumulative_pnl >= 0 ? '+' : '') + s.cumulative_pnl.toFixed(2) + '%' : '—'; const ddStr = s.max_drawdown != null ? '-' + s.max_drawdown.toFixed(2) + '%' : '—'; const wrStr = s.win_rate != null ? s.win_rate.toFixed(0) + '%' : '—'; // Build equity curve SVG const curve = d.equity_curve; const svgPath = buildEquitySVG(curve, 620, 130); // Last signal const lastTrade = d.trades[d.trades.length - 1]; const sig = lastTrade ? lastTrade.signal : null; const sigStr = sig === 1 ? 'LONG' : sig === -1 ? 'SHORT' : 'FLAT'; const sigCls = sig === 1 ? 'long' : sig === -1 ? 'short' : 'flat'; // Recent trades table (last 20) const recent = d.trades.slice(-20).reverse(); const tradeRows = recent.map(t => { const pct = t.trade_pnl_pct; const cls = pct != null ? (pct >= 0 ? 'pos' : 'neg') : ''; const pStr = pct != null ? (pct >= 0 ? '+' : '') + pct.toFixed(3) + '%' : '—'; const sigL = t.signal === 1 ? 'LONG' : t.signal === -1 ? 'SHORT' : 'FLAT'; return ` ${t.date} ${sigL} ${t.price.toFixed(2)} ${pStr} ${t.cumulative_pnl != null ? (t.cumulative_pnl >= 0 ? '+' : '') + t.cumulative_pnl.toFixed(2) + '%' : '—'} `; }).join(''); el.innerHTML = `
${esc(d.strategy_name)}
${esc(d.ticker)}
${sigStr}
×
${pnlStr}
Cumulative P&L
${ddStr}
Max drawdown
${wrStr}
Win rate
${s.n_trades}
Trade signals
${svgPath}
${tradeRows}
DateSignalPriceDay P&LCumulative
`; document.getElementById('close-detail').addEventListener('click', loadDemoPortfolio); } function buildEquitySVG(curve, w, h) { if (!curve.length) return ''; const vals = curve.map(c => c.equity); const minV = Math.min(...vals); const maxV = Math.max(...vals); const range = maxV - minV || 1; const pad = 4; const pts = vals.map((v, i) => { const x = pad + (i / Math.max(vals.length - 1, 1)) * (w - pad * 2); const y = pad + (1 - (v - minV) / range) * (h - pad * 2); return `${x},${y}`; }); const positive = vals[vals.length - 1] >= 100; const color = positive ? '#15803d' : '#dc2626'; const fill = positive ? '#dcfce7' : '#fee2e2'; const first = pts[0]; const last = pts[pts.length - 1]; const fillPts = `${first.split(',')[0]},${h} ${pts.join(' ')} ${last.split(',')[0]},${h}`; return ` `; } // ── Helpers ─────────────────────────────────────────────────────── function apiFetch(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${auth.token}`, }, }; if (body !== undefined) opts.body = JSON.stringify(body); return fetch(`${API}${path}`, opts); } function setBusy(on) { const btn = document.getElementById('send-btn'); btn.disabled = on; btn.textContent = on ? 'Running...' : 'Run'; } function appendError(msg) { const el = document.createElement('div'); el.className = 'error-msg'; el.textContent = msg; document.getElementById('thread').appendChild(el); } function hideEmpty() { const el = document.getElementById('workspace-empty'); if (el) el.style.display = 'none'; } function updateCreditsDisplay() { set('credits-display', auth.credits); } async function pingAPI() { try { const r = await fetch(`${API}/api/health`, { signal: AbortSignal.timeout(3000) }); document.getElementById('api-status-dot').className = 'api-dot ' + (r.ok ? 'ok' : 'fail'); } catch(_) { document.getElementById('api-status-dot').className = 'api-dot fail'; } } function set(id, val) { const el = document.getElementById(id); if (el) el.textContent = val; } function esc(str) { if (str == null) return ''; return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }