feat(proxy): Add Express proxy gateway for Python services (ARCH-001)

- Add proxy module with types, service, controller, and routes
- Configure llmAgent and dataService in config
- Register proxy routes in main Express app
- All Python service access now goes through authenticated Express gateway

ARCH-001: Centralized proxy with auth, logging, and error handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-28 15:43:41 -06:00
parent b99953bf3e
commit 58a7b44673
18 changed files with 3539 additions and 0 deletions

View File

@ -68,6 +68,16 @@ export const config = {
timeout: parseInt(process.env.ML_ENGINE_TIMEOUT || '5000', 10),
},
llmAgent: {
url: process.env.LLM_AGENT_URL || 'http://localhost:3085',
timeout: parseInt(process.env.LLM_AGENT_TIMEOUT || '30000', 10),
},
dataService: {
url: process.env.DATA_SERVICE_URL || 'http://localhost:3084',
timeout: parseInt(process.env.DATA_SERVICE_TIMEOUT || '10000', 10),
},
firebase: {
serviceAccountKey: process.env.FIREBASE_SERVICE_ACCOUNT_KEY || '',
projectId: process.env.FIREBASE_PROJECT_ID || '',

View File

@ -40,6 +40,7 @@ import { notificationRouter } from './modules/notifications/notification.routes.
import { marketDataRouter } from './modules/market-data/index.js';
import { currencyRouter } from './modules/currency/index.js';
import { auditRouter } from './modules/audit/index.js';
import { proxyRoutes } from './modules/proxy/index.js';
// Service clients for health checks
import { tradingAgentsClient, mlEngineClient, llmAgentClient } from './shared/clients/index.js';
@ -160,6 +161,7 @@ apiRouter.use('/notifications', notificationRouter);
apiRouter.use('/market-data', marketDataRouter);
apiRouter.use('/currency', currencyRouter);
apiRouter.use('/audit', auditRouter);
apiRouter.use('/proxy', proxyRoutes); // ARCH-001: Gateway to Python services
// Mount API router
app.use('/api/v1', apiRouter);

View File

@ -0,0 +1,97 @@
/**
* Audit Routes
* API endpoints for audit logs, security events, and compliance tracking
*/
import { Router, RequestHandler } from 'express';
import * as auditController from './controllers/audit.controller';
import { requireAuth, requireAdmin } from '../../core/guards/auth.guard';
const router = Router();
// Type cast helper for authenticated routes
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const authHandler = (fn: Function): RequestHandler => fn as RequestHandler;
/**
* GET /api/v1/audit/my-activity
* Get current user's own audit logs
* Authentication required
*/
router.get('/my-activity', requireAuth, authHandler(auditController.getMyActivity));
/**
* GET /api/v1/audit/logs
* Get audit logs with filters
* Admin only
* Query params: userId, eventType, resourceType, resourceId, severity, dateFrom, dateTo, limit, offset
*/
router.get('/logs', requireAuth, requireAdmin, authHandler(auditController.getAuditLogs));
/**
* GET /api/v1/audit/security-events
* Get security events with filters
* Admin only
* Query params: userId, category, severity, isBlocked, requiresReview, dateFrom, dateTo, limit, offset
*/
router.get(
'/security-events',
requireAuth,
requireAdmin,
authHandler(auditController.getSecurityEvents)
);
/**
* GET /api/v1/audit/compliance
* Get all compliance logs
* Admin only
* Query params: userId, regulation, complianceStatus, riskLevel, remediationRequired, dateFrom, dateTo, limit, offset
*/
router.get(
'/compliance',
requireAuth,
requireAdmin,
authHandler(auditController.getComplianceLogs)
);
/**
* GET /api/v1/audit/compliance/:userId
* Get compliance logs for a specific user
* Admin only
* Query params: regulation, complianceStatus, riskLevel, remediationRequired, dateFrom, dateTo, limit, offset
*/
router.get(
'/compliance/:userId',
requireAuth,
requireAdmin,
authHandler(auditController.getUserComplianceLogs)
);
/**
* GET /api/v1/audit/stats
* Get audit statistics
* Admin only
* Query params: dateFrom, dateTo
*/
router.get('/stats', requireAuth, requireAdmin, authHandler(auditController.getAuditStats));
/**
* POST /api/v1/audit/log
* Create an audit log entry
* Admin only (for internal/system use)
*/
router.post('/log', requireAuth, requireAdmin, authHandler(auditController.createAuditLog));
/**
* POST /api/v1/audit/security-event
* Log a security event
* Admin only (for internal/system use)
*/
router.post(
'/security-event',
requireAuth,
requireAdmin,
authHandler(auditController.createSecurityEvent)
);
export { router as auditRouter };

View File

@ -0,0 +1,328 @@
/**
* Audit Controller
* Handles audit-related HTTP requests
*/
import { Response } from 'express';
import { AuthenticatedRequest } from '../../../core/guards/auth.guard';
import { auditService } from '../services/audit.service';
import { logger } from '../../../shared/utils/logger';
import type {
AuditLogFilters,
SecurityEventFilters,
ComplianceLogFilters,
} from '../types/audit.types';
/**
* GET /api/v1/audit/logs
* Get audit logs (admin only)
*/
export async function getAuditLogs(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const {
userId,
eventType,
resourceType,
resourceId,
severity,
dateFrom,
dateTo,
limit,
offset,
} = req.query;
const filters: AuditLogFilters = {
userId: userId as string | undefined,
eventType: eventType as AuditLogFilters['eventType'],
resourceType: resourceType as AuditLogFilters['resourceType'],
resourceId: resourceId as string | undefined,
severity: severity as AuditLogFilters['severity'],
dateFrom: dateFrom ? new Date(dateFrom as string) : undefined,
dateTo: dateTo ? new Date(dateTo as string) : undefined,
limit: limit ? parseInt(limit as string, 10) : undefined,
offset: offset ? parseInt(offset as string, 10) : undefined,
};
const logs = await auditService.getAuditLogs(filters);
res.json({
success: true,
data: logs,
});
} catch (error) {
logger.error('[AuditController] Failed to get audit logs:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve audit logs',
});
}
}
/**
* GET /api/v1/audit/security-events
* Get security events (admin only)
*/
export async function getSecurityEvents(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const {
userId,
category,
severity,
isBlocked,
requiresReview,
dateFrom,
dateTo,
limit,
offset,
} = req.query;
const filters: SecurityEventFilters = {
userId: userId as string | undefined,
category: category as SecurityEventFilters['category'],
severity: severity as SecurityEventFilters['severity'],
isBlocked: isBlocked === 'true' ? true : isBlocked === 'false' ? false : undefined,
requiresReview: requiresReview === 'true' ? true : requiresReview === 'false' ? false : undefined,
dateFrom: dateFrom ? new Date(dateFrom as string) : undefined,
dateTo: dateTo ? new Date(dateTo as string) : undefined,
limit: limit ? parseInt(limit as string, 10) : undefined,
offset: offset ? parseInt(offset as string, 10) : undefined,
};
const events = await auditService.getSecurityEvents(filters);
res.json({
success: true,
data: events,
});
} catch (error) {
logger.error('[AuditController] Failed to get security events:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve security events',
});
}
}
/**
* GET /api/v1/audit/compliance/:userId
* Get compliance logs for a user (admin only)
*/
export async function getUserComplianceLogs(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { userId } = req.params;
const {
regulation,
complianceStatus,
riskLevel,
remediationRequired,
dateFrom,
dateTo,
limit,
offset,
} = req.query;
const filters: ComplianceLogFilters = {
userId,
regulation: regulation as ComplianceLogFilters['regulation'],
complianceStatus: complianceStatus as ComplianceLogFilters['complianceStatus'],
riskLevel: riskLevel as ComplianceLogFilters['riskLevel'],
remediationRequired: remediationRequired === 'true' ? true : remediationRequired === 'false' ? false : undefined,
dateFrom: dateFrom ? new Date(dateFrom as string) : undefined,
dateTo: dateTo ? new Date(dateTo as string) : undefined,
limit: limit ? parseInt(limit as string, 10) : undefined,
offset: offset ? parseInt(offset as string, 10) : undefined,
};
const logs = await auditService.getComplianceLogs(filters);
res.json({
success: true,
data: logs,
});
} catch (error) {
logger.error('[AuditController] Failed to get compliance logs:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve compliance logs',
});
}
}
/**
* GET /api/v1/audit/compliance
* Get all compliance logs (admin only)
*/
export async function getComplianceLogs(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const {
userId,
regulation,
complianceStatus,
riskLevel,
remediationRequired,
dateFrom,
dateTo,
limit,
offset,
} = req.query;
const filters: ComplianceLogFilters = {
userId: userId as string | undefined,
regulation: regulation as ComplianceLogFilters['regulation'],
complianceStatus: complianceStatus as ComplianceLogFilters['complianceStatus'],
riskLevel: riskLevel as ComplianceLogFilters['riskLevel'],
remediationRequired: remediationRequired === 'true' ? true : remediationRequired === 'false' ? false : undefined,
dateFrom: dateFrom ? new Date(dateFrom as string) : undefined,
dateTo: dateTo ? new Date(dateTo as string) : undefined,
limit: limit ? parseInt(limit as string, 10) : undefined,
offset: offset ? parseInt(offset as string, 10) : undefined,
};
const logs = await auditService.getComplianceLogs(filters);
res.json({
success: true,
data: logs,
});
} catch (error) {
logger.error('[AuditController] Failed to get compliance logs:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve compliance logs',
});
}
}
/**
* GET /api/v1/audit/my-activity
* Get current user's audit logs
*/
export async function getMyActivity(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const userId = req.user!.id;
const { limit, offset } = req.query;
const logs = await auditService.getUserAuditLogs(
userId,
limit ? parseInt(limit as string, 10) : 100,
offset ? parseInt(offset as string, 10) : 0
);
res.json({
success: true,
data: logs,
});
} catch (error) {
logger.error('[AuditController] Failed to get user activity:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve your activity logs',
});
}
}
/**
* GET /api/v1/audit/stats
* Get audit statistics (admin only)
*/
export async function getAuditStats(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { dateFrom, dateTo } = req.query;
const stats = await auditService.getAuditStats(
dateFrom ? new Date(dateFrom as string) : undefined,
dateTo ? new Date(dateTo as string) : undefined
);
res.json({
success: true,
data: stats,
});
} catch (error) {
logger.error('[AuditController] Failed to get audit stats:', error);
res.status(500).json({
success: false,
error: 'Failed to retrieve audit statistics',
});
}
}
/**
* POST /api/v1/audit/log
* Create an audit log entry (internal use)
*/
export async function createAuditLog(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const data = req.body;
const log = await auditService.logAction({
...data,
userId: data.userId || req.user!.id,
ipAddress: data.ipAddress || (req.ip as string),
userAgent: data.userAgent || req.headers['user-agent'],
});
res.status(201).json({
success: true,
data: log,
});
} catch (error) {
logger.error('[AuditController] Failed to create audit log:', error);
res.status(500).json({
success: false,
error: 'Failed to create audit log',
});
}
}
/**
* POST /api/v1/audit/security-event
* Log a security event (internal use)
*/
export async function createSecurityEvent(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const data = req.body;
const event = await auditService.logSecurityEvent({
...data,
userId: data.userId || req.user!.id,
ipAddress: data.ipAddress || (req.ip as string),
userAgent: data.userAgent || req.headers['user-agent'],
});
res.status(201).json({
success: true,
data: event,
});
} catch (error) {
logger.error('[AuditController] Failed to create security event:', error);
res.status(500).json({
success: false,
error: 'Failed to create security event',
});
}
}

