Skip to content

Commit 2a3768e

Browse files
authored
Merge pull request #393 from nafsonig/feat/maching
Feat/maching
2 parents b9ae92e + 1994d3a commit 2a3768e

14 files changed

Lines changed: 4927 additions & 0 deletions

app/backend/src/anomaly-detection/alerts/anomaly-alert.service.ts

Lines changed: 414 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 399 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,399 @@
1+
/**
2+
* Anomaly Detection REST API Controller
3+
*/
4+
5+
import {
6+
Controller,
7+
Get,
8+
Post,
9+
Put,
10+
Body,
11+
Param,
12+
Query,
13+
HttpCode,
14+
BadRequestException,
15+
NotFoundException,
16+
ConflictException,
17+
InternalServerErrorException,
18+
} from '@nestjs/common';
19+
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
20+
import {
21+
OrderbookSnapshot,
22+
OrderbookEvent,
23+
MarketData,
24+
AnomalyAlert,
25+
AnomalyConfig,
26+
} from './types/order-book.types';
27+
import { AnomalyDetectionCoordinatorService, DetectionReport } from './coordinator/anomaly-detection-coordinator.service';
28+
import { AnomalyAlertService } from './alerts/anomaly-alert.service';
29+
30+
@ApiTags('Anomaly Detection')
31+
@Controller('api/anomaly-detection')
32+
export class AnomalyDetectionController {
33+
constructor(
34+
private coordinator: AnomalyDetectionCoordinatorService,
35+
private alertService: AnomalyAlertService,
36+
) {}
37+
38+
/**
39+
* Ingest orderbook snapshot
40+
*/
41+
@Post('orderbook/snapshot')
42+
@HttpCode(202)
43+
@ApiOperation({ summary: 'Ingest an orderbook snapshot' })
44+
@ApiResponse({
45+
status: 202,
46+
description: 'Snapshot ingested and detection started',
47+
})
48+
@ApiResponse({ status: 400, description: 'Invalid request' })
49+
async ingestSnapshot(@Body() snapshot: OrderbookSnapshot): Promise<DetectionReport> {
50+
if (!snapshot.symbol || !snapshot.bids || !snapshot.asks) {
51+
throw new BadRequestException(
52+
'Invalid snapshot: must include symbol, bids, and asks',
53+
);
54+
}
55+
56+
try {
57+
return await this.coordinator.processOrderbookSnapshot(snapshot);
58+
} catch (error) {
59+
throw new InternalServerErrorException(
60+
`Failed to process snapshot: ${error.message}`,
61+
);
62+
}
63+
}
64+
65+
/**
66+
* Ingest batch orderbook snapshots
67+
*/
68+
@Post('orderbook/snapshot-batch')
69+
@HttpCode(202)
70+
@ApiOperation({ summary: 'Ingest batch of orderbook snapshots' })
71+
@ApiResponse({ status: 202, description: 'Snapshots ingested' })
72+
async ingestSnapshotBatch(
73+
@Body() snapshots: OrderbookSnapshot[],
74+
): Promise<DetectionReport[]> {
75+
if (!Array.isArray(snapshots) || snapshots.length === 0) {
76+
throw new BadRequestException('Must provide array of snapshots');
77+
}
78+
79+
const reports: DetectionReport[] = [];
80+
for (const snapshot of snapshots) {
81+
try {
82+
reports.push(await this.coordinator.processOrderbookSnapshot(snapshot));
83+
} catch (error) {
84+
this.coordinator.processOrderbookSnapshot(snapshot).catch(() => {
85+
// Log and continue
86+
});
87+
}
88+
}
89+
return reports;
90+
}
91+
92+
/**
93+
* Ingest orderbook event
94+
*/
95+
@Post('orderbook/event')
96+
@HttpCode(202)
97+
@ApiOperation({ summary: 'Ingest an orderbook event (order, cancel, etc)' })
98+
@ApiResponse({ status: 202, description: 'Event ingested' })
99+
async ingestEvent(@Body() event: OrderbookEvent): Promise<{ success: boolean }> {
100+
if (!event.symbol || !event.orderId || !event.traderId) {
101+
throw new BadRequestException(
102+
'Invalid event: must include symbol, orderId, and traderId',
103+
);
104+
}
105+
106+
try {
107+
await this.coordinator.processOrderbookEvent(event);
108+
return { success: true };
109+
} catch (error) {
110+
throw new InternalServerErrorException(`Failed to process event: ${error.message}`);
111+
}
112+
}
113+
114+
/**
115+
* Ingest market data
116+
*/
117+
@Post('market-data')
118+
@HttpCode(202)
119+
@ApiOperation({ summary: 'Ingest market data' })
120+
@ApiResponse({ status: 202, description: 'Market data ingested' })
121+
async ingestMarketData(@Body() marketData: MarketData): Promise<{ success: boolean }> {
122+
if (!marketData.symbol) {
123+
throw new BadRequestException('Market data must include symbol');
124+
}
125+
126+
try {
127+
await this.coordinator.processMarketData(marketData);
128+
return { success: true };
129+
} catch (error) {
130+
throw new InternalServerErrorException(
131+
`Failed to process market data: ${error.message}`,
132+
);
133+
}
134+
}
135+
136+
/**
137+
* Get alerts for a symbol
138+
*/
139+
@Get('alerts/symbol/:symbol')
140+
@ApiOperation({ summary: 'Get anomaly alerts for a symbol' })
141+
@ApiParam({ name: 'symbol', type: 'string', description: 'Trading symbol' })
142+
@ApiResponse({
143+
status: 200,
144+
description: 'List of alerts',
145+
type: [AnomalyAlert],
146+
})
147+
getSymbolAlerts(@Param('symbol') symbol: string): AnomalyAlert[] {
148+
return this.coordinator.getSymbolAlerts(symbol);
149+
}
150+
151+
/**
152+
* Get alerts for a trader
153+
*/
154+
@Get('alerts/trader/:traderId')
155+
@ApiOperation({ summary: 'Get anomaly alerts for a trader' })
156+
@ApiParam({ name: 'traderId', type: 'string', description: 'Trader identifier' })
157+
@ApiResponse({
158+
status: 200,
159+
description: 'List of alerts',
160+
type: [AnomalyAlert],
161+
})
162+
getTraderAlerts(@Param('traderId') traderId: string): AnomalyAlert[] {
163+
return this.coordinator.getTraderAlerts(traderId);
164+
}
165+
166+
/**
167+
* Get a specific alert
168+
*/
169+
@Get('alerts/:alertId')
170+
@ApiOperation({ summary: 'Get a specific alert' })
171+
@ApiParam({ name: 'alertId', type: 'string', description: 'Alert ID' })
172+
@ApiResponse({ status: 200, description: 'Alert details' })
173+
@ApiResponse({ status: 404, description: 'Alert not found' })
174+
getAlert(@Param('alertId') alertId: string): AnomalyAlert {
175+
const alert = this.alertService.getAlert(alertId);
176+
if (!alert) {
177+
throw new NotFoundException(`Alert ${alertId} not found`);
178+
}
179+
return alert;
180+
}
181+
182+
/**
183+
* Acknowledge an alert
184+
*/
185+
@Put('alerts/:alertId/acknowledge')
186+
@ApiOperation({ summary: 'Acknowledge an alert' })
187+
@ApiParam({ name: 'alertId', type: 'string' })
188+
@ApiResponse({ status: 200, description: 'Alert acknowledged' })
189+
acknowledgeAlert(
190+
@Param('alertId') alertId: string,
191+
@Query('acknowledgedBy') acknowledgedBy?: string,
192+
): { success: boolean } {
193+
const success = this.alertService.acknowledgeAlert(alertId, acknowledgedBy);
194+
if (!success) {
195+
throw new NotFoundException(`Alert ${alertId} not found`);
196+
}
197+
return { success };
198+
}
199+
200+
/**
201+
* Set alert status to investigating
202+
*/
203+
@Put('alerts/:alertId/investigating')
204+
@ApiOperation({ summary: 'Set alert status to investigating' })
205+
@ApiParam({ name: 'alertId', type: 'string' })
206+
@ApiResponse({ status: 200, description: 'Status updated' })
207+
setInvestigating(@Param('alertId') alertId: string): { success: boolean } {
208+
const success = this.alertService.setAlertInvestigating(alertId);
209+
if (!success) {
210+
throw new NotFoundException(`Alert ${alertId} not found`);
211+
}
212+
return { success };
213+
}
214+
215+
/**
216+
* Resolve an alert
217+
*/
218+
@Put('alerts/:alertId/resolve')
219+
@ApiOperation({ summary: 'Resolve an alert' })
220+
@ApiParam({ name: 'alertId', type: 'string' })
221+
@ApiQuery({
222+
name: 'as',
223+
enum: ['RESOLVED', 'FALSE_POSITIVE'],
224+
description: 'Resolution type',
225+
})
226+
@ApiResponse({ status: 200, description: 'Alert resolved' })
227+
resolveAlert(
228+
@Param('alertId') alertId: string,
229+
@Query('as') resolution: 'RESOLVED' | 'FALSE_POSITIVE' = 'RESOLVED',
230+
): { success: boolean } {
231+
const success = this.alertService.resolveAlert(alertId, resolution);
232+
if (!success) {
233+
throw new NotFoundException(`Alert ${alertId} not found`);
234+
}
235+
return { success };
236+
}
237+
238+
/**
239+
* Execute an action on an alert
240+
*/
241+
@Post('alerts/:alertId/action')
242+
@ApiOperation({ summary: 'Execute an action on an alert' })
243+
@ApiParam({ name: 'alertId', type: 'string' })
244+
@ApiResponse({
245+
status: 200,
246+
description: 'Action executed',
247+
})
248+
executeAction(
249+
@Param('alertId') alertId: string,
250+
@Query('type')
251+
actionType:
252+
| 'THROTTLE'
253+
| 'BLOCK'
254+
| 'AUTO_BAN'
255+
| 'REVIEW'
256+
| 'ESCALATE' = 'THROTTLE',
257+
@Body() parameters?: Record<string, any>,
258+
): { success: boolean; result?: string } {
259+
const action = this.alertService.executeAction(alertId, actionType, parameters);
260+
if (!action) {
261+
throw new NotFoundException(`Alert ${alertId} not found or invalid action`);
262+
}
263+
return { success: action.status === 'EXECUTED', result: action.result };
264+
}
265+
266+
/**
267+
* Get alert statistics
268+
*/
269+
@Get('statistics')
270+
@ApiOperation({ summary: 'Get alert statistics' })
271+
@ApiQuery({ name: 'days', type: 'number', description: 'Period in days' })
272+
@ApiResponse({ status: 200, description: 'Statistics' })
273+
getStatistics(@Query('days') days: number = 7): any {
274+
return this.coordinator.getAlertStatistics(days);
275+
}
276+
277+
/**
278+
* Get anomaly detection configuration
279+
*/
280+
@Get('config')
281+
@ApiOperation({ summary: 'Get current configuration' })
282+
@ApiResponse({ status: 200, description: 'Configuration' })
283+
getConfig(): AnomalyConfig {
284+
return this.coordinator.getConfig();
285+
}
286+
287+
/**
288+
* Update anomaly detection configuration
289+
*/
290+
@Put('config')
291+
@ApiOperation({ summary: 'Update configuration' })
292+
@ApiResponse({ status: 200, description: 'Configuration updated' })
293+
updateConfig(@Body() config: Partial<AnomalyConfig>): { success: boolean } {
294+
this.coordinator.updateConfig(config);
295+
return { success: true };
296+
}
297+
298+
/**
299+
* Check if a trader is throttled
300+
*/
301+
@Get('throttle/:traderId')
302+
@ApiOperation({ summary: 'Check throttle status for a trader' })
303+
@ApiParam({ name: 'traderId', type: 'string' })
304+
@ApiResponse({ status: 200, description: 'Throttle info' })
305+
getThrottleStatus(@Param('traderId') traderId: string): {
306+
isThrottled: boolean;
307+
until?: number;
308+
reason?: string;
309+
} {
310+
const throttleInfo = this.alertService.getThrottleInfo(traderId);
311+
if (throttleInfo) {
312+
return {
313+
isThrottled: true,
314+
until: throttleInfo.until,
315+
reason: throttleInfo.reason,
316+
};
317+
}
318+
return { isThrottled: false };
319+
}
320+
321+
/**
322+
* Export alerts as CSV or JSON
323+
*/
324+
@Get('export')
325+
@ApiOperation({ summary: 'Export alerts' })
326+
@ApiQuery({
327+
name: 'format',
328+
enum: ['json', 'csv'],
329+
default: 'json',
330+
})
331+
@ApiResponse({ status: 200, description: 'Exported data' })
332+
exportAlerts(@Query('format') format: 'json' | 'csv' = 'json'): any {
333+
const data = this.coordinator.exportAuditTrail(format);
334+
if (format === 'csv') {
335+
return { csv: data };
336+
}
337+
return JSON.parse(data);
338+
}
339+
340+
/**
341+
* Run backtest on historical data
342+
*/
343+
@Post('backtest')
344+
@HttpCode(200)
345+
@ApiOperation({ summary: 'Run backtest on historical data' })
346+
@ApiResponse({
347+
status: 200,
348+
description: 'Backtest results',
349+
})
350+
async runBacktest(
351+
@Body()
352+
request: {
353+
symbol: string;
354+
startTime: number;
355+
endTime: number;
356+
snapshots: OrderbookSnapshot[];
357+
events: OrderbookEvent[];
358+
},
359+
): Promise<any> {
360+
if (
361+
!request.symbol ||
362+
typeof request.startTime !== 'number' ||
363+
typeof request.endTime !== 'number'
364+
) {
365+
throw new BadRequestException(
366+
'Invalid request: must include symbol, startTime, endTime',
367+
);
368+
}
369+
370+
if (request.startTime >= request.endTime) {
371+
throw new BadRequestException('startTime must be before endTime');
372+
}
373+
374+
try {
375+
return await this.coordinator.runBacktest(
376+
request.symbol,
377+
request.startTime,
378+
request.endTime,
379+
request.snapshots || [],
380+
request.events || [],
381+
);
382+
} catch (error) {
383+
throw new InternalServerErrorException(`Backtest failed: ${error.message}`);
384+
}
385+
}
386+
387+
/**
388+
* Health check
389+
*/
390+
@Get('health')
391+
@ApiOperation({ summary: 'Health check' })
392+
@ApiResponse({ status: 200, description: 'Service is healthy' })
393+
health(): { status: string; timestamp: number } {
394+
return {
395+
status: 'healthy',
396+
timestamp: Date.now(),
397+
};
398+
}
399+
}

0 commit comments

Comments
 (0)