Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
6f1177b
feat(types): add TerminalOrderRequest, OhlcvBar, OhlcvResponse, RiskA…
rogerdigital Apr 14, 2026
bebb9f3
feat(api): add POST /api/trading/orders terminal order endpoint and r…
rogerdigital Apr 14, 2026
640859e
feat(web): add trading order service
rogerdigital Apr 14, 2026
4bd580a
feat(web): wire BUY/SELL buttons in TradingPage
rogerdigital Apr 14, 2026
8b097f2
feat(web): add TradingPage button loading styles
rogerdigital Apr 14, 2026
ffd2891
feat(charts): install lightweight-charts dependency
rogerdigital Apr 14, 2026
0f50093
feat(charts): implement EquityChart component
rogerdigital Apr 14, 2026
0954353
feat(charts): implement SignalBarChart component
rogerdigital Apr 14, 2026
647bdcd
feat(charts): implement CandlestickChart component
rogerdigital Apr 14, 2026
6008c3f
feat(charts): replace ChartCanvas with new chart components
rogerdigital Apr 14, 2026
7928084
feat(trading): replace Canvas K-line with CandlestickChart
rogerdigital Apr 14, 2026
c6984d8
feat(charts): verify typecheck after chart replacement
rogerdigital Apr 14, 2026
094223f
feat(api): add GET /api/market/ohlcv endpoint
rogerdigital Apr 14, 2026
c197efd
feat(api): register market router in platform-routes
rogerdigital Apr 14, 2026
db21c36
feat(web): add fetchOhlcv to controlPlane API client
rogerdigital Apr 14, 2026
fc60441
feat(web): implement useOhlcvData hook
rogerdigital Apr 14, 2026
cf17124
feat(trading): use real OHLCV data in CandlestickChart
rogerdigital Apr 14, 2026
ffc45e0
feat(backtest): define backtest engine types
rogerdigital Apr 14, 2026
26b0c39
feat(backtest): implement historical OHLCV generator
rogerdigital Apr 14, 2026
b8f7aff
feat(backtest): implement metrics calculators
rogerdigital Apr 14, 2026
e073296
feat(backtest): implement event-driven backtest engine
rogerdigital Apr 14, 2026
a965cb8
feat(backtest): wire engine into task-workflow-engine
rogerdigital Apr 14, 2026
3491855
feat(sse): implement SSE connection manager
rogerdigital Apr 14, 2026
aafa513
feat(sse): add GET /api/sse/state endpoint
rogerdigital Apr 14, 2026
0b811d4
feat(sse): register SSE router in platform-routes
rogerdigital Apr 14, 2026
4b3fa26
feat(sse): broadcast after state cycle completion
rogerdigital Apr 14, 2026
0f24237
feat(web): implement useSSE hook
rogerdigital Apr 14, 2026
975130c
feat(web): integrate SSE into TradingSystemProvider
rogerdigital Apr 14, 2026
4e47aa9
feat(agent): add AgentDailySummaryCard styles
rogerdigital Apr 14, 2026
dfe3083
feat(agent): implement AgentDailySummaryCard component
rogerdigital Apr 14, 2026
ce1a999
feat(dashboard): add AgentDailySummaryCard to OverviewPage
rogerdigital Apr 14, 2026
a79656a
feat(risk): implement VaR/CVaR calculator
rogerdigital Apr 14, 2026
24f3abf
feat(risk): implement Beta and HHI calculators
rogerdigital Apr 14, 2026
86f16a7
feat(types): add analytics field to RiskWorkbench response
rogerdigital Apr 14, 2026
969747f
feat(risk): compute analytics in getRiskWorkbench
rogerdigital Apr 14, 2026
18febd9
feat(api): install hono and @hono/node-server
rogerdigital Apr 14, 2026
ba55880
feat(api): create Hono app with middleware
rogerdigital Apr 14, 2026
3c728c3
feat(api): migrate execution routes to Hono router
rogerdigital Apr 14, 2026
246bb73
feat(api): add USE_HONO feature flag to main.ts
rogerdigital Apr 14, 2026
a4082f4
feat(db): install better-sqlite3 and drizzle-orm
rogerdigital Apr 14, 2026
c6d4d89
feat(db): define Drizzle schema with JSON blob strategy
rogerdigital Apr 14, 2026
9189028
feat(db): implement SQLite adapter
rogerdigital Apr 14, 2026
afe9b9c
feat(db): wire SQLite adapter into createEmbeddedDbStore
rogerdigital Apr 14, 2026
3910458
feat(auth): install jose
rogerdigital Apr 14, 2026
97a76a3
feat(auth): implement JWT service
rogerdigital Apr 14, 2026
b85eb4f
feat(auth): implement Broker Key encryption service
rogerdigital Apr 14, 2026
2cd4b96
feat(auth): add POST /api/auth/login endpoint
rogerdigital Apr 14, 2026
6acce53
feat(auth): add optional JWT validation to getSession
rogerdigital Apr 14, 2026
6c892b9
feat(auth): encrypt broker keys in user-account-router
rogerdigital Apr 14, 2026
e53499d
feat(auth): update .env.example with new secrets
rogerdigital Apr 14, 2026
b2cdd06
style: fix biome formatting violations
rogerdigital Apr 14, 2026
ca34225
fix(db): align SQLite adapter metadata with legacy db adapter contracts
rogerdigital Apr 14, 2026
2946f3f
fix(sse): correct import path for sse-manager in sse-router
rogerdigital Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ ALPACA_DATA_FEED=iex
# file | db
QUANTPILOT_CONTROL_PLANE_ADAPTER=file
QUANTPILOT_CONTROL_PLANE_NAMESPACE=control-plane