View File

@ -0,0 +1,8 @@
/**
* Audit Module
* Exports for audit logging, security events, and compliance tracking
*/
export { auditRouter } from './audit.routes';
export { auditService } from './services/audit.service';
export * from './types/audit.types';

View File

@ -0,0 +1,653 @@
/**
* Audit Service
* Handles audit logging, security events, and compliance tracking
*/
import { db } from '../../../shared/database';
import { logger } from '../../../shared/utils/logger';
import type {
AuditLog,
CreateAuditLogInput,
AuditLogFilters,
SecurityEvent,
CreateSecurityEventInput,
SecurityEventFilters,
ComplianceLog,
CreateComplianceLogInput,
ComplianceLogFilters,
AuditStats,
AuditEventType,
EventSeverity,
} from '../types/audit.types';
interface AuditLogRow {
id: string;
event_type: string;
event_status: string;
severity: string;
user_id: string | null;
session_id: string | null;
ip_address: string | null;
user_agent: string | null;
resource_type: string;
resource_id: string | null;
resource_name: string | null;
action: string;
description: string | null;
old_values: string | null;
new_values: string | null;
metadata: string;
request_id: string | null;
correlation_id: string | null;
service_name: string | null;
created_at: string;
}
interface SecurityEventRow {
id: string;
category: string;
severity: string;
event_status: string;
user_id: string | null;
ip_address: string;
user_agent: string | null;
geo_location: string | null;
event_code: string;
event_name: string;
description: string | null;
request_path: string | null;
request_method: string | null;
response_code: number | null;
risk_score: string | null;
is_blocked: boolean;
block_reason: string | null;
requires_review: boolean;
reviewed_by: string | null;
reviewed_at: string | null;
review_notes: string | null;
raw_data: string;
created_at: string;
}
interface ComplianceLogRow {
id: string;
regulation: string;
requirement: string;
event_type: string;
event_description: string;
user_id: string | null;
system_initiated: boolean;
compliance_status: string;
risk_level: string | null;
evidence: string | null;
remediation_required: boolean;
remediation_deadline: string | null;
remediation_notes: string | null;
reviewed_by: string | null;
reviewed_at: string | null;
metadata: string;
created_at: string;
updated_at: string;
}
class AuditService {
/**
* Get audit logs with optional filters
*/
async getAuditLogs(filters: AuditLogFilters = {}): Promise<AuditLog[]> {
const {
userId,
eventType,
resourceType,
resourceId,
severity,
dateFrom,
dateTo,
limit = 100,
offset = 0,
} = filters;
const conditions: string[] = [];
const params: (string | number | Date)[] = [];
let paramIndex = 1;
if (userId) {
conditions.push(`user_id = $${paramIndex++}`);
params.push(userId);
}
if (eventType) {
conditions.push(`event_type = $${paramIndex++}`);
params.push(eventType);
}
if (resourceType) {
conditions.push(`resource_type = $${paramIndex++}`);
params.push(resourceType);
}
if (resourceId) {
conditions.push(`resource_id = $${paramIndex++}`);
params.push(resourceId);
}
if (severity) {
conditions.push(`severity = $${paramIndex++}`);
params.push(severity);
}
if (dateFrom) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(dateFrom);
}
if (dateTo) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(dateTo);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await db.query<AuditLogRow>(
`SELECT * FROM audit.audit_logs
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...params, limit, offset]
);
return result.rows.map(this.transformAuditLog);
}
/**
* Get audit logs for a specific user
*/
async getUserAuditLogs(userId: string, limit = 100, offset = 0): Promise<AuditLog[]> {
return this.getAuditLogs({ userId, limit, offset });
}
/**
* Create a new audit log entry
*/
async logAction(data: CreateAuditLogInput): Promise<AuditLog> {
const {
eventType,
eventStatus = 'success',
severity = 'info',
userId,
sessionId,
ipAddress,
userAgent,
resourceType,
resourceId,
resourceName,
action,
description,
oldValues,
newValues,
metadata = {},
requestId,
correlationId,
serviceName,
} = data;
const result = await db.query<AuditLogRow>(
`INSERT INTO audit.audit_logs (
event_type, event_status, severity,
user_id, session_id, ip_address, user_agent,
resource_type, resource_id, resource_name,
action, description, old_values, new_values,
metadata, request_id, correlation_id, service_name
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18
) RETURNING *`,
[
eventType,
eventStatus,
severity,
userId || null,
sessionId || null,
ipAddress || null,
userAgent || null,
resourceType,
resourceId || null,
resourceName || null,
action,
description || null,
oldValues ? JSON.stringify(oldValues) : null,
newValues ? JSON.stringify(newValues) : null,
JSON.stringify(metadata),
requestId || null,
correlationId || null,
serviceName || null,
]
);
logger.info('[AuditService] Action logged:', {
eventType,
action,
userId,
resourceType,
resourceId,
});
return this.transformAuditLog(result.rows[0]);
}
/**
* Get security events with optional filters
*/
async getSecurityEvents(filters: SecurityEventFilters = {}): Promise<SecurityEvent[]> {
const {
userId,
category,
severity,
isBlocked,
requiresReview,
dateFrom,
dateTo,
limit = 100,
offset = 0,
} = filters;
const conditions: string[] = [];
const params: (string | number | boolean | Date)[] = [];
let paramIndex = 1;
if (userId) {
conditions.push(`user_id = $${paramIndex++}`);
params.push(userId);
}
if (category) {
conditions.push(`category = $${paramIndex++}`);
params.push(category);
}
if (severity) {
conditions.push(`severity = $${paramIndex++}`);
params.push(severity);
}
if (isBlocked !== undefined) {
conditions.push(`is_blocked = $${paramIndex++}`);
params.push(isBlocked);
}
if (requiresReview !== undefined) {
conditions.push(`requires_review = $${paramIndex++}`);
params.push(requiresReview);
}
if (dateFrom) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(dateFrom);
}
if (dateTo) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(dateTo);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await db.query<SecurityEventRow>(
`SELECT * FROM audit.security_events
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...params, limit, offset]
);
return result.rows.map(this.transformSecurityEvent);
}
/**
* Create a new security event
*/
async logSecurityEvent(data: CreateSecurityEventInput): Promise<SecurityEvent> {
const {
category,
severity,
eventStatus = 'success',
userId,
ipAddress,
userAgent,
geoLocation,
eventCode,
eventName,
description,
requestPath,
requestMethod,
responseCode,
riskScore,
isBlocked = false,
blockReason,
requiresReview = false,
rawData = {},
} = data;
const result = await db.query<SecurityEventRow>(
`INSERT INTO audit.security_events (
category, severity, event_status,
user_id, ip_address, user_agent, geo_location,
event_code, event_name, description,
request_path, request_method, response_code,
risk_score, is_blocked, block_reason,
requires_review, raw_data
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18
) RETURNING *`,
[
category,
severity,
eventStatus,
userId || null,
ipAddress,
userAgent || null,
geoLocation ? JSON.stringify(geoLocation) : null,
eventCode,
eventName,
description || null,
requestPath || null,
requestMethod || null,
responseCode || null,
riskScore || null,
isBlocked,
blockReason || null,
requiresReview,
JSON.stringify(rawData),
]
);
logger.warn('[AuditService] Security event logged:', {
category,
severity,
eventCode,
eventName,
userId,
isBlocked,
});
return this.transformSecurityEvent(result.rows[0]);
}
/**
* Get compliance logs with optional filters
*/
async getComplianceLogs(filters: ComplianceLogFilters = {}): Promise<ComplianceLog[]> {
const {
userId,
regulation,
complianceStatus,
riskLevel,
remediationRequired,
dateFrom,
dateTo,
limit = 100,
offset = 0,
} = filters;
const conditions: string[] = [];
const params: (string | number | boolean | Date)[] = [];
let paramIndex = 1;
if (userId) {
conditions.push(`user_id = $${paramIndex++}`);
params.push(userId);
}
if (regulation) {
conditions.push(`regulation = $${paramIndex++}`);
params.push(regulation);
}
if (complianceStatus) {
conditions.push(`compliance_status = $${paramIndex++}`);
params.push(complianceStatus);
}
if (riskLevel) {
conditions.push(`risk_level = $${paramIndex++}`);
params.push(riskLevel);
}
if (remediationRequired !== undefined) {
conditions.push(`remediation_required = $${paramIndex++}`);
params.push(remediationRequired);
}
if (dateFrom) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(dateFrom);
}
if (dateTo) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(dateTo);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await db.query<ComplianceLogRow>(
`SELECT * FROM audit.compliance_logs
${whereClause}
ORDER BY created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex++}`,
[...params, limit, offset]
);
return result.rows.map(this.transformComplianceLog);
}
/**
* Create a new compliance log entry
*/
async logComplianceEvent(data: CreateComplianceLogInput): Promise<ComplianceLog> {
const {
regulation,
requirement,
eventType,
eventDescription,
userId,
systemInitiated = false,
complianceStatus,
riskLevel,
evidence,
remediationRequired = false,
remediationDeadline,
remediationNotes,
metadata = {},
} = data;
const result = await db.query<ComplianceLogRow>(
`INSERT INTO audit.compliance_logs (
regulation, requirement, event_type, event_description,
user_id, system_initiated, compliance_status, risk_level,
evidence, remediation_required, remediation_deadline,
remediation_notes, metadata
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
) RETURNING *`,
[
regulation,
requirement,
eventType,
eventDescription,
userId || null,
systemInitiated,
complianceStatus,
riskLevel || null,
evidence ? JSON.stringify(evidence) : null,
remediationRequired,
remediationDeadline || null,
remediationNotes || null,
JSON.stringify(metadata),
]
);
logger.info('[AuditService] Compliance event logged:', {
regulation,
requirement,
complianceStatus,
riskLevel,
});
return this.transformComplianceLog(result.rows[0]);
}
/**
* Get audit statistics
*/
async getAuditStats(dateFrom?: Date, dateTo?: Date): Promise<AuditStats> {
const conditions: string[] = [];
const params: Date[] = [];
let paramIndex = 1;
if (dateFrom) {
conditions.push(`created_at >= $${paramIndex++}`);
params.push(dateFrom);
}
if (dateTo) {
conditions.push(`created_at <= $${paramIndex++}`);
params.push(dateTo);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await db.query<{
total: string;
event_type: string;
severity: string;
count: string;
}>(
`SELECT
COUNT(*) as total,
event_type,
severity,
COUNT(*) as count
FROM audit.audit_logs
${whereClause}
GROUP BY event_type, severity`,
params
);
const stats: AuditStats = {
totalLogs: 0,
byEventType: {} as Record<AuditEventType, number>,
bySeverity: {} as Record<EventSeverity, number>,
criticalEvents: 0,
};
result.rows.forEach((row) => {
const count = parseInt(row.count, 10);
stats.totalLogs += count;
if (row.event_type) {
stats.byEventType[row.event_type as AuditEventType] =
(stats.byEventType[row.event_type as AuditEventType] || 0) + count;
}
if (row.severity) {
stats.bySeverity[row.severity as EventSeverity] =
(stats.bySeverity[row.severity as EventSeverity] || 0) + count;
if (row.severity === 'critical' || row.severity === 'error') {
stats.criticalEvents += count;
}
}
});
return stats;
}
/**
* Transform database row to AuditLog
*/
private transformAuditLog(row: AuditLogRow): AuditLog {
return {
id: row.id,
eventType: row.event_type as AuditLog['eventType'],
eventStatus: row.event_status as AuditLog['eventStatus'],
severity: row.severity as AuditLog['severity'],
userId: row.user_id,
sessionId: row.session_id,
ipAddress: row.ip_address,
userAgent: row.user_agent,
resourceType: row.resource_type as AuditLog['resourceType'],
resourceId: row.resource_id,
resourceName: row.resource_name,
action: row.action,
description: row.description,
oldValues: row.old_values ? JSON.parse(row.old_values) : null,
newValues: row.new_values ? JSON.parse(row.new_values) : null,
metadata: JSON.parse(row.metadata),
requestId: row.request_id,
correlationId: row.correlation_id,
serviceName: row.service_name,
createdAt: new Date(row.created_at),
};
}
/**
* Transform database row to SecurityEvent
*/
private transformSecurityEvent(row: SecurityEventRow): SecurityEvent {
return {
id: row.id,
category: row.category as SecurityEvent['category'],
severity: row.severity as SecurityEvent['severity'],
eventStatus: row.event_status as SecurityEvent['eventStatus'],
userId: row.user_id,
ipAddress: row.ip_address,
userAgent: row.user_agent,
geoLocation: row.geo_location ? JSON.parse(row.geo_location) : null,
eventCode: row.event_code,
eventName: row.event_name,
description: row.description,
requestPath: row.request_path,
requestMethod: row.request_method,
responseCode: row.response_code,
riskScore: row.risk_score ? parseFloat(row.risk_score) : null,
isBlocked: row.is_blocked,
blockReason: row.block_reason,
requiresReview: row.requires_review,
reviewedBy: row.reviewed_by,
reviewedAt: row.reviewed_at ? new Date(row.reviewed_at) : null,
reviewNotes: row.review_notes,
rawData: JSON.parse(row.raw_data),
createdAt: new Date(row.created_at),
};
}
/**
* Transform database row to ComplianceLog
*/
private transformComplianceLog(row: ComplianceLogRow): ComplianceLog {
return {
id: row.id,
regulation: row.regulation as ComplianceLog['regulation'],
requirement: row.requirement,
eventType: row.event_type,
eventDescription: row.event_description,
userId: row.user_id,
systemInitiated: row.system_initiated,
complianceStatus: row.compliance_status as ComplianceLog['complianceStatus'],
riskLevel: row.risk_level as ComplianceLog['riskLevel'],
evidence: row.evidence ? JSON.parse(row.evidence) : null,
remediationRequired: row.remediation_required,
remediationDeadline: row.remediation_deadline ? new Date(row.remediation_deadline) : null,
remediationNotes: row.remediation_notes,
reviewedBy: row.reviewed_by,
reviewedAt: row.reviewed_at ? new Date(row.reviewed_at) : null,
metadata: JSON.parse(row.metadata),
createdAt: new Date(row.created_at),
updatedAt: new Date(row.updated_at),
};
}
}
export const auditService = new AuditService();

View File

@ -0,0 +1,239 @@
/**
* Audit Module Types
* Type definitions for audit logging, security events, and compliance
*/
export type AuditEventType =
| 'create'
| 'read'
| 'update'
| 'delete'
| 'login'
| 'logout'
| 'permission_change'
| 'config_change'
| 'export'
| 'import';
export type EventSeverity =
| 'debug'
| 'info'
| 'warning'
| 'error'
| 'critical';
export type SecurityEventCategory =
| 'authentication'
| 'authorization'
| 'data_access'
| 'configuration'
| 'suspicious_activity'
| 'compliance';
export type EventStatus =
| 'success'
| 'failure'
| 'blocked'
| 'pending_review';
export type ResourceType =
| 'user'
| 'account'
| 'transaction'
| 'order'
| 'position'
| 'bot'
| 'subscription'
| 'payment'
| 'course'
| 'model'
| 'system_config';
export type ComplianceStatus =
| 'compliant'
| 'non_compliant'
| 'remediation';
export type RiskLevel =
| 'low'
| 'medium'
| 'high'
| 'critical';
export type Regulation =
| 'GDPR'
| 'CCPA'
| 'SOX'
| 'PCI-DSS'
| 'MiFID';
export interface AuditLog {
id: string;
eventType: AuditEventType;
eventStatus: EventStatus;
severity: EventSeverity;
userId: string | null;
sessionId: string | null;
ipAddress: string | null;
userAgent: string | null;
resourceType: ResourceType;
resourceId: string | null;
resourceName: string | null;
action: string;
description: string | null;
oldValues: Record<string, unknown> | null;
newValues: Record<string, unknown> | null;
metadata: Record<string, unknown>;
requestId: string | null;
correlationId: string | null;
serviceName: string | null;
createdAt: Date;
}
export interface CreateAuditLogInput {
eventType: AuditEventType;
eventStatus?: EventStatus;
severity?: EventSeverity;
userId?: string;
sessionId?: string;
ipAddress?: string;
userAgent?: string;
resourceType: ResourceType;
resourceId?: string;
resourceName?: string;
action: string;
description?: string;
oldValues?: Record<string, unknown>;
newValues?: Record<string, unknown>;
metadata?: Record<string, unknown>;
requestId?: string;
correlationId?: string;
serviceName?: string;
}
export interface SecurityEvent {
id: string;
category: SecurityEventCategory;
severity: EventSeverity;
eventStatus: EventStatus;
userId: string | null;
ipAddress: string;
userAgent: string | null;
geoLocation: Record<string, unknown> | null;
eventCode: string;
eventName: string;
description: string | null;
requestPath: string | null;
requestMethod: string | null;
responseCode: number | null;
riskScore: number | null;
isBlocked: boolean;
blockReason: string | null;
requiresReview: boolean;
reviewedBy: string | null;
reviewedAt: Date | null;
reviewNotes: string | null;
rawData: Record<string, unknown>;
createdAt: Date;
}
export interface CreateSecurityEventInput {
category: SecurityEventCategory;
severity: EventSeverity;
eventStatus?: EventStatus;
userId?: string;
ipAddress: string;
userAgent?: string;
geoLocation?: Record<string, unknown>;
eventCode: string;
eventName: string;
description?: string;
requestPath?: string;
requestMethod?: string;
responseCode?: number;
riskScore?: number;
isBlocked?: boolean;
blockReason?: string;
requiresReview?: boolean;
rawData?: Record<string, unknown>;
}
export interface ComplianceLog {
id: string;
regulation: Regulation;
requirement: string;
eventType: string;
eventDescription: string;
userId: string | null;
systemInitiated: boolean;
complianceStatus: ComplianceStatus;
riskLevel: RiskLevel | null;
evidence: Record<string, unknown> | null;
remediationRequired: boolean;
remediationDeadline: Date | null;
remediationNotes: string | null;
reviewedBy: string | null;
reviewedAt: Date | null;
metadata: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
export interface CreateComplianceLogInput {
regulation: Regulation;
requirement: string;
eventType: string;
eventDescription: string;
userId?: string;
systemInitiated?: boolean;
complianceStatus: ComplianceStatus;
riskLevel?: RiskLevel;
evidence?: Record<string, unknown>;
remediationRequired?: boolean;
remediationDeadline?: Date;
remediationNotes?: string;
metadata?: Record<string, unknown>;
}
export interface AuditLogFilters {
userId?: string;
eventType?: AuditEventType;
resourceType?: ResourceType;
resourceId?: string;
severity?: EventSeverity;
dateFrom?: Date;
dateTo?: Date;
limit?: number;
offset?: number;
}
export interface SecurityEventFilters {
userId?: string;
category?: SecurityEventCategory;
severity?: EventSeverity;
isBlocked?: boolean;
requiresReview?: boolean;
dateFrom?: Date;
dateTo?: Date;
limit?: number;
offset?: number;
}
export interface ComplianceLogFilters {
userId?: string;
regulation?: Regulation;
complianceStatus?: ComplianceStatus;
riskLevel?: RiskLevel;
remediationRequired?: boolean;
dateFrom?: Date;
dateTo?: Date;
limit?: number;
offset?: number;
}
export interface AuditStats {
totalLogs: number;
byEventType: Record<AuditEventType, number>;
bySeverity: Record<EventSeverity, number>;
criticalEvents: number;
}

View File

@ -0,0 +1,414 @@
// Proxy Controller - REST API for Python Services Gateway
// ARCH-001 Fix: Exposes proxy endpoints with authentication
import { Request, Response } from 'express';
import { proxyService } from '../services/proxy.service';
export class ProxyController {
// ========== ML Engine Endpoints ==========
async getLatestSignal(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const result = await proxyService.getLatestSignal(symbol);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getActiveSignals(_req: Request, res: Response): Promise<void> {
const result = await proxyService.getActiveSignals();
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getAMDPhase(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const result = await proxyService.getAMDPhase(symbol);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getRangePrediction(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const result = await proxyService.getRangePrediction(symbol);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async generateSignal(req: Request, res: Response): Promise<void> {
const { symbol, timeframe } = req.body;
const result = await proxyService.generateSignal({ symbol, timeframe });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async runMLBacktest(req: Request, res: Response): Promise<void> {
const { symbol, strategy, startDate, endDate } = req.body;
const result = await proxyService.runMLBacktest({
symbol,
strategy,
startDate,
endDate,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getICTAnalysis(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const { timeframe } = req.body;
const result = await proxyService.getICTAnalysis(symbol, { timeframe });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getEnsembleSignal(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const { models } = req.body;
const result = await proxyService.getEnsembleSignal(symbol, { models });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getQuickSignal(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const result = await proxyService.getQuickSignal(symbol);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async scanSymbols(req: Request, res: Response): Promise<void> {
const { symbols, filters } = req.body;
const result = await proxyService.scanSymbols({ symbols, filters });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getMLModels(_req: Request, res: Response): Promise<void> {
const result = await proxyService.getMLModels();
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getMLModelStatus(req: Request, res: Response): Promise<void> {
const { modelId } = req.params;
const result = await proxyService.getMLModelStatus(modelId);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async updateMLModelStatus(req: Request, res: Response): Promise<void> {
const { modelId } = req.params;
const { status } = req.body;
const result = await proxyService.updateMLModelStatus(modelId, status);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getModelAccuracy(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const result = await proxyService.getModelAccuracy(symbol);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getAvailableStrategies(_req: Request, res: Response): Promise<void> {
const result = await proxyService.getAvailableStrategies();
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async compareStrategies(req: Request, res: Response): Promise<void> {
const { symbol, strategies, startDate, endDate } = req.body;
const result = await proxyService.compareStrategies({
symbol,
strategies,
startDate,
endDate,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getHistoricalPredictions(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const { startDate, endDate } = req.query as { startDate?: string; endDate?: string };
const result = await proxyService.getHistoricalPredictions(symbol, {
startDate,
endDate,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
// ========== LLM Agent Endpoints ==========
async analyzeSymbol(req: Request, res: Response): Promise<void> {
const { symbol, timeframe, context } = req.body;
const result = await proxyService.analyzeSymbol({ symbol, timeframe, context });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getLLMActiveSignals(_req: Request, res: Response): Promise<void> {
const result = await proxyService.getLLMActiveSignals();
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getRiskSummary(_req: Request, res: Response): Promise<void> {
const result = await proxyService.getRiskSummary();
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async setRiskLevel(req: Request, res: Response): Promise<void> {
const { level, parameters } = req.body;
const result = await proxyService.setRiskLevel({ level, parameters });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async validateTrade(req: Request, res: Response): Promise<void> {
const { symbol, direction, size, entry, stopLoss, takeProfit } = req.body;
const result = await proxyService.validateTrade({
symbol,
direction,
size,
entry,
stopLoss,
takeProfit,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async startLLMBacktest(req: Request, res: Response): Promise<void> {
const { symbol, strategy, startDate, endDate, parameters } = req.body;
const result = await proxyService.startLLMBacktest({
symbol,
strategy,
startDate,
endDate,
parameters,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async runQuickBacktest(req: Request, res: Response): Promise<void> {
const { symbol } = req.query as { symbol?: string };
const result = await proxyService.runQuickBacktest({ symbol });
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getBacktestStatus(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const result = await proxyService.getBacktestStatus(id);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getBacktestResults(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const result = await proxyService.getBacktestResults(id);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async listBacktests(req: Request, res: Response): Promise<void> {
const { limit, offset } = req.query as { limit?: string; offset?: string };
const result = await proxyService.listBacktests({
limit: limit ? parseInt(limit, 10) : undefined,
offset: offset ? parseInt(offset, 10) : undefined,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async deleteBacktest(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const result = await proxyService.deleteBacktest(id);
if (result.success) {
res.json({ success: true });
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
// ========== Data Service Endpoints ==========
async getHistoricalCandles(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const { timeframe, start, end, limit } = req.query as {
timeframe?: string;
start?: string;
end?: string;
limit?: string;
};
const result = await proxyService.getHistoricalCandles(symbol, {
timeframe,
start,
end,
limit: limit ? parseInt(limit, 10) : undefined,
});
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
async getAvailableDateRange(req: Request, res: Response): Promise<void> {
const { symbol } = req.params;
const result = await proxyService.getAvailableDateRange(symbol);
if (result.success) {
res.json(result.data);
} else {
res.status(result.statusCode).json({ error: result.error });
}
}
// ========== Health Check Endpoints ==========
async getMLHealth(_req: Request, res: Response): Promise<void> {
const health = await proxyService.checkMLHealth();
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
}
async getLLMHealth(_req: Request, res: Response): Promise<void> {
const health = await proxyService.checkLLMHealth();
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
}
async getDataServiceHealth(_req: Request, res: Response): Promise<void> {
const health = await proxyService.checkDataServiceHealth();
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
}
async getAllServicesHealth(_req: Request, res: Response): Promise<void> {
const healthChecks = await proxyService.checkAllServicesHealth();
const allHealthy = healthChecks.every((h) => h.status === 'healthy');
const statusCode = allHealthy ? 200 : 503;
res.status(statusCode).json({
status: allHealthy ? 'healthy' : 'degraded',
services: healthChecks,
});
}
}
export const proxyController = new ProxyController();

View File

@ -0,0 +1,7 @@
// Proxy Module - Gateway to Python Services
// ARCH-001 Fix: Centralized access to ML/LLM/Data services
export { proxyService } from './services/proxy.service';
export { proxyController } from './controllers/proxy.controller';
export { default as proxyRoutes } from './proxy.routes';
export * from './types/proxy.types';

View File

@ -0,0 +1,89 @@
// Proxy Routes - Gateway to Python Services
// ARCH-001 Fix: All Python service access goes through Express with auth
import { Router } from 'express';
import { proxyController } from './controllers/proxy.controller';
import { authenticate } from '../../core/middleware/auth.middleware';
const router = Router();
// All proxy routes require authentication
router.use(authenticate);
// ========== ML Engine Routes ==========
// Base path: /api/v1/proxy/ml
// Signals
router.get('/ml/signals/latest/:symbol', (req, res) => proxyController.getLatestSignal(req, res));
router.get('/ml/signals/active', (req, res) => proxyController.getActiveSignals(req, res));
router.post('/ml/signals/generate', (req, res) => proxyController.generateSignal(req, res));
// AMD Phase Detection
router.get('/ml/amd/:symbol', (req, res) => proxyController.getAMDPhase(req, res));
// Predictions
router.get('/ml/predict/range/:symbol', (req, res) => proxyController.getRangePrediction(req, res));
router.get('/ml/predictions/history/:symbol', (req, res) => proxyController.getHistoricalPredictions(req, res));
// ICT Analysis
router.post('/ml/ict/:symbol', (req, res) => proxyController.getICTAnalysis(req, res));
// Ensemble
router.post('/ml/ensemble/:symbol', (req, res) => proxyController.getEnsembleSignal(req, res));
router.get('/ml/ensemble/quick/:symbol', (req, res) => proxyController.getQuickSignal(req, res));
// Scanner
router.post('/ml/scan', (req, res) => proxyController.scanSymbols(req, res));
// Backtest
router.post('/ml/backtest/run', (req, res) => proxyController.runMLBacktest(req, res));
router.post('/ml/backtest/compare', (req, res) => proxyController.compareStrategies(req, res));
// Models (Admin)
router.get('/ml/models', (req, res) => proxyController.getMLModels(req, res));
router.get('/ml/models/:modelId/status', (req, res) => proxyController.getMLModelStatus(req, res));
router.patch('/ml/models/:modelId/status', (req, res) => proxyController.updateMLModelStatus(req, res));
router.get('/ml/models/accuracy/:symbol', (req, res) => proxyController.getModelAccuracy(req, res));
// Strategies
router.get('/ml/strategies', (req, res) => proxyController.getAvailableStrategies(req, res));
// Health
router.get('/ml/health', (req, res) => proxyController.getMLHealth(req, res));
// ========== LLM Agent Routes ==========
// Base path: /api/v1/proxy/llm
// Analysis
router.post('/llm/analyze', (req, res) => proxyController.analyzeSymbol(req, res));
router.get('/llm/signals/active', (req, res) => proxyController.getLLMActiveSignals(req, res));
// Risk
router.get('/llm/risk/summary', (req, res) => proxyController.getRiskSummary(req, res));
router.post('/llm/risk/level', (req, res) => proxyController.setRiskLevel(req, res));
// Trade Validation
router.post('/llm/validate-trade', (req, res) => proxyController.validateTrade(req, res));
// Backtesting
router.post('/llm/backtest/run', (req, res) => proxyController.startLLMBacktest(req, res));
router.get('/llm/backtest/quick', (req, res) => proxyController.runQuickBacktest(req, res));
router.get('/llm/backtest/status/:id', (req, res) => proxyController.getBacktestStatus(req, res));
router.get('/llm/backtest/results/:id', (req, res) => proxyController.getBacktestResults(req, res));
router.get('/llm/backtest/list', (req, res) => proxyController.listBacktests(req, res));
router.delete('/llm/backtest/:id', (req, res) => proxyController.deleteBacktest(req, res));
// Health
router.get('/llm/health', (req, res) => proxyController.getLLMHealth(req, res));
// ========== Data Service Routes ==========
// Base path: /api/v1/proxy/data
router.get('/data/candles/:symbol', (req, res) => proxyController.getHistoricalCandles(req, res));
router.get('/data/symbols/:symbol/date-range', (req, res) => proxyController.getAvailableDateRange(req, res));
router.get('/data/health', (req, res) => proxyController.getDataServiceHealth(req, res));
// ========== Combined Health Check ==========
router.get('/health', (req, res) => proxyController.getAllServicesHealth(req, res));
export default router;

View File

@ -0,0 +1,444 @@
// Proxy Service - Gateway to Python Services
// ARCH-001 Fix: Centralized proxy with auth, logging, and error handling
import { config } from '../../../config';
import {
ProxyConfig,
ProxyRequestOptions,
ProxyResponse,
ServiceHealth,
} from '../types/proxy.types';
const proxyConfig: ProxyConfig = {
mlEngineUrl: config.mlEngine?.baseUrl || 'http://localhost:3083',
llmAgentUrl: config.llmAgent?.url || 'http://localhost:3085',
dataServiceUrl: config.dataService?.url || 'http://localhost:3084',
timeout: Math.max(
config.mlEngine?.timeout || 5000,
config.llmAgent?.timeout || 30000,
config.dataService?.timeout || 10000
),
};
class ProxyService {
private async makeRequest<T>(
baseUrl: string,
options: ProxyRequestOptions
): Promise<ProxyResponse<T>> {
const startTime = Date.now();
const url = `${baseUrl}${options.path}`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
options.timeout || proxyConfig.timeout
);
const fetchOptions: RequestInit = {
method: options.method,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
signal: controller.signal,
};
if (options.body && ['POST', 'PUT', 'PATCH'].includes(options.method)) {
fetchOptions.body = JSON.stringify(options.body);
}
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
const latency = Date.now() - startTime;
if (!response.ok) {
const errorText = await response.text();
return {
success: false,
error: errorText || `HTTP ${response.status}`,
statusCode: response.status,
latency,
};
}
const data = await response.json();
return {
success: true,
data: data as T,
statusCode: response.status,
latency,
};
} catch (error) {
const latency = Date.now() - startTime;
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
if (errorMessage.includes('abort')) {
return {
success: false,
error: 'Request timeout',
statusCode: 408,
latency,
};
}
return {
success: false,
error: errorMessage,
statusCode: 500,
latency,
};
}
}
// ========== ML Engine Proxy ==========
async mlRequest<T>(options: ProxyRequestOptions): Promise<ProxyResponse<T>> {
return this.makeRequest<T>(proxyConfig.mlEngineUrl, options);
}
async getLatestSignal(symbol: string) {
return this.mlRequest({
method: 'GET',
path: `/api/v1/signals/latest/${symbol}`,
});
}
async getActiveSignals() {
return this.mlRequest({
method: 'GET',
path: '/api/v1/signals/active',
});
}
async getAMDPhase(symbol: string) {
return this.mlRequest({
method: 'GET',
path: `/api/v1/amd/detect/${symbol}`,
});
}
async getRangePrediction(symbol: string) {
return this.mlRequest({
method: 'GET',
path: `/api/v1/predict/range/${symbol}`,
});
}
async generateSignal(params: { symbol: string; timeframe: string }) {
return this.mlRequest({
method: 'POST',
path: '/api/v1/signals/generate',
body: params,
});
}
async runMLBacktest(params: {
symbol: string;
strategy: string;
startDate: string;
endDate: string;
}) {
return this.mlRequest({
method: 'POST',
path: '/api/v1/backtest/run',
body: params,
});
}
async getICTAnalysis(symbol: string, params?: { timeframe?: string }) {
return this.mlRequest({
method: 'POST',
path: `/api/ict/${symbol}`,
body: params || {},
});
}
async getEnsembleSignal(symbol: string, params?: { models?: string[] }) {
return this.mlRequest({
method: 'POST',
path: `/api/ensemble/${symbol}`,
body: params || {},
});
}
async getQuickSignal(symbol: string) {
return this.mlRequest({
method: 'GET',
path: `/api/ensemble/quick/${symbol}`,
});
}
async scanSymbols(params: { symbols: string[]; filters?: Record<string, unknown> }) {
return this.mlRequest({
method: 'POST',
path: '/api/scan',
body: params,
});
}
async getMLModels() {
return this.mlRequest({
method: 'GET',
path: '/models',
});
}
async getMLModelStatus(modelId: string) {
return this.mlRequest({
method: 'GET',
path: `/models/${modelId}/status`,
});
}
async updateMLModelStatus(modelId: string, status: string) {
return this.mlRequest({
method: 'PATCH',
path: `/models/${modelId}/status`,
body: { status },
});
}
async getModelAccuracy(symbol: string) {
return this.mlRequest({
method: 'GET',
path: `/api/models/accuracy/${symbol}`,
});
}
async getAvailableStrategies() {
return this.mlRequest({
method: 'GET',
path: '/api/strategies',
});
}
async compareStrategies(params: {
symbol: string;
strategies: string[];
startDate: string;
endDate: string;
}) {
return this.mlRequest({
method: 'POST',
path: '/api/backtest/compare',
body: params,
});
}
async getHistoricalPredictions(symbol: string, params?: { startDate?: string; endDate?: string }) {
const query = params
? `?${new URLSearchParams(params as Record<string, string>).toString()}`
: '';
return this.mlRequest({
method: 'GET',
path: `/api/predictions/history/${symbol}${query}`,
});
}
// ========== LLM Agent Proxy ==========
async llmRequest<T>(options: ProxyRequestOptions): Promise<ProxyResponse<T>> {
return this.makeRequest<T>(proxyConfig.llmAgentUrl, options);
}
async analyzeSymbol(params: {
symbol: string;
timeframe?: string;
context?: string;
}) {
return this.llmRequest({
method: 'POST',
path: '/api/v1/predictions/analyze',
body: params,
});
}
async getLLMActiveSignals() {
return this.llmRequest({
method: 'GET',
path: '/api/v1/predictions/active-signals',
});
}
async getRiskSummary() {
return this.llmRequest({
method: 'GET',
path: '/api/v1/predictions/risk-summary',
});
}
async setRiskLevel(params: { level: string; parameters?: Record<string, unknown> }) {
return this.llmRequest({
method: 'POST',
path: '/api/v1/predictions/risk-level',
body: params,
});
}
async validateTrade(params: {
symbol: string;
direction: 'long' | 'short';
size: number;
entry: number;
stopLoss: number;
takeProfit: number;
}) {
return this.llmRequest({
method: 'POST',
path: '/api/v1/predictions/validate-trade',
body: params,
});
}
async startLLMBacktest(params: {
symbol: string;
strategy: string;
startDate: string;
endDate: string;
parameters?: Record<string, unknown>;
}) {
return this.llmRequest({
method: 'POST',
path: '/api/v1/backtesting/run',
body: params,
});
}
async runQuickBacktest(params?: { symbol?: string }) {
const query = params?.symbol ? `?symbol=${params.symbol}` : '';
return this.llmRequest({
method: 'GET',
path: `/api/v1/backtesting/quick-test${query}`,
});
}
async getBacktestStatus(id: string) {
return this.llmRequest({
method: 'GET',
path: `/api/v1/backtesting/status/${id}`,
});
}
async getBacktestResults(id: string) {
return this.llmRequest({
method: 'GET',
path: `/api/v1/backtesting/results/${id}`,
});
}
async listBacktests(params?: { limit?: number; offset?: number }) {
const query = params
? `?${new URLSearchParams(params as Record<string, string>).toString()}`
: '';
return this.llmRequest({
method: 'GET',
path: `/api/v1/backtesting/list${query}`,
});
}
async deleteBacktest(id: string) {
return this.llmRequest({
method: 'DELETE',
path: `/api/v1/backtesting/${id}`,
});
}
// ========== Data Service Proxy ==========
async dataRequest<T>(options: ProxyRequestOptions): Promise<ProxyResponse<T>> {
return this.makeRequest<T>(proxyConfig.dataServiceUrl, options);
}
async getHistoricalCandles(
symbol: string,
params?: { timeframe?: string; start?: string; end?: string; limit?: number }
) {
const query = params
? `?${new URLSearchParams(params as Record<string, string>).toString()}`
: '';
return this.dataRequest({
method: 'GET',
path: `/api/v1/candles/${symbol}${query}`,
});
}
async getAvailableDateRange(symbol: string) {
return this.dataRequest({
method: 'GET',
path: `/api/v1/symbols/${symbol}/date-range`,
});
}
// ========== Health Checks ==========
async checkMLHealth(): Promise<ServiceHealth> {
const startTime = Date.now();
try {
const response = await this.mlRequest({ method: 'GET', path: '/health' });
return {
service: 'ml-engine',
status: response.success ? 'healthy' : 'unhealthy',
latency: response.latency,
details: response.data as Record<string, unknown>,
};
} catch {
return {
service: 'ml-engine',
status: 'unhealthy',
latency: Date.now() - startTime,
};
}
}
async checkLLMHealth(): Promise<ServiceHealth> {
const startTime = Date.now();
try {
const response = await this.llmRequest({ method: 'GET', path: '/' });
return {
service: 'llm-agent',
status: response.success ? 'healthy' : 'unhealthy',
latency: response.latency,
details: response.data as Record<string, unknown>,
};
} catch {
return {
service: 'llm-agent',
status: 'unhealthy',
latency: Date.now() - startTime,
};
}
}
async checkDataServiceHealth(): Promise<ServiceHealth> {
const startTime = Date.now();
try {
const response = await this.dataRequest({ method: 'GET', path: '/health' });
return {
service: 'data-service',
status: response.success ? 'healthy' : 'unhealthy',
latency: response.latency,
details: response.data as Record<string, unknown>,
};
} catch {
return {
service: 'data-service',
status: 'unhealthy',
latency: Date.now() - startTime,
};
}
}
async checkAllServicesHealth(): Promise<ServiceHealth[]> {
const [ml, llm, data] = await Promise.all([
this.checkMLHealth(),
this.checkLLMHealth(),
this.checkDataServiceHealth(),
]);
return [ml, llm, data];
}
}
export const proxyService = new ProxyService();

View File

@ -0,0 +1,178 @@
// Proxy Types - Gateway to Python Services
// ARCH-001 Fix: Centralize access to ML/LLM services via Express
export interface ProxyConfig {
mlEngineUrl: string;
llmAgentUrl: string;
dataServiceUrl: string;
timeout: number;
}
export interface ProxyRequestOptions {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
body?: unknown;
headers?: Record<string, string>;
timeout?: number;
}
export interface ProxyResponse<T = unknown> {
success: boolean;
data?: T;
error?: string;
statusCode: number;
latency: number;
}
// ML Engine Types
export interface MLSignal {
id: string;
symbol: string;
direction: 'long' | 'short' | 'neutral';
confidence: number;
entry_price: number;
stop_loss: number;
take_profit: number;
timestamp: string;
model: string;
features?: Record<string, number>;
}
export interface AMDPhase {
symbol: string;
phase: 'accumulation' | 'manipulation' | 'distribution';
confidence: number;
details: {
volume_profile: string;
price_action: string;
order_flow: string;
};
}
export interface RangePrediction {
symbol: string;
timeframe: string;
high_predicted: number;
low_predicted: number;
current_price: number;
confidence: number;
}
export interface ICTAnalysis {
symbol: string;
order_blocks: Array<{
type: 'bullish' | 'bearish';
price_high: number;
price_low: number;
timestamp: string;
strength: number;
}>;
fair_value_gaps: Array<{
type: 'bullish' | 'bearish';
top: number;
bottom: number;
filled_percent: number;
}>;
liquidity_pools: Array<{
type: 'buy_side' | 'sell_side';
price: number;
strength: number;
}>;
}
export interface EnsembleSignal {
symbol: string;
consensus_direction: 'long' | 'short' | 'neutral';
consensus_confidence: number;
models: Array<{
name: string;
direction: 'long' | 'short' | 'neutral';
confidence: number;
weight: number;
}>;
entry_zone: { min: number; max: number };
stop_loss: number;
targets: number[];
}
// LLM Agent Types
export interface LLMAnalysisRequest {
symbol: string;
timeframe?: string;
context?: string;
include_technicals?: boolean;
include_fundamentals?: boolean;
}
export interface LLMAnalysisResponse {
symbol: string;
analysis: string;
sentiment: 'bullish' | 'bearish' | 'neutral';
key_levels: {
support: number[];
resistance: number[];
};
recommendation: string;
confidence: number;
}
export interface BacktestRequest {
symbol: string;
strategy: string;
start_date: string;
end_date: string;
initial_capital?: number;
parameters?: Record<string, unknown>;
}
export interface BacktestResult {
id: string;
symbol: string;
strategy: string;
status: 'pending' | 'running' | 'completed' | 'failed';
metrics?: {
total_return: number;
sharpe_ratio: number;
max_drawdown: number;
win_rate: number;
profit_factor: number;
total_trades: number;
};
equity_curve?: Array<{ date: string; value: number }>;
trades?: Array<{
entry_time: string;
exit_time: string;
direction: 'long' | 'short';
entry_price: number;
exit_price: number;
pnl: number;
pnl_percent: number;
}>;
}
// Data Service Types
export interface CandleData {
timestamp: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
}
export interface HistoricalDataRequest {
symbol: string;
timeframe: string;
start?: string;
end?: string;
limit?: number;
}
// Health Check Types
export interface ServiceHealth {
service: string;
status: 'healthy' | 'degraded' | 'unhealthy';
latency: number;
version?: string;
details?: Record<string, unknown>;
}

View File

@ -0,0 +1,266 @@
/**
* Risk Assessment Controller
* Handles HTTP endpoints for risk questionnaire and assessments
*/
import { Request, Response, NextFunction } from 'express';
import { riskService } from '../services/risk.service';
// ============================================================================
// Types
// ============================================================================
type AuthRequest = Request;
// ============================================================================
// Controllers
// ============================================================================
/**
* GET /api/v1/risk/questions
* Get all risk questionnaire questions
*/
export async function getQuestions(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const questions = riskService.getQuestions();
res.json({
success: true,
data: questions,
});
} catch (error) {
next(error);
}
}
/**
* GET /api/v1/risk/assessment
* Get current user's risk assessment
*/
export async function getCurrentUserAssessment(
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
error: { message: 'Unauthorized', code: 'UNAUTHORIZED' },
});
return;
}
const assessment = await riskService.getUserAssessment(req.user.id);
if (!assessment) {
res.status(404).json({
success: false,
error: {
message: 'No risk assessment found',
code: 'NOT_FOUND',
},
});
return;
}
res.json({
success: true,
data: assessment,
});
} catch (error) {
next(error);
}
}
/**
* GET /api/v1/risk/assessment/valid
* Check if current user has a valid (non-expired) assessment
*/
export async function checkValidAssessment(
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
error: { message: 'Unauthorized', code: 'UNAUTHORIZED' },
});
return;
}
const isValid = await riskService.isAssessmentValid(req.user.id);
const assessment = isValid
? await riskService.getValidAssessment(req.user.id)
: null;
res.json({
success: true,
data: {
isValid,
assessment,
},
});
} catch (error) {
next(error);
}
}
/**
* POST /api/v1/risk/assessment
* Submit risk questionnaire responses
* Body: { responses: [{ questionId, answer }] }
*/
export async function submitAssessment(
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
error: { message: 'Unauthorized', code: 'UNAUTHORIZED' },
});
return;
}
const { responses, completionTimeSeconds } = req.body;
if (!responses || !Array.isArray(responses)) {
res.status(400).json({
success: false,
error: {
message: 'Invalid request: responses array is required',
code: 'INVALID_REQUEST',
},
});
return;
}
const assessment = await riskService.submitAssessment({
userId: req.user.id,
responses,
ipAddress: req.ip,
userAgent: req.get('user-agent'),
completionTimeSeconds,
});
res.status(201).json({
success: true,
data: assessment,
message: `Risk assessment completed. Your profile: ${assessment.riskProfile}`,
});
} catch (error) {
next(error);
}
}
/**
* GET /api/v1/risk/assessment/:userId
* Get risk assessment for specific user (admin only)
*/
export async function getUserAssessment(
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
error: { message: 'Unauthorized', code: 'UNAUTHORIZED' },
});
return;
}
const { userId } = req.params;
if (!userId) {
res.status(400).json({
success: false,
error: {
message: 'User ID is required',
code: 'INVALID_REQUEST',
},
});
return;
}
const assessment = await riskService.getUserAssessment(userId);
if (!assessment) {
res.status(404).json({
success: false,
error: {
message: 'No risk assessment found for this user',
code: 'NOT_FOUND',
},
});
return;
}
res.json({
success: true,
data: assessment,
});
} catch (error) {
next(error);
}
}
/**
* GET /api/v1/risk/assessment/history
* Get assessment history for current user
*/
export async function getAssessmentHistory(
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> {
try {
if (!req.user) {
res.status(401).json({
success: false,
error: { message: 'Unauthorized', code: 'UNAUTHORIZED' },
});
return;
}
const history = await riskService.getAssessmentHistory(req.user.id);
res.json({
success: true,
data: history,
});
} catch (error) {
next(error);
}
}
/**
* GET /api/v1/risk/statistics
* Get risk profile statistics (admin only)
*/
export async function getStatistics(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
try {
const statistics = await riskService.getProfileStatistics();
res.json({
success: true,
data: statistics,
});
} catch (error) {
next(error);
}
}

10
src/modules/risk/index.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* Risk Assessment Module
* Exports for risk questionnaire and profile assessment
*/
export * from './types/risk.types';
export * from './services/risk.service';
export * from './controllers/risk.controller';
export * from './repositories/risk.repository';
export { riskRouter } from './risk.routes';

View File

@ -0,0 +1,272 @@
/**
* Risk Questionnaire Repository
* Handles database operations for risk assessments
*/
import { db } from '../../../shared/database';
import type {
RiskQuestionnaireRow,
RiskAssessment,
RiskProfile,
TradingAgent,
RiskQuestionnaireResponse,
} from '../types/risk.types';
// ============================================================================
// Helper Functions
// ============================================================================
function mapRowToAssessment(row: RiskQuestionnaireRow): RiskAssessment {
const now = new Date();
const isExpired = new Date(row.expires_at) < now;
return {
id: row.id,
userId: row.user_id,
responses: row.responses.map((r) => ({
questionId: r.question_id,
answer: r.answer,
score: r.score,
})),
totalScore: row.total_score,
riskProfile: row.calculated_profile,
recommendedAgent: row.recommended_agent,
completedAt: row.completed_at,
expiresAt: row.expires_at,
isExpired,
ipAddress: row.ip_address,
userAgent: row.user_agent,
completionTimeSeconds: row.completion_time_seconds,
createdAt: row.created_at,
};
}
// ============================================================================
// Repository
// ============================================================================
class RiskRepository {
/**
* Find user's most recent risk assessment
*/
async findByUserId(userId: string): Promise<RiskAssessment | null> {
const query = `
SELECT
id,
user_id,
responses,
total_score,
calculated_profile,
recommended_agent,
completed_at,
expires_at,
ip_address,
user_agent,
completion_time_seconds,
created_at
FROM investment.risk_questionnaire
WHERE user_id = $1
ORDER BY completed_at DESC
LIMIT 1
`;
const result = await db.query<RiskQuestionnaireRow>(query, [userId]);
if (result.rows.length === 0) {
return null;
}
return mapRowToAssessment(result.rows[0]);
}
/**
* Find assessment by ID
*/
async findById(id: string): Promise<RiskAssessment | null> {
const query = `
SELECT
id,
user_id,
responses,
total_score,
calculated_profile,
recommended_agent,
completed_at,
expires_at,
ip_address,
user_agent,
completion_time_seconds,
created_at
FROM investment.risk_questionnaire
WHERE id = $1
`;
const result = await db.query<RiskQuestionnaireRow>(query, [id]);
if (result.rows.length === 0) {
return null;
}
return mapRowToAssessment(result.rows[0]);
}
/**
* Find user's valid (non-expired) assessment
*/
async findValidAssessment(userId: string): Promise<RiskAssessment | null> {
const query = `
SELECT
id,
user_id,
responses,
total_score,
calculated_profile,
recommended_agent,
completed_at,
expires_at,
ip_address,
user_agent,
completion_time_seconds,
created_at
FROM investment.risk_questionnaire
WHERE user_id = $1
AND expires_at > NOW()
ORDER BY completed_at DESC
LIMIT 1
`;
const result = await db.query<RiskQuestionnaireRow>(query, [userId]);
if (result.rows.length === 0) {
return null;
}
return mapRowToAssessment(result.rows[0]);
}
/**
* Create a new risk assessment
*/
async create(input: {
userId: string;
responses: RiskQuestionnaireResponse[];
totalScore: number;
calculatedProfile: RiskProfile;
recommendedAgent: TradingAgent | null;
ipAddress?: string;
userAgent?: string;
completionTimeSeconds?: number;
}): Promise<RiskAssessment> {
const expiresAt = new Date();
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
const responsesJsonb = input.responses.map((r) => ({
question_id: r.questionId,
answer: r.answer,
score: r.score,
}));
const query = `
INSERT INTO investment.risk_questionnaire (
user_id,
responses,
total_score,
calculated_profile,
recommended_agent,
expires_at,
ip_address,
user_agent,
completion_time_seconds
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING
id,
user_id,
responses,
total_score,
calculated_profile,
recommended_agent,
completed_at,
expires_at,
ip_address,
user_agent,
completion_time_seconds,
created_at
`;
const result = await db.query<RiskQuestionnaireRow>(query, [
input.userId,
JSON.stringify(responsesJsonb),
input.totalScore,
input.calculatedProfile,
input.recommendedAgent,
expiresAt,
input.ipAddress || null,
input.userAgent || null,
input.completionTimeSeconds || null,
]);
return mapRowToAssessment(result.rows[0]);
}
/**
* Get all assessments for a user (history)
*/
async findAllByUserId(userId: string): Promise<RiskAssessment[]> {
const query = `
SELECT
id,
user_id,
responses,
total_score,
calculated_profile,
recommended_agent,
completed_at,
expires_at,
ip_address,
user_agent,
completion_time_seconds,
created_at
FROM investment.risk_questionnaire
WHERE user_id = $1
ORDER BY completed_at DESC
`;
const result = await db.query<RiskQuestionnaireRow>(query, [userId]);
return result.rows.map(mapRowToAssessment);
}
/**
* Count assessments by risk profile
*/
async countByProfile(): Promise<Record<RiskProfile, number>> {
const query = `
SELECT
calculated_profile,
COUNT(*) as count
FROM investment.risk_questionnaire
WHERE expires_at > NOW()
GROUP BY calculated_profile
`;
const result = await db.query<{
calculated_profile: RiskProfile;
count: string;
}>(query);
const counts: Record<RiskProfile, number> = {
conservative: 0,
moderate: 0,
aggressive: 0,
};
result.rows.forEach((row) => {
counts[row.calculated_profile] = parseInt(row.count, 10);
});
return counts;
}
}
export const riskRepository = new RiskRepository();

View File

@ -0,0 +1,72 @@
/**
* Risk Assessment Routes
* API endpoints for risk questionnaire and assessments
*/
import { Router, RequestHandler } from 'express';
import * as riskController from './controllers/risk.controller';
import { requireAuth } from '../../core/guards/auth.guard';
const router = Router();
// Type cast helper for authenticated routes
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const authHandler = (fn: Function): RequestHandler => fn as RequestHandler;
// ============================================================================
// Public Routes
// ============================================================================
/**
* GET /api/v1/risk/questions
* Get all risk questionnaire questions
*/
router.get('/questions', riskController.getQuestions);
/**
* GET /api/v1/risk/statistics
* Get risk profile statistics (public aggregate data)
*/
router.get('/statistics', riskController.getStatistics);
// ============================================================================
// Authenticated Routes
// All routes below require authentication via JWT token
// ============================================================================
/**
* GET /api/v1/risk/assessment
* Get current user's most recent risk assessment
*/
router.get('/assessment', requireAuth, authHandler(riskController.getCurrentUserAssessment));
/**
* GET /api/v1/risk/assessment/valid
* Check if current user has a valid (non-expired) assessment
*/
router.get('/assessment/valid', requireAuth, authHandler(riskController.checkValidAssessment));
/**
* GET /api/v1/risk/assessment/history
* Get assessment history for current user
*/
router.get('/assessment/history', requireAuth, authHandler(riskController.getAssessmentHistory));
/**
* POST /api/v1/risk/assessment
* Submit risk questionnaire responses
* Body: {
* responses: [{ questionId: string, answer: string }],
* completionTimeSeconds?: number
* }
*/
router.post('/assessment', requireAuth, authHandler(riskController.submitAssessment));
/**
* GET /api/v1/risk/assessment/:userId
* Get risk assessment for specific user (admin only)
* Note: Should be protected with admin guard in production
*/
router.get('/assessment/:userId', requireAuth, authHandler(riskController.getUserAssessment));
export { router as riskRouter };

View File

@ -0,0 +1,353 @@
/**
* Risk Assessment Service
* Business logic for risk questionnaire and profile calculation
*/
import { riskRepository } from '../repositories/risk.repository';
import type {
RiskQuestion,
RiskAssessment,
SubmitAssessmentInput,
RiskProfile,
TradingAgent,
RiskQuestionnaireResponse,
} from '../types/risk.types';
// ============================================================================
// Risk Assessment Questionnaire (15 questions)
// ============================================================================
const RISK_QUESTIONS: RiskQuestion[] = [
// Experience (Q1-Q3)
{
id: 'Q1',
text: 'How long have you been investing in financial markets?',
category: 'experience',
options: [
{ value: 'none', label: 'I have no investment experience', weight: 0 },
{ value: 'less_1', label: 'Less than 1 year', weight: 2 },
{ value: '1_3', label: '1-3 years', weight: 4 },
{ value: '3_5', label: '3-5 years', weight: 6 },
{ value: 'more_5', label: 'More than 5 years', weight: 8 },
],
},
{
id: 'Q2',
text: 'How familiar are you with investment concepts like diversification, asset allocation, and risk-return tradeoffs?',
category: 'experience',
options: [
{ value: 'not_familiar', label: 'Not familiar at all', weight: 0 },
{ value: 'somewhat', label: 'Somewhat familiar', weight: 3 },
{ value: 'familiar', label: 'Familiar', weight: 5 },
{ value: 'very_familiar', label: 'Very familiar', weight: 7 },
{ value: 'expert', label: 'Expert level', weight: 10 },
],
},
{
id: 'Q3',
text: 'Have you ever traded stocks, forex, or cryptocurrencies?',
category: 'experience',
options: [
{ value: 'never', label: 'Never', weight: 0 },
{ value: 'tried', label: 'Tried a few times', weight: 3 },
{ value: 'occasionally', label: 'Trade occasionally', weight: 5 },
{ value: 'regularly', label: 'Trade regularly', weight: 7 },
{ value: 'professional', label: 'Professional trader', weight: 10 },
],
},
// Investment Goals (Q4-Q6)
{
id: 'Q4',
text: 'What is your primary investment goal?',
category: 'goals',
options: [
{ value: 'preserve', label: 'Preserve capital with minimal growth', weight: 0 },
{ value: 'steady_income', label: 'Generate steady income', weight: 2 },
{ value: 'balanced', label: 'Balanced growth and income', weight: 5 },
{ value: 'growth', label: 'Long-term capital growth', weight: 7 },
{ value: 'aggressive_growth', label: 'Aggressive growth', weight: 10 },
],
},
{
id: 'Q5',
text: 'What percentage of your total savings are you planning to invest?',
category: 'goals',
options: [
{ value: 'less_10', label: 'Less than 10%', weight: 0 },
{ value: '10_25', label: '10-25%', weight: 3 },
{ value: '25_50', label: '25-50%', weight: 5 },
{ value: '50_75', label: '50-75%', weight: 8 },
{ value: 'more_75', label: 'More than 75%', weight: 10 },
],
},
{
id: 'Q6',
text: 'What annual return do you expect from your investments?',
category: 'goals',
options: [
{ value: 'less_5', label: 'Less than 5%', weight: 0 },
{ value: '5_10', label: '5-10%', weight: 3 },
{ value: '10_20', label: '10-20%', weight: 6 },
{ value: '20_30', label: '20-30%', weight: 8 },
{ value: 'more_30', label: 'More than 30%', weight: 10 },
],
},
// Time Horizon (Q7-Q9)
{
id: 'Q7',
text: 'How long do you plan to keep your money invested?',
category: 'timeframe',
options: [
{ value: 'less_1', label: 'Less than 1 year', weight: 0 },
{ value: '1_3', label: '1-3 years', weight: 3 },
{ value: '3_5', label: '3-5 years', weight: 5 },
{ value: '5_10', label: '5-10 years', weight: 7 },
{ value: 'more_10', label: 'More than 10 years', weight: 10 },
],
},
{
id: 'Q8',
text: 'When do you expect to need this money?',
category: 'timeframe',
options: [
{ value: 'immediately', label: 'Within 6 months', weight: 0 },
{ value: 'short_term', label: '6 months to 2 years', weight: 2 },
{ value: 'medium_term', label: '2-5 years', weight: 5 },
{ value: 'long_term', label: '5-10 years', weight: 7 },
{ value: 'retirement', label: 'For retirement (10+ years)', weight: 10 },
],
},
{
id: 'Q9',
text: 'How would you describe your current financial situation?',
category: 'timeframe',
options: [
{ value: 'tight', label: 'I need this money for living expenses', weight: 0 },
{ value: 'stable', label: 'Stable with emergency fund', weight: 3 },
{ value: 'comfortable', label: 'Comfortable with some savings', weight: 5 },
{ value: 'strong', label: 'Strong financial position', weight: 8 },
{ value: 'wealthy', label: 'Wealthy with significant reserves', weight: 10 },
],
},
// Risk Tolerance (Q10-Q12)
{
id: 'Q10',
text: 'How do you feel about investment risk?',
category: 'volatility',
options: [
{ value: 'avoid', label: 'I want to avoid risk entirely', weight: 0 },
{ value: 'minimal', label: 'I can handle minimal risk', weight: 2 },
{ value: 'moderate', label: 'I can handle moderate risk for better returns', weight: 5 },
{ value: 'significant', label: 'I can handle significant risk', weight: 8 },
{ value: 'maximum', label: 'I seek maximum returns regardless of risk', weight: 10 },
],
},
{
id: 'Q11',
text: 'How comfortable are you with your portfolio value fluctuating?',
category: 'volatility',
options: [
{ value: 'very_uncomfortable', label: 'Very uncomfortable with any decline', weight: 0 },
{ value: 'uncomfortable', label: 'Uncomfortable with declines over 5%', weight: 2 },
{ value: 'neutral', label: 'Neutral to declines of 10-15%', weight: 5 },
{ value: 'comfortable', label: 'Comfortable with declines of 20-30%', weight: 8 },
{ value: 'very_comfortable', label: 'Comfortable with declines over 30%', weight: 10 },
],
},
{
id: 'Q12',
text: 'Which investment scenario would you prefer?',
category: 'volatility',
options: [
{ value: 'guaranteed', label: 'Guaranteed 3% return', weight: 0 },
{ value: 'likely_5', label: '75% chance of 5% return, 25% chance of 0%', weight: 3 },
{ value: 'likely_10', label: '50% chance of 10% return, 50% chance of -5%', weight: 6 },
{ value: 'likely_20', label: '40% chance of 20% return, 60% chance of -10%', weight: 8 },
{ value: 'likely_50', label: '25% chance of 50% return, 75% chance of -20%', weight: 10 },
],
},
// Loss Reaction (Q13-Q15)
{
id: 'Q13',
text: 'If your portfolio lost 20% in a month, what would you do?',
category: 'loss_reaction',
options: [
{ value: 'sell_all', label: 'Sell everything immediately', weight: 0 },
{ value: 'sell_some', label: 'Reduce my investment significantly', weight: 2 },
{ value: 'hold', label: 'Hold and wait for recovery', weight: 5 },
{ value: 'buy_little', label: 'Buy a little more at lower prices', weight: 8 },
{ value: 'buy_much', label: 'Buy significantly more (opportunity)', weight: 10 },
],
},
{
id: 'Q14',
text: 'Have you experienced significant investment losses before?',
category: 'loss_reaction',
options: [
{ value: 'no_and_worried', label: 'No, and it worries me greatly', weight: 0 },
{ value: 'no', label: 'No, but I understand it can happen', weight: 3 },
{ value: 'yes_learned', label: 'Yes, and I learned from it', weight: 6 },
{ value: 'yes_comfortable', label: 'Yes, I am comfortable with losses', weight: 8 },
{ value: 'yes_part_of_game', label: 'Yes, losses are part of the game', weight: 10 },
],
},
{
id: 'Q15',
text: 'How would you react to a 30% gain followed by a 15% loss?',
category: 'loss_reaction',
options: [
{ value: 'very_upset', label: 'Very upset about the loss', weight: 0 },
{ value: 'concerned', label: 'Concerned but still positive overall', weight: 3 },
{ value: 'neutral', label: 'Neutral, still up 15% overall', weight: 5 },
{ value: 'positive', label: 'Positive, this is normal volatility', weight: 8 },
{ value: 'opportunity', label: 'Excited about potential opportunities', weight: 10 },
],
},
];
// ============================================================================
// Service
// ============================================================================
class RiskService {
/**
* Get all questionnaire questions
*/
getQuestions(): RiskQuestion[] {
return RISK_QUESTIONS;
}
/**
* Get user's current risk assessment
*/
async getUserAssessment(userId: string): Promise<RiskAssessment | null> {
return await riskRepository.findByUserId(userId);
}
/**
* Get user's valid (non-expired) assessment
*/
async getValidAssessment(userId: string): Promise<RiskAssessment | null> {
return await riskRepository.findValidAssessment(userId);
}
/**
* Check if user has a valid assessment
*/
async isAssessmentValid(userId: string): Promise<boolean> {
const assessment = await riskRepository.findValidAssessment(userId);
return assessment !== null;
}
/**
* Submit risk assessment and calculate profile
*/
async submitAssessment(input: SubmitAssessmentInput): Promise<RiskAssessment> {
const { userId, responses, ipAddress, userAgent, completionTimeSeconds } = input;
if (responses.length !== RISK_QUESTIONS.length) {
throw new Error(
`Invalid number of responses. Expected ${RISK_QUESTIONS.length}, got ${responses.length}`
);
}
const scoredResponses: RiskQuestionnaireResponse[] = [];
for (const response of responses) {
const question = RISK_QUESTIONS.find((q) => q.id === response.questionId);
if (!question) {
throw new Error(`Invalid question ID: ${response.questionId}`);
}
const option = question.options.find((o) => o.value === response.answer);
if (!option) {
throw new Error(
`Invalid answer "${response.answer}" for question ${response.questionId}`
);
}
scoredResponses.push({
questionId: response.questionId,
answer: response.answer,
score: option.weight,
});
}
const calculatedScore = this.calculateRiskScore(scoredResponses);
const riskProfile = this.calculateRiskProfile(calculatedScore);
const recommendedAgent = this.recommendAgent(riskProfile);
const assessment = await riskRepository.create({
userId,
responses: scoredResponses,
totalScore: calculatedScore,
calculatedProfile: riskProfile,
recommendedAgent,
ipAddress,
userAgent,
completionTimeSeconds,
});
return assessment;
}
/**
* Calculate total risk score from responses
*/
calculateRiskScore(responses: RiskQuestionnaireResponse[]): number {
const totalScore = responses.reduce((sum, response) => sum + response.score, 0);
return totalScore;
}
/**
* Calculate risk profile from total score
* Conservative: 0-40
* Moderate: 41-70
* Aggressive: 71-150
*/
calculateRiskProfile(totalScore: number): RiskProfile {
if (totalScore <= 40) {
return 'conservative';
} else if (totalScore <= 70) {
return 'moderate';
} else {
return 'aggressive';
}
}
/**
* Recommend trading agent based on risk profile
*/
recommendAgent(riskProfile: RiskProfile): TradingAgent {
switch (riskProfile) {
case 'conservative':
return 'atlas';
case 'moderate':
return 'orion';
case 'aggressive':
return 'nova';
default:
return 'orion';
}
}
/**
* Get assessment history for a user
*/
async getAssessmentHistory(userId: string): Promise<RiskAssessment[]> {
return await riskRepository.findAllByUserId(userId);
}
/**
* Get statistics on risk profiles
*/
async getProfileStatistics(): Promise<Record<RiskProfile, number>> {
return await riskRepository.countByProfile();
}
}
export const riskService = new RiskService();

View File

@ -0,0 +1,97 @@
/**
* Risk Assessment Types
* Type definitions for risk questionnaire and risk profile assessment
*/
// ============================================================================
// Risk Enums
// ============================================================================
export type RiskProfile = 'conservative' | 'moderate' | 'aggressive';
export enum RiskProfileEnum {
CONSERVATIVE = 'conservative',
MODERATE = 'moderate',
AGGRESSIVE = 'aggressive',
}
export type TradingAgent = 'atlas' | 'orion' | 'nova';
export enum TradingAgentEnum {
ATLAS = 'atlas',
ORION = 'orion',
NOVA = 'nova',
}
// ============================================================================
// Questionnaire Types
// ============================================================================
export interface RiskQuestionnaireResponse {
questionId: string;
answer: string | number;
score: number;
}
export interface RiskAssessment {
id: string;
userId: string;
responses: RiskQuestionnaireResponse[];
totalScore: number;
riskProfile: RiskProfile;
recommendedAgent: TradingAgent | null;
completedAt: Date;
expiresAt: Date;
isExpired: boolean;
ipAddress: string | null;
userAgent: string | null;
completionTimeSeconds: number | null;
createdAt: Date;
}
export interface RiskQuestion {
id: string;
text: string;
category: 'experience' | 'goals' | 'timeframe' | 'volatility' | 'loss_reaction';
options: RiskQuestionOption[];
}
export interface RiskQuestionOption {
value: string;
label: string;
weight: number;
}
export interface SubmitAssessmentInput {
userId: string;
responses: Array<{
questionId: string;
answer: string;
}>;
ipAddress?: string;
userAgent?: string;
completionTimeSeconds?: number;
}
// ============================================================================
// Database Row Types
// ============================================================================
export interface RiskQuestionnaireRow {
id: string;
user_id: string;
responses: {
question_id: string;
answer: string;
score: number;
}[];
total_score: number;
calculated_profile: RiskProfile;
recommended_agent: TradingAgent | null;
completed_at: Date;
expires_at: Date;
ip_address: string | null;
user_agent: string | null;
completion_time_seconds: number | null;
created_at: Date;
}