# Auth (JWT + Broker Key Encryption)
JWT_SECRET=your-secret-key-at-least-32-chars
BROKER_KEY_ENCRYPTION_KEY=0000000000000000000000000000000000000000000000000000000000000000
DEMO_USERNAME=admin
DEMO_PASSWORD=changeme
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ npm-debug.log*

# Testing
coverage/
.quantpilot/
5 changes: 5 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
"type": "module",
"scripts": {
"dev": "node src/main.mjs"
},
"dependencies": {
"@hono/node-server": "^1.19.14",
"hono": "^4.12.12",
"jose": "^6.2.2"
}
}
30 changes: 30 additions & 0 deletions apps/api/src/app/hono-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @ts-nocheck
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { timing } from 'hono/timing';

export function createHonoApp() {
const app = new Hono();

app.use(
'*',
cors({
origin: '*',
allowMethods: ['GET', 'POST', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
})
);

app.use('*', logger());
app.use('*', timing());

app.onError((err, c) => {
console.error('[hono-gateway]', err.message);
return c.json({ ok: false, message: err.message }, 500);
});

app.notFound((c) => c.json({ ok: false, message: 'Not found' }, 404));

return app;
}
165 changes: 165 additions & 0 deletions apps/api/src/app/routes/hono/execution-hono-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// @ts-nocheck
import { Hono } from 'hono';
import {
approveExecutionPlan,
bulkOperateExecutionPlans,
cancelExecutionPlan,
compensateExecutionPlan,
ingestBrokerExecutionEvent,
reconcileExecutionPlan,
recoverExecutionPlan,
settleExecutionPlan,
syncExecutionPlan,
} from '../../../domains/execution/services/lifecycle-service.js';
import {
getExecutionPlanDetail,
getExecutionWorkbench,
getLatestBrokerAccountSnapshot,
listBrokerAccountSnapshots,
listBrokerExecutionEvents,
listExecutionLedger,
listExecutionPlans,
listExecutionRuntimeEvents,
} from '../../../domains/execution/services/query-service.js';
import { hasPermission, writeForbiddenJson } from '../../../modules/auth/service.js';

function requireApproval(c, action = '') {
if (!hasPermission('execution:approve')) {
return c.json(
{ ok: false, message: `Permission 'execution:approve' required to ${action}` },
403
);
}
return null;
}

export function createExecutionHonoRouter() {
const router = new Hono();

router.get('/plans', (c) => {
return c.json({ ok: true, plans: listExecutionPlans() });
});

router.get('/workbench', (c) => {
return c.json(getExecutionWorkbench());
});

router.post('/plans/bulk', async (c) => {
const denied = requireApproval(c, 'run bulk execution actions');
if (denied) return denied;
const body = await c.req.json();
const result = bulkOperateExecutionPlans(body);
return c.json(result, result.ok ? 200 : 400);
});

router.get('/plans/:planId', (c) => {
const planId = c.req.param('planId');
const detail = getExecutionPlanDetail(planId);
return detail
? c.json({ ok: true, ...detail })
: c.json({ ok: false, message: 'execution plan not found' }, 404);
});

router.get('/runtime', (c) => {
return c.json({ ok: true, events: listExecutionRuntimeEvents() });
});

router.get('/account-snapshots', (c) => {
return c.json({ ok: true, snapshots: listBrokerAccountSnapshots() });
});

router.get('/account-snapshots/latest', (c) => {
return c.json({ ok: true, snapshot: getLatestBrokerAccountSnapshot() });
});

router.get('/broker-events', (c) => {
const q = c.req.query;
return c.json({
ok: true,
events: listBrokerExecutionEvents(Number(q('limit') || 40), {
executionPlanId: q('executionPlanId') || '',
executionRunId: q('executionRunId') || '',
symbol: q('symbol') || '',
eventType: q('eventType') || '',
}),
});
});

router.get('/ledger', (c) => {
return c.json({ ok: true, entries: listExecutionLedger() });
});

router.post('/plans/:planId/approve', async (c) => {
const denied = requireApproval(c, 'approve execution plans');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = approveExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/settle', async (c) => {
const denied = requireApproval(c, 'settle execution plans');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = settleExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/sync', async (c) => {
const denied = requireApproval(c, 'sync execution plans');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = syncExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/broker-events', async (c) => {
const denied = requireApproval(c, 'ingest broker execution events');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = ingestBrokerExecutionEvent(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/cancel', async (c) => {
const denied = requireApproval(c, 'cancel execution plans');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = cancelExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/reconcile', async (c) => {
const denied = requireApproval(c, 'reconcile execution plans');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = reconcileExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/compensate', async (c) => {
const denied = requireApproval(c, 'run execution compensation automation');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = compensateExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

router.post('/plans/:planId/recover', async (c) => {
const denied = requireApproval(c, 'recover execution plans');
if (denied) return denied;
const planId = c.req.param('planId');
const body = await c.req.json();
const result = recoverExecutionPlan(planId, body);
return c.json(result, result.ok ? 200 : 409);
});

return router;
}
6 changes: 6 additions & 0 deletions apps/api/src/app/routes/platform-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { handleAuthRoutes } from './routers/auth-router.js';
import { handleBacktestRoutes } from './routers/backtest-router.js';
import { handleExecutionRoutes } from './routers/execution-router.js';
import { handleHealthRoutes } from './routers/health-router.js';
import { handleMarketRoutes } from './routers/market-router.js';
import { handleMonitoringRoutes } from './routers/monitoring-router.js';
import { handleOperationsRoutes } from './routers/operations-router.js';
import { handleResearchRoutes } from './routers/research-router.js';
import { handleSseRoutes } from './routers/sse-router.js';
import { handleStrategyRoutes } from './routers/strategy-router.js';
import { handleTradingRoutes } from './routers/trading-router.js';
import { handleUserAccountRoutes } from './routers/user-account-router.js';

const routers = [
handleSseRoutes,
handleHealthRoutes,
handleMonitoringRoutes,
handleOperationsRoutes,
Expand All @@ -22,6 +26,8 @@ const routers = [
handleBacktestRoutes,
handleResearchRoutes,
handleExecutionRoutes,
handleTradingRoutes,
handleMarketRoutes,
];

export async function handlePlatformRoutes(context) {
Expand Down
40 changes: 38 additions & 2 deletions apps/api/src/app/routes/routers/auth-router.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
// @ts-nocheck

import { createHash } from 'node:crypto';
import { signToken } from '../../../modules/auth/jwt-service.js';
import { listPermissionDescriptors } from '../../../modules/auth/permission-catalog.js';
import { getSession } from '../../../modules/auth/service.js';

export function handleAuthRoutes({ req, reqUrl, res, writeJson }) {
function sha256hex(value) {
return createHash('sha256').update(value).digest('hex');
}

export async function handleAuthRoutes({ req, reqUrl, res, readJsonBody, writeJson }) {
if (req.method === 'GET' && reqUrl.pathname === '/api/auth/session') {
writeJson(res, 200, getSession());
return true;
Expand All @@ -14,5 +19,36 @@ export function handleAuthRoutes({ req, reqUrl, res, writeJson }) {
return true;
}

if (req.method === 'POST' && reqUrl.pathname === '/api/auth/login') {
const body = await readJsonBody(req);
const { username, password } = body || {};

const expectedUsername = process.env.DEMO_USERNAME ?? 'admin';
const expectedPasswordHash = sha256hex(process.env.DEMO_PASSWORD ?? 'changeme');

if (
typeof username !== 'string' ||
typeof password !== 'string' ||
username !== expectedUsername ||
sha256hex(password) !== expectedPasswordHash
) {
writeJson(res, 401, { ok: false, message: 'Invalid credentials' });
return true;
}

const permissions = [
'dashboard:read',
'strategy:write',
'risk:review',
'execution:approve',
'account:write',
];
const expiresIn = '8h';
const token = await signToken({ userId: username, permissions }, expiresIn);
const expiresAt = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString();
writeJson(res, 200, { ok: true, token, expiresAt });
return true;
}

return false;
}
Loading
Loading