Migración desde trading-platform/apps/mcp-predictions - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
486bfa1670
27
.env.example
Normal file
27
.env.example
Normal file
@ -0,0 +1,27 @@
|
||||
# MCP PREDICTIONS SERVER CONFIGURATION
|
||||
|
||||
PORT=3094
|
||||
NODE_ENV=development
|
||||
LOG_LEVEL=info
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=trading_platform
|
||||
DB_USER=trading_app
|
||||
DB_PASSWORD=your_password
|
||||
DB_SSL=false
|
||||
DB_POOL_MAX=20
|
||||
|
||||
# Wallet Service
|
||||
WALLET_SERVICE_URL=http://localhost:3090
|
||||
WALLET_SERVICE_TIMEOUT=10000
|
||||
|
||||
# VIP Service
|
||||
VIP_SERVICE_URL=http://localhost:3092
|
||||
VIP_SERVICE_TIMEOUT=10000
|
||||
|
||||
# Predictions Config
|
||||
DEFAULT_PREDICTION_EXPIRY_HOURS=24
|
||||
MAX_PREDICTIONS_PER_PURCHASE=100
|
||||
OUTCOME_VERIFICATION_WINDOW_HOURS=48
|
||||
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S predictions -u 1001
|
||||
COPY --from=builder --chown=predictions:nodejs /app/node_modules ./node_modules
|
||||
COPY --from=builder --chown=predictions:nodejs /app/dist ./dist
|
||||
COPY --from=builder --chown=predictions:nodejs /app/package.json ./
|
||||
USER predictions
|
||||
EXPOSE 3094
|
||||
HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:3094/health || exit 1
|
||||
CMD ["node", "dist/index.js"]
|
||||
100
README.md
Normal file
100
README.md
Normal file
@ -0,0 +1,100 @@
|
||||
# MCP Predictions Server
|
||||
|
||||
ML Predictions Marketplace MCP Server for the Trading Platform. Manages prediction packages, purchases, delivery, and outcome validation.
|
||||
|
||||
## Prediction Types
|
||||
|
||||
| Type | Description | VIP Required |
|
||||
|------|-------------|--------------|
|
||||
| **AMD** | Accumulation/Manipulation/Distribution | - |
|
||||
| **RANGE** | Range detection and breakout | - |
|
||||
| **TPSL** | Take Profit/Stop Loss optimization | Platinum+ |
|
||||
| **ICT_SMC** | ICT/SMC Smart Money Concepts | Platinum+ |
|
||||
| **STRATEGY_ENSEMBLE** | Multi-model ensemble | Diamond |
|
||||
|
||||
## Asset Classes
|
||||
|
||||
- FOREX (e.g., EURUSD, GBPJPY)
|
||||
- CRYPTO (e.g., BTCUSD, ETHUSD)
|
||||
- INDICES (e.g., SPX500, NAS100)
|
||||
- COMMODITIES (e.g., XAUUSD, XTIUSD)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Packages
|
||||
- `POST /api/v1/packages` - Create package (admin)
|
||||
- `GET /api/v1/packages` - List packages
|
||||
- `GET /api/v1/packages/:id` - Get package
|
||||
- `PATCH /api/v1/packages/:id/status` - Update status
|
||||
|
||||
### Purchases
|
||||
- `POST /api/v1/purchases` - Purchase package
|
||||
- `GET /api/v1/purchases/:id` - Get purchase
|
||||
- `GET /api/v1/users/:userId/purchases` - User purchases
|
||||
|
||||
### Predictions
|
||||
- `POST /api/v1/predictions` - Request prediction
|
||||
- `GET /api/v1/predictions` - List predictions
|
||||
- `GET /api/v1/predictions/:id` - Get prediction
|
||||
|
||||
### Outcomes
|
||||
- `POST /api/v1/predictions/:id/outcome` - Record outcome
|
||||
- `GET /api/v1/predictions/:id/outcome` - Get outcome
|
||||
|
||||
### Stats
|
||||
- `GET /api/v1/users/:userId/prediction-stats` - User stats
|
||||
|
||||
## MCP Tools (13)
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `prediction_create_package` | Create package |
|
||||
| `prediction_get_package` | Get package |
|
||||
| `prediction_list_packages` | List packages |
|
||||
| `prediction_update_package_status` | Update status |
|
||||
| `prediction_purchase_package` | Purchase package |
|
||||
| `prediction_get_purchase` | Get purchase |
|
||||
| `prediction_get_user_purchases` | User purchases |
|
||||
| `prediction_request` | Request prediction |
|
||||
| `prediction_get` | Get prediction |
|
||||
| `prediction_list` | List predictions |
|
||||
| `prediction_record_outcome` | Record outcome |
|
||||
| `prediction_get_outcome` | Get outcome |
|
||||
| `prediction_get_user_stats` | User stats |
|
||||
|
||||
## Prediction Status
|
||||
|
||||
- `PENDING` - Awaiting delivery
|
||||
- `DELIVERED` - Prediction delivered
|
||||
- `EXPIRED` - Prediction expired
|
||||
- `VALIDATED` - Outcome verified (win/partial)
|
||||
- `INVALIDATED` - Outcome verified (loss/expired)
|
||||
|
||||
## Outcome Results
|
||||
|
||||
- `WIN` - Prediction was correct
|
||||
- `LOSS` - Prediction was incorrect
|
||||
- `PARTIAL` - Partially correct
|
||||
- `EXPIRED` - Price not reached before expiry
|
||||
- `CANCELLED` - Trade cancelled
|
||||
|
||||
## Features
|
||||
|
||||
- Package-based prediction credits
|
||||
- VIP tier requirements for premium models
|
||||
- 24-hour prediction validity
|
||||
- Outcome tracking and validation
|
||||
- Win rate statistics
|
||||
- Integration with Wallet and VIP services
|
||||
|
||||
## License
|
||||
|
||||
UNLICENSED - Private
|
||||
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@trading-platform/mcp-predictions",
|
||||
"version": "1.0.0",
|
||||
"description": "MCP Server for ML Predictions Marketplace",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"express": "^4.18.2",
|
||||
"pg": "^8.11.3",
|
||||
"zod": "^3.22.4",
|
||||
"winston": "^3.11.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"uuid": "^9.0.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"jsonwebtoken": "^9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.10.0",
|
||||
"@types/pg": "^8.10.9",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/cors": "^2.8.17",
|
||||
"typescript": "^5.3.2",
|
||||
"ts-node-dev": "^2.0.0"
|
||||
},
|
||||
"engines": { "node": ">=18.0.0" },
|
||||
"private": true
|
||||
}
|
||||
76
src/config.ts
Normal file
76
src/config.ts
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* MCP Predictions Server Configuration
|
||||
* ML Predictions marketplace and delivery
|
||||
*/
|
||||
|
||||
import { Pool, PoolConfig } from 'pg';
|
||||
|
||||
// Database configuration
|
||||
const dbConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432', 10),
|
||||
database: process.env.DB_NAME || 'trading_platform',
|
||||
user: process.env.DB_USER || 'trading_app',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
|
||||
max: parseInt(process.env.DB_POOL_MAX || '20', 10),
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
};
|
||||
|
||||
// Database pool singleton
|
||||
let pool: Pool | null = null;
|
||||
|
||||
export function getPool(): Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool(dbConfig);
|
||||
pool.on('error', (err) => {
|
||||
console.error('Unexpected database pool error:', err);
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
export async function closePool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Server configuration
|
||||
export const serverConfig = {
|
||||
port: parseInt(process.env.PORT || '3094', 10),
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
};
|
||||
|
||||
// External services
|
||||
export const walletServiceConfig = {
|
||||
baseUrl: process.env.WALLET_SERVICE_URL || 'http://localhost:3090',
|
||||
timeout: parseInt(process.env.WALLET_SERVICE_TIMEOUT || '10000', 10),
|
||||
};
|
||||
|
||||
export const vipServiceConfig = {
|
||||
baseUrl: process.env.VIP_SERVICE_URL || 'http://localhost:3092',
|
||||
timeout: parseInt(process.env.VIP_SERVICE_TIMEOUT || '10000', 10),
|
||||
};
|
||||
|
||||
// Predictions configuration
|
||||
export const predictionsConfig = {
|
||||
// Default expiry hours for predictions
|
||||
defaultExpiryHours: parseInt(process.env.DEFAULT_PREDICTION_EXPIRY_HOURS || '24', 10),
|
||||
// Maximum predictions per purchase
|
||||
maxPredictionsPerPurchase: parseInt(process.env.MAX_PREDICTIONS_PER_PURCHASE || '100', 10),
|
||||
// Outcome verification window (hours)
|
||||
outcomeVerificationWindowHours: parseInt(process.env.OUTCOME_VERIFICATION_WINDOW_HOURS || '48', 10),
|
||||
// Prediction types
|
||||
predictionTypes: ['AMD', 'RANGE', 'TPSL', 'ICT_SMC', 'STRATEGY_ENSEMBLE'] as const,
|
||||
// Asset classes
|
||||
assetClasses: ['FOREX', 'CRYPTO', 'INDICES', 'COMMODITIES'] as const,
|
||||
};
|
||||
|
||||
// Helper to set tenant context for RLS
|
||||
export async function setTenantContext(client: any, tenantId: string): Promise<void> {
|
||||
await client.query(`SET LOCAL app.current_tenant_id = '${tenantId}'`);
|
||||
}
|
||||
304
src/index.ts
Normal file
304
src/index.ts
Normal file
@ -0,0 +1,304 @@
|
||||
/**
|
||||
* MCP Predictions Server
|
||||
* ML Predictions marketplace and delivery
|
||||
*/
|
||||
|
||||
import 'dotenv/config';
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import { serverConfig, closePool } from './config';
|
||||
import { logger } from './utils/logger';
|
||||
import { allToolSchemas, allToolHandlers, listTools } from './tools';
|
||||
import { authMiddleware, optionalAuthMiddleware, adminMiddleware } from './middleware';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, _res: Response, next: NextFunction) => {
|
||||
logger.debug('Incoming request', {
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/health', (_req: Request, res: Response) => {
|
||||
res.json({ status: 'healthy', service: 'mcp-predictions', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// MCP Endpoints
|
||||
|
||||
// List available tools
|
||||
app.get('/mcp/tools', (_req: Request, res: Response) => {
|
||||
res.json({ tools: listTools() });
|
||||
});
|
||||
|
||||
// Execute tool
|
||||
app.post('/mcp/tools/:toolName', async (req: Request, res: Response) => {
|
||||
const { toolName } = req.params;
|
||||
const handler = allToolHandlers[toolName];
|
||||
|
||||
if (!handler) {
|
||||
res.status(404).json({ success: false, error: `Tool '${toolName}' not found`, code: 'TOOL_NOT_FOUND' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(req.body);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.error('Tool execution error', { toolName, error });
|
||||
res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR' });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// REST API Endpoints - Protected by Auth
|
||||
// ============================================================================
|
||||
|
||||
// Packages - Public list/get, Admin create/update
|
||||
const packagesRouter = express.Router();
|
||||
|
||||
// List packages (optional auth - for personalized data)
|
||||
packagesRouter.get('/', optionalAuthMiddleware, async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_list_packages({
|
||||
tenantId: req.tenantId || req.headers['x-tenant-id'] as string,
|
||||
predictionType: req.query.predictionType,
|
||||
assetClass: req.query.assetClass,
|
||||
activeOnly: req.query.activeOnly !== 'false',
|
||||
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
|
||||
offset: req.query.offset ? parseInt(req.query.offset as string, 10) : undefined,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Get single package (optional auth)
|
||||
packagesRouter.get('/:packageId', optionalAuthMiddleware, async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_get_package({
|
||||
tenantId: req.tenantId || req.headers['x-tenant-id'] as string,
|
||||
packageId: req.params.packageId,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Create package (admin only)
|
||||
packagesRouter.post('/', authMiddleware, adminMiddleware, async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_create_package({
|
||||
...req.body,
|
||||
tenantId: req.tenantId,
|
||||
});
|
||||
res.status(result.success ? 201 : 400).json(result);
|
||||
});
|
||||
|
||||
// Update package status (admin only)
|
||||
packagesRouter.patch('/:packageId/status', authMiddleware, adminMiddleware, async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_update_package_status({
|
||||
tenantId: req.tenantId,
|
||||
packageId: req.params.packageId,
|
||||
isActive: req.body.isActive,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.use('/api/v1/packages', packagesRouter);
|
||||
|
||||
// Purchases - Protected routes
|
||||
const purchasesRouter = express.Router();
|
||||
purchasesRouter.use(authMiddleware);
|
||||
|
||||
// Purchase a package
|
||||
purchasesRouter.post('/', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_purchase_package({
|
||||
...req.body,
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
});
|
||||
res.status(result.success ? 201 : 400).json(result);
|
||||
});
|
||||
|
||||
// Get purchase details
|
||||
purchasesRouter.get('/:purchaseId', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_get_purchase({
|
||||
tenantId: req.tenantId,
|
||||
purchaseId: req.params.purchaseId,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.use('/api/v1/purchases', purchasesRouter);
|
||||
|
||||
// Predictions - Protected routes
|
||||
const predictionsRouter = express.Router();
|
||||
predictionsRouter.use(authMiddleware);
|
||||
|
||||
// Request a prediction
|
||||
predictionsRouter.post('/', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_request({
|
||||
...req.body,
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
});
|
||||
res.status(result.success ? 201 : 400).json(result);
|
||||
});
|
||||
|
||||
// List predictions
|
||||
predictionsRouter.get('/', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_list({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.query.userId || req.userId,
|
||||
purchaseId: req.query.purchaseId,
|
||||
predictionType: req.query.predictionType,
|
||||
assetClass: req.query.assetClass,
|
||||
status: req.query.status,
|
||||
asset: req.query.asset,
|
||||
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
|
||||
offset: req.query.offset ? parseInt(req.query.offset as string, 10) : undefined,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Get prediction details
|
||||
predictionsRouter.get('/:predictionId', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_get({
|
||||
tenantId: req.tenantId,
|
||||
predictionId: req.params.predictionId,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Get prediction outcome
|
||||
predictionsRouter.get('/:predictionId/outcome', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_get_outcome({
|
||||
tenantId: req.tenantId,
|
||||
predictionId: req.params.predictionId,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.use('/api/v1/predictions', predictionsRouter);
|
||||
|
||||
// User endpoints - Protected
|
||||
const userRouter = express.Router();
|
||||
userRouter.use(authMiddleware);
|
||||
|
||||
// Get current user's purchases
|
||||
userRouter.get('/me/purchases', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_get_user_purchases({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
activeOnly: req.query.activeOnly !== 'false',
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Get current user's prediction stats
|
||||
userRouter.get('/me/prediction-stats', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_get_user_stats({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.userId,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Get specific user's purchases (for admins or own data)
|
||||
userRouter.get('/:userId/purchases', async (req: Request, res: Response) => {
|
||||
if (req.params.userId !== req.userId && !req.isOwner) {
|
||||
res.status(403).json({ success: false, error: 'Forbidden', code: 'ACCESS_DENIED' });
|
||||
return;
|
||||
}
|
||||
const result = await allToolHandlers.prediction_get_user_purchases({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.params.userId,
|
||||
activeOnly: req.query.activeOnly !== 'false',
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
// Get specific user's prediction stats
|
||||
userRouter.get('/:userId/prediction-stats', async (req: Request, res: Response) => {
|
||||
if (req.params.userId !== req.userId && !req.isOwner) {
|
||||
res.status(403).json({ success: false, error: 'Forbidden', code: 'ACCESS_DENIED' });
|
||||
return;
|
||||
}
|
||||
const result = await allToolHandlers.prediction_get_user_stats({
|
||||
tenantId: req.tenantId,
|
||||
userId: req.params.userId,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
||||
app.use('/api/v1/users', userRouter);
|
||||
|
||||
// Admin endpoints - Protected + Admin only
|
||||
const adminRouter = express.Router();
|
||||
adminRouter.use(authMiddleware);
|
||||
adminRouter.use(adminMiddleware);
|
||||
|
||||
// Record prediction outcome (admin only)
|
||||
adminRouter.post('/predictions/:predictionId/outcome', async (req: Request, res: Response) => {
|
||||
const result = await allToolHandlers.prediction_record_outcome({
|
||||
...req.body,
|
||||
tenantId: req.tenantId,
|
||||
predictionId: req.params.predictionId,
|
||||
recordedBy: req.userId,
|
||||
});
|
||||
res.status(result.success ? 201 : 400).json(result);
|
||||
});
|
||||
|
||||
app.use('/api/v1/admin', adminRouter);
|
||||
|
||||
// ============================================================================
|
||||
// Error Handling
|
||||
// ============================================================================
|
||||
|
||||
app.use((_req: Request, res: Response) => {
|
||||
res.status(404).json({ success: false, error: 'Not found' });
|
||||
});
|
||||
|
||||
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
logger.error('Unhandled error', { error: err.message, stack: err.stack });
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Server Startup
|
||||
// ============================================================================
|
||||
|
||||
async function shutdown() {
|
||||
logger.info('Shutting down...');
|
||||
await closePool();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGTERM', shutdown);
|
||||
process.on('SIGINT', shutdown);
|
||||
|
||||
app.listen(serverConfig.port, () => {
|
||||
logger.info(`MCP Predictions Server running on port ${serverConfig.port}`, {
|
||||
env: serverConfig.env,
|
||||
tools: Object.keys(allToolSchemas).length,
|
||||
});
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ MCP PREDICTIONS SERVER ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ Port: ${serverConfig.port} ║
|
||||
║ Tools: ${String(Object.keys(allToolSchemas).length).padEnd(12)} ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ /api/v1/packages/* - Prediction packages ║
|
||||
║ /api/v1/purchases/* - Package purchases ║
|
||||
║ /api/v1/predictions/* - Predictions ║
|
||||
║ /api/v1/users/* - User data ║
|
||||
║ /api/v1/admin/* - Admin operations ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
96
src/middleware/auth.middleware.ts
Normal file
96
src/middleware/auth.middleware.ts
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Auth Middleware for MCP Predictions
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production-min-256-bits';
|
||||
|
||||
export interface JWTPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
tenantId: string;
|
||||
isOwner: boolean;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
userId?: string;
|
||||
tenantId?: string;
|
||||
userEmail?: string;
|
||||
isOwner?: boolean;
|
||||
isAuthenticated?: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function authMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
res.status(401).json({ success: false, error: 'Unauthorized', code: 'MISSING_TOKEN' });
|
||||
return;
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
|
||||
req.userId = decoded.sub;
|
||||
req.tenantId = decoded.tenantId;
|
||||
req.userEmail = decoded.email;
|
||||
req.isOwner = decoded.isOwner;
|
||||
req.isAuthenticated = true;
|
||||
req.headers['x-tenant-id'] = decoded.tenantId;
|
||||
req.headers['x-user-id'] = decoded.sub;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
res.status(401).json({ success: false, error: 'Unauthorized', code: 'TOKEN_EXPIRED' });
|
||||
return;
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
res.status(401).json({ success: false, error: 'Unauthorized', code: 'INVALID_TOKEN' });
|
||||
return;
|
||||
}
|
||||
logger.error('Auth middleware error', { error });
|
||||
res.status(500).json({ success: false, error: 'Internal server error', code: 'AUTH_ERROR' });
|
||||
}
|
||||
}
|
||||
|
||||
export function optionalAuthMiddleware(req: Request, _res: Response, next: NextFunction): void {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
req.isAuthenticated = false;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(authHeader.substring(7), JWT_SECRET) as JWTPayload;
|
||||
req.userId = decoded.sub;
|
||||
req.tenantId = decoded.tenantId;
|
||||
req.userEmail = decoded.email;
|
||||
req.isOwner = decoded.isOwner;
|
||||
req.isAuthenticated = true;
|
||||
req.headers['x-tenant-id'] = decoded.tenantId;
|
||||
req.headers['x-user-id'] = decoded.sub;
|
||||
} catch {
|
||||
req.isAuthenticated = false;
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
export function adminMiddleware(req: Request, res: Response, next: NextFunction): void {
|
||||
if (!req.isOwner) {
|
||||
res.status(403).json({ success: false, error: 'Forbidden', code: 'ADMIN_REQUIRED' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
}
|
||||
1
src/middleware/index.ts
Normal file
1
src/middleware/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { authMiddleware, optionalAuthMiddleware, adminMiddleware } from './auth.middleware';
|
||||
820
src/services/prediction.service.ts
Normal file
820
src/services/prediction.service.ts
Normal file
@ -0,0 +1,820 @@
|
||||
/**
|
||||
* Prediction Service
|
||||
* Handles ML prediction packages, purchases, delivery, and outcomes
|
||||
*/
|
||||
|
||||
import { Pool, PoolClient } from 'pg';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import Decimal from 'decimal.js';
|
||||
import {
|
||||
PredictionType,
|
||||
AssetClass,
|
||||
PredictionPackage,
|
||||
PackagePurchase,
|
||||
PurchaseWithPackage,
|
||||
Prediction,
|
||||
PredictionWithOutcome,
|
||||
PredictionOutcome,
|
||||
PredictionStats,
|
||||
CreatePackageInput,
|
||||
PurchasePackageInput,
|
||||
RequestPredictionInput,
|
||||
RecordOutcomeInput,
|
||||
ListPackagesFilter,
|
||||
ListPredictionsFilter,
|
||||
PurchaseStatus,
|
||||
OutcomeResult,
|
||||
} from '../types/prediction.types';
|
||||
import { getPool, setTenantContext, walletServiceConfig, vipServiceConfig, predictionsConfig } from '../config';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
export class PredictionService {
|
||||
private pool: Pool;
|
||||
|
||||
constructor() {
|
||||
this.pool = getPool();
|
||||
}
|
||||
|
||||
// ============ Packages ============
|
||||
|
||||
async createPackage(input: CreatePackageInput): Promise<PredictionPackage> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, input.tenantId);
|
||||
|
||||
const packageId = uuidv4();
|
||||
const result = await client.query<PredictionPackage>(
|
||||
`INSERT INTO ml.prediction_packages (
|
||||
id, tenant_id, name, description, prediction_type, asset_classes,
|
||||
predictions_count, price_credits, validity_days, vip_tier_required, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
RETURNING id, tenant_id as "tenantId", name, description,
|
||||
prediction_type as "predictionType", asset_classes as "assetClasses",
|
||||
predictions_count as "predictionsCount", price_credits as "priceCredits",
|
||||
validity_days as "validityDays", vip_tier_required as "vipTierRequired",
|
||||
is_active as "isActive", metadata,
|
||||
created_at as "createdAt", updated_at as "updatedAt"`,
|
||||
[
|
||||
packageId,
|
||||
input.tenantId,
|
||||
input.name,
|
||||
input.description || null,
|
||||
input.predictionType,
|
||||
input.assetClasses,
|
||||
input.predictionsCount,
|
||||
input.priceCredits,
|
||||
input.validityDays,
|
||||
input.vipTierRequired || null,
|
||||
input.metadata || {},
|
||||
]
|
||||
);
|
||||
|
||||
logger.info('Package created', { packageId, name: input.name });
|
||||
return result.rows[0];
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getPackage(packageId: string, tenantId: string): Promise<PredictionPackage | null> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
const result = await client.query<PredictionPackage>(
|
||||
`SELECT id, tenant_id as "tenantId", name, description,
|
||||
prediction_type as "predictionType", asset_classes as "assetClasses",
|
||||
predictions_count as "predictionsCount", price_credits as "priceCredits",
|
||||
validity_days as "validityDays", vip_tier_required as "vipTierRequired",
|
||||
is_active as "isActive", metadata,
|
||||
created_at as "createdAt", updated_at as "updatedAt"
|
||||
FROM ml.prediction_packages
|
||||
WHERE id = $1 AND tenant_id = $2`,
|
||||
[packageId, tenantId]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async listPackages(filter: ListPackagesFilter): Promise<PredictionPackage[]> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, filter.tenantId);
|
||||
|
||||
const conditions: string[] = ['tenant_id = $1'];
|
||||
const params: unknown[] = [filter.tenantId];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (filter.predictionType) {
|
||||
conditions.push(`prediction_type = $${paramIndex++}`);
|
||||
params.push(filter.predictionType);
|
||||
}
|
||||
if (filter.assetClass) {
|
||||
conditions.push(`$${paramIndex++} = ANY(asset_classes)`);
|
||||
params.push(filter.assetClass);
|
||||
}
|
||||
if (filter.activeOnly !== false) {
|
||||
conditions.push('is_active = true');
|
||||
}
|
||||
|
||||
const limit = filter.limit || 50;
|
||||
const offset = filter.offset || 0;
|
||||
|
||||
const result = await client.query<PredictionPackage>(
|
||||
`SELECT id, tenant_id as "tenantId", name, description,
|
||||
prediction_type as "predictionType", asset_classes as "assetClasses",
|
||||
predictions_count as "predictionsCount", price_credits as "priceCredits",
|
||||
validity_days as "validityDays", vip_tier_required as "vipTierRequired",
|
||||
is_active as "isActive", metadata,
|
||||
created_at as "createdAt", updated_at as "updatedAt"
|
||||
FROM ml.prediction_packages
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY price_credits ASC, name ASC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
|
||||
[...params, limit, offset]
|
||||
);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async updatePackageStatus(packageId: string, tenantId: string, isActive: boolean): Promise<PredictionPackage> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
const result = await client.query<PredictionPackage>(
|
||||
`UPDATE ml.prediction_packages
|
||||
SET is_active = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND tenant_id = $3
|
||||
RETURNING id, tenant_id as "tenantId", name, description,
|
||||
prediction_type as "predictionType", asset_classes as "assetClasses",
|
||||
predictions_count as "predictionsCount", price_credits as "priceCredits",
|
||||
validity_days as "validityDays", vip_tier_required as "vipTierRequired",
|
||||
is_active as "isActive", metadata,
|
||||
created_at as "createdAt", updated_at as "updatedAt"`,
|
||||
[isActive, packageId, tenantId]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
throw new Error('Package not found');
|
||||
}
|
||||
return result.rows[0];
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Purchases ============
|
||||
|
||||
async purchasePackage(input: PurchasePackageInput): Promise<PurchaseWithPackage> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await setTenantContext(client, input.tenantId);
|
||||
|
||||
// Get package
|
||||
const pkg = await this.getPackageInternal(client, input.packageId, input.tenantId);
|
||||
if (!pkg) {
|
||||
throw new Error('Package not found');
|
||||
}
|
||||
if (!pkg.isActive) {
|
||||
throw new Error('Package is not available for purchase');
|
||||
}
|
||||
|
||||
// Check VIP requirement if applicable
|
||||
if (pkg.vipTierRequired) {
|
||||
const hasAccess = await this.checkVipAccess(input.userId, pkg.vipTierRequired, input.tenantId);
|
||||
if (!hasAccess) {
|
||||
throw new Error(`VIP tier ${pkg.vipTierRequired} required for this package`);
|
||||
}
|
||||
}
|
||||
|
||||
// Debit wallet
|
||||
const walletTxId = await this.debitWallet(
|
||||
input.walletId,
|
||||
pkg.priceCredits,
|
||||
input.tenantId,
|
||||
'PREDICTION_PURCHASE',
|
||||
`Purchase: ${pkg.name}`,
|
||||
null
|
||||
);
|
||||
|
||||
// Calculate expiry
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setDate(expiresAt.getDate() + pkg.validityDays);
|
||||
|
||||
// Create purchase
|
||||
const purchaseId = uuidv4();
|
||||
const result = await client.query<PackagePurchase>(
|
||||
`INSERT INTO ml.package_purchases (
|
||||
id, tenant_id, user_id, wallet_id, package_id, status,
|
||||
amount_paid, predictions_remaining, expires_at, wallet_transaction_id, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, 'COMPLETED', $6, $7, $8, $9, $10)
|
||||
RETURNING id, tenant_id as "tenantId", user_id as "userId", wallet_id as "walletId",
|
||||
package_id as "packageId", status, amount_paid as "amountPaid",
|
||||
predictions_remaining as "predictionsRemaining", predictions_used as "predictionsUsed",
|
||||
expires_at as "expiresAt", wallet_transaction_id as "walletTransactionId",
|
||||
metadata, created_at as "createdAt", updated_at as "updatedAt"`,
|
||||
[
|
||||
purchaseId,
|
||||
input.tenantId,
|
||||
input.userId,
|
||||
input.walletId,
|
||||
input.packageId,
|
||||
pkg.priceCredits,
|
||||
pkg.predictionsCount,
|
||||
expiresAt,
|
||||
walletTxId,
|
||||
input.metadata || {},
|
||||
]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
const purchase = result.rows[0];
|
||||
return {
|
||||
...purchase,
|
||||
packageName: pkg.name,
|
||||
predictionType: pkg.predictionType,
|
||||
totalPredictions: pkg.predictionsCount,
|
||||
};
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getPurchase(purchaseId: string, tenantId: string): Promise<PurchaseWithPackage | null> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
const result = await client.query<PurchaseWithPackage>(
|
||||
`SELECT p.id, p.tenant_id as "tenantId", p.user_id as "userId", p.wallet_id as "walletId",
|
||||
p.package_id as "packageId", p.status, p.amount_paid as "amountPaid",
|
||||
p.predictions_remaining as "predictionsRemaining", p.predictions_used as "predictionsUsed",
|
||||
p.expires_at as "expiresAt", p.wallet_transaction_id as "walletTransactionId",
|
||||
p.metadata, p.created_at as "createdAt", p.updated_at as "updatedAt",
|
||||
pkg.name as "packageName", pkg.prediction_type as "predictionType",
|
||||
pkg.predictions_count as "totalPredictions"
|
||||
FROM ml.package_purchases p
|
||||
JOIN ml.prediction_packages pkg ON p.package_id = pkg.id
|
||||
WHERE p.id = $1 AND p.tenant_id = $2`,
|
||||
[purchaseId, tenantId]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getUserPurchases(userId: string, tenantId: string, activeOnly = true): Promise<PurchaseWithPackage[]> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
let condition = 'p.user_id = $1 AND p.tenant_id = $2';
|
||||
if (activeOnly) {
|
||||
condition += " AND p.status = 'COMPLETED' AND p.expires_at > NOW() AND p.predictions_remaining > 0";
|
||||
}
|
||||
|
||||
const result = await client.query<PurchaseWithPackage>(
|
||||
`SELECT p.id, p.tenant_id as "tenantId", p.user_id as "userId", p.wallet_id as "walletId",
|
||||
p.package_id as "packageId", p.status, p.amount_paid as "amountPaid",
|
||||
p.predictions_remaining as "predictionsRemaining", p.predictions_used as "predictionsUsed",
|
||||
p.expires_at as "expiresAt", p.wallet_transaction_id as "walletTransactionId",
|
||||
p.metadata, p.created_at as "createdAt", p.updated_at as "updatedAt",
|
||||
pkg.name as "packageName", pkg.prediction_type as "predictionType",
|
||||
pkg.predictions_count as "totalPredictions"
|
||||
FROM ml.package_purchases p
|
||||
JOIN ml.prediction_packages pkg ON p.package_id = pkg.id
|
||||
WHERE ${condition}
|
||||
ORDER BY p.created_at DESC`,
|
||||
[userId, tenantId]
|
||||
);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Predictions ============
|
||||
|
||||
async requestPrediction(input: RequestPredictionInput): Promise<Prediction> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await setTenantContext(client, input.tenantId);
|
||||
|
||||
// Get purchase and validate
|
||||
const purchase = await this.getPurchaseInternal(client, input.purchaseId, input.tenantId);
|
||||
if (!purchase) {
|
||||
throw new Error('Purchase not found');
|
||||
}
|
||||
if (purchase.userId !== input.userId) {
|
||||
throw new Error('Purchase does not belong to user');
|
||||
}
|
||||
if (purchase.status !== 'COMPLETED') {
|
||||
throw new Error('Purchase is not active');
|
||||
}
|
||||
if (new Date() > purchase.expiresAt) {
|
||||
throw new Error('Purchase has expired');
|
||||
}
|
||||
if (purchase.predictionsRemaining <= 0) {
|
||||
throw new Error('No predictions remaining in this purchase');
|
||||
}
|
||||
|
||||
// Get package to determine prediction type
|
||||
const pkg = await this.getPackageInternal(client, purchase.packageId, input.tenantId);
|
||||
if (!pkg) {
|
||||
throw new Error('Package not found');
|
||||
}
|
||||
|
||||
// Validate asset class
|
||||
if (!pkg.assetClasses.includes(input.assetClass)) {
|
||||
throw new Error(`Asset class ${input.assetClass} not supported by this package`);
|
||||
}
|
||||
|
||||
// Generate prediction (in real system, this would call ML service)
|
||||
const prediction = this.generatePrediction(pkg.predictionType, input);
|
||||
|
||||
// Calculate expiry
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + predictionsConfig.defaultExpiryHours);
|
||||
|
||||
// Create prediction record
|
||||
const predictionId = uuidv4();
|
||||
const result = await client.query<Prediction>(
|
||||
`INSERT INTO ml.predictions (
|
||||
id, tenant_id, purchase_id, user_id, prediction_type, asset, asset_class,
|
||||
timeframe, direction, entry_price, target_price, stop_loss, confidence,
|
||||
status, expires_at, delivered_at, prediction_data, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, 'delivered', $14, NOW(), $15, $16)
|
||||
RETURNING id, tenant_id as "tenantId", purchase_id as "purchaseId", user_id as "userId",
|
||||
prediction_type as "predictionType", asset, asset_class as "assetClass",
|
||||
timeframe, direction, entry_price as "entryPrice", target_price as "targetPrice",
|
||||
stop_loss as "stopLoss", confidence, status, expires_at as "expiresAt",
|
||||
delivered_at as "deliveredAt", prediction_data as "predictionData",
|
||||
metadata, created_at as "createdAt"`,
|
||||
[
|
||||
predictionId,
|
||||
input.tenantId,
|
||||
input.purchaseId,
|
||||
input.userId,
|
||||
pkg.predictionType,
|
||||
input.asset,
|
||||
input.assetClass,
|
||||
input.timeframe,
|
||||
prediction.direction,
|
||||
prediction.entryPrice,
|
||||
prediction.targetPrice,
|
||||
prediction.stopLoss,
|
||||
prediction.confidence,
|
||||
expiresAt,
|
||||
prediction.predictionData,
|
||||
input.metadata || {},
|
||||
]
|
||||
);
|
||||
|
||||
// Decrement remaining predictions
|
||||
await client.query(
|
||||
`UPDATE ml.package_purchases
|
||||
SET predictions_remaining = predictions_remaining - 1,
|
||||
predictions_used = predictions_used + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1 AND tenant_id = $2`,
|
||||
[input.purchaseId, input.tenantId]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
logger.info('Prediction delivered', { predictionId, asset: input.asset, type: pkg.predictionType });
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getPrediction(predictionId: string, tenantId: string): Promise<PredictionWithOutcome | null> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
const predResult = await client.query<Prediction>(
|
||||
`SELECT id, tenant_id as "tenantId", purchase_id as "purchaseId", user_id as "userId",
|
||||
prediction_type as "predictionType", asset, asset_class as "assetClass",
|
||||
timeframe, direction, entry_price as "entryPrice", target_price as "targetPrice",
|
||||
stop_loss as "stopLoss", confidence, status, expires_at as "expiresAt",
|
||||
delivered_at as "deliveredAt", prediction_data as "predictionData",
|
||||
metadata, created_at as "createdAt"
|
||||
FROM ml.predictions
|
||||
WHERE id = $1 AND tenant_id = $2`,
|
||||
[predictionId, tenantId]
|
||||
);
|
||||
|
||||
if (predResult.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prediction = predResult.rows[0];
|
||||
|
||||
// Get outcome if exists
|
||||
const outcomeResult = await client.query<PredictionOutcome>(
|
||||
`SELECT id, tenant_id as "tenantId", prediction_id as "predictionId",
|
||||
result, actual_price as "actualPrice", pnl_percent as "pnlPercent",
|
||||
pnl_absolute as "pnlAbsolute", verified_at as "verifiedAt",
|
||||
verification_source as "verificationSource", notes, metadata,
|
||||
created_at as "createdAt"
|
||||
FROM ml.prediction_outcomes
|
||||
WHERE prediction_id = $1 AND tenant_id = $2`,
|
||||
[predictionId, tenantId]
|
||||
);
|
||||
|
||||
return {
|
||||
...prediction,
|
||||
outcome: outcomeResult.rows[0] || undefined,
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async listPredictions(filter: ListPredictionsFilter): Promise<Prediction[]> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, filter.tenantId);
|
||||
|
||||
const conditions: string[] = ['tenant_id = $1'];
|
||||
const params: unknown[] = [filter.tenantId];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (filter.userId) {
|
||||
conditions.push(`user_id = $${paramIndex++}`);
|
||||
params.push(filter.userId);
|
||||
}
|
||||
if (filter.purchaseId) {
|
||||
conditions.push(`purchase_id = $${paramIndex++}`);
|
||||
params.push(filter.purchaseId);
|
||||
}
|
||||
if (filter.predictionType) {
|
||||
conditions.push(`prediction_type = $${paramIndex++}`);
|
||||
params.push(filter.predictionType);
|
||||
}
|
||||
if (filter.assetClass) {
|
||||
conditions.push(`asset_class = $${paramIndex++}`);
|
||||
params.push(filter.assetClass);
|
||||
}
|
||||
if (filter.status) {
|
||||
conditions.push(`status = $${paramIndex++}`);
|
||||
params.push(filter.status);
|
||||
}
|
||||
if (filter.asset) {
|
||||
conditions.push(`asset = $${paramIndex++}`);
|
||||
params.push(filter.asset);
|
||||
}
|
||||
|
||||
const limit = filter.limit || 50;
|
||||
const offset = filter.offset || 0;
|
||||
|
||||
const result = await client.query<Prediction>(
|
||||
`SELECT id, tenant_id as "tenantId", purchase_id as "purchaseId", user_id as "userId",
|
||||
prediction_type as "predictionType", asset, asset_class as "assetClass",
|
||||
timeframe, direction, entry_price as "entryPrice", target_price as "targetPrice",
|
||||
stop_loss as "stopLoss", confidence, status, expires_at as "expiresAt",
|
||||
delivered_at as "deliveredAt", prediction_data as "predictionData",
|
||||
metadata, created_at as "createdAt"
|
||||
FROM ml.predictions
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
|
||||
[...params, limit, offset]
|
||||
);
|
||||
return result.rows;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Outcomes ============
|
||||
|
||||
async recordOutcome(input: RecordOutcomeInput): Promise<PredictionOutcome> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await setTenantContext(client, input.tenantId);
|
||||
|
||||
// Get prediction
|
||||
const prediction = await this.getPredictionInternal(client, input.predictionId, input.tenantId);
|
||||
if (!prediction) {
|
||||
throw new Error('Prediction not found');
|
||||
}
|
||||
if (prediction.status === 'validated' || prediction.status === 'invalidated') {
|
||||
throw new Error('Outcome already recorded for this prediction');
|
||||
}
|
||||
|
||||
// Create outcome
|
||||
const outcomeId = uuidv4();
|
||||
const result = await client.query<PredictionOutcome>(
|
||||
`INSERT INTO ml.prediction_outcomes (
|
||||
id, tenant_id, prediction_id, result, actual_price, pnl_percent,
|
||||
pnl_absolute, verified_at, verification_source, notes, metadata
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10)
|
||||
RETURNING id, tenant_id as "tenantId", prediction_id as "predictionId",
|
||||
result, actual_price as "actualPrice", pnl_percent as "pnlPercent",
|
||||
pnl_absolute as "pnlAbsolute", verified_at as "verifiedAt",
|
||||
verification_source as "verificationSource", notes, metadata,
|
||||
created_at as "createdAt"`,
|
||||
[
|
||||
outcomeId,
|
||||
input.tenantId,
|
||||
input.predictionId,
|
||||
input.result,
|
||||
input.actualPrice || null,
|
||||
input.pnlPercent || null,
|
||||
input.pnlAbsolute || null,
|
||||
input.verificationSource,
|
||||
input.notes || null,
|
||||
{},
|
||||
]
|
||||
);
|
||||
|
||||
// Update prediction status
|
||||
const newStatus = input.result === 'win' || input.result === 'partial' ? 'validated' : 'invalidated';
|
||||
await client.query(
|
||||
`UPDATE ml.predictions SET status = $1, updated_at = NOW() WHERE id = $2 AND tenant_id = $3`,
|
||||
[newStatus, input.predictionId, input.tenantId]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
logger.info('Outcome recorded', { predictionId: input.predictionId, result: input.result });
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
async getOutcome(predictionId: string, tenantId: string): Promise<PredictionOutcome | null> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
const result = await client.query<PredictionOutcome>(
|
||||
`SELECT id, tenant_id as "tenantId", prediction_id as "predictionId",
|
||||
result, actual_price as "actualPrice", pnl_percent as "pnlPercent",
|
||||
pnl_absolute as "pnlAbsolute", verified_at as "verifiedAt",
|
||||
verification_source as "verificationSource", notes, metadata,
|
||||
created_at as "createdAt"
|
||||
FROM ml.prediction_outcomes
|
||||
WHERE prediction_id = $1 AND tenant_id = $2`,
|
||||
[predictionId, tenantId]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Statistics ============
|
||||
|
||||
async getUserStats(userId: string, tenantId: string): Promise<PredictionStats> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await setTenantContext(client, tenantId);
|
||||
|
||||
// Overall stats
|
||||
const overallResult = await client.query(
|
||||
`SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE o.result = 'win') as wins,
|
||||
COUNT(*) FILTER (WHERE o.result = 'loss') as losses,
|
||||
COUNT(*) FILTER (WHERE o.result = 'partial') as partials,
|
||||
COUNT(*) FILTER (WHERE o.result = 'expired') as expired,
|
||||
AVG(o.pnl_percent) FILTER (WHERE o.pnl_percent IS NOT NULL) as avg_pnl
|
||||
FROM ml.predictions p
|
||||
LEFT JOIN ml.prediction_outcomes o ON p.id = o.prediction_id
|
||||
WHERE p.user_id = $1 AND p.tenant_id = $2`,
|
||||
[userId, tenantId]
|
||||
);
|
||||
|
||||
const overall = overallResult.rows[0];
|
||||
const total = parseInt(overall.total, 10);
|
||||
const wins = parseInt(overall.wins || '0', 10);
|
||||
|
||||
// By prediction type
|
||||
const byTypeResult = await client.query(
|
||||
`SELECT
|
||||
p.prediction_type,
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE o.result = 'win') as wins
|
||||
FROM ml.predictions p
|
||||
LEFT JOIN ml.prediction_outcomes o ON p.id = o.prediction_id
|
||||
WHERE p.user_id = $1 AND p.tenant_id = $2
|
||||
GROUP BY p.prediction_type`,
|
||||
[userId, tenantId]
|
||||
);
|
||||
|
||||
// By asset class
|
||||
const byAssetResult = await client.query(
|
||||
`SELECT
|
||||
p.asset_class,
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE o.result = 'win') as wins
|
||||
FROM ml.predictions p
|
||||
LEFT JOIN ml.prediction_outcomes o ON p.id = o.prediction_id
|
||||
WHERE p.user_id = $1 AND p.tenant_id = $2
|
||||
GROUP BY p.asset_class`,
|
||||
[userId, tenantId]
|
||||
);
|
||||
|
||||
return {
|
||||
totalPredictions: total,
|
||||
winCount: wins,
|
||||
lossCount: parseInt(overall.losses || '0', 10),
|
||||
partialCount: parseInt(overall.partials || '0', 10),
|
||||
expiredCount: parseInt(overall.expired || '0', 10),
|
||||
winRate: total > 0 ? new Decimal(wins).dividedBy(total).times(100).toDecimalPlaces(2).toNumber() : 0,
|
||||
averagePnlPercent: parseFloat(overall.avg_pnl) || 0,
|
||||
byType: byTypeResult.rows.map((row) => ({
|
||||
predictionType: row.prediction_type as PredictionType,
|
||||
total: parseInt(row.total, 10),
|
||||
wins: parseInt(row.wins || '0', 10),
|
||||
winRate:
|
||||
parseInt(row.total, 10) > 0
|
||||
? new Decimal(parseInt(row.wins || '0', 10))
|
||||
.dividedBy(parseInt(row.total, 10))
|
||||
.times(100)
|
||||
.toDecimalPlaces(2)
|
||||
.toNumber()
|
||||
: 0,
|
||||
})),
|
||||
byAssetClass: byAssetResult.rows.map((row) => ({
|
||||
assetClass: row.asset_class as AssetClass,
|
||||
total: parseInt(row.total, 10),
|
||||
wins: parseInt(row.wins || '0', 10),
|
||||
winRate:
|
||||
parseInt(row.total, 10) > 0
|
||||
? new Decimal(parseInt(row.wins || '0', 10))
|
||||
.dividedBy(parseInt(row.total, 10))
|
||||
.times(100)
|
||||
.toDecimalPlaces(2)
|
||||
.toNumber()
|
||||
: 0,
|
||||
})),
|
||||
};
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Private Helpers ============
|
||||
|
||||
private async getPackageInternal(client: PoolClient, packageId: string, tenantId: string): Promise<PredictionPackage | null> {
|
||||
const result = await client.query<PredictionPackage>(
|
||||
`SELECT id, tenant_id as "tenantId", name, description,
|
||||
prediction_type as "predictionType", asset_classes as "assetClasses",
|
||||
predictions_count as "predictionsCount", price_credits as "priceCredits",
|
||||
validity_days as "validityDays", vip_tier_required as "vipTierRequired",
|
||||
is_active as "isActive", metadata,
|
||||
created_at as "createdAt", updated_at as "updatedAt"
|
||||
FROM ml.prediction_packages
|
||||
WHERE id = $1 AND tenant_id = $2`,
|
||||
[packageId, tenantId]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
private async getPurchaseInternal(client: PoolClient, purchaseId: string, tenantId: string): Promise<PackagePurchase | null> {
|
||||
const result = await client.query<PackagePurchase>(
|
||||
`SELECT id, tenant_id as "tenantId", user_id as "userId", wallet_id as "walletId",
|
||||
package_id as "packageId", status, amount_paid as "amountPaid",
|
||||
predictions_remaining as "predictionsRemaining", predictions_used as "predictionsUsed",
|
||||
expires_at as "expiresAt", wallet_transaction_id as "walletTransactionId",
|
||||
metadata, created_at as "createdAt", updated_at as "updatedAt"
|
||||
FROM ml.package_purchases
|
||||
WHERE id = $1 AND tenant_id = $2`,
|
||||
[purchaseId, tenantId]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
private async getPredictionInternal(client: PoolClient, predictionId: string, tenantId: string): Promise<Prediction | null> {
|
||||
const result = await client.query<Prediction>(
|
||||
`SELECT id, tenant_id as "tenantId", purchase_id as "purchaseId", user_id as "userId",
|
||||
prediction_type as "predictionType", asset, asset_class as "assetClass",
|
||||
timeframe, direction, entry_price as "entryPrice", target_price as "targetPrice",
|
||||
stop_loss as "stopLoss", confidence, status, expires_at as "expiresAt",
|
||||
delivered_at as "deliveredAt", prediction_data as "predictionData",
|
||||
metadata, created_at as "createdAt"
|
||||
FROM ml.predictions
|
||||
WHERE id = $1 AND tenant_id = $2`,
|
||||
[predictionId, tenantId]
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
}
|
||||
|
||||
private generatePrediction(type: PredictionType, input: RequestPredictionInput) {
|
||||
// Mock prediction generation - in real system, this calls ML service
|
||||
const directions: Array<'LONG' | 'SHORT' | 'NEUTRAL'> = ['LONG', 'SHORT', 'NEUTRAL'];
|
||||
const direction = directions[Math.floor(Math.random() * 2)]; // Exclude NEUTRAL for simplicity
|
||||
const confidence = Math.round(65 + Math.random() * 30); // 65-95%
|
||||
const basePrice = 100 + Math.random() * 1000;
|
||||
const variation = basePrice * 0.02;
|
||||
|
||||
return {
|
||||
direction,
|
||||
confidence,
|
||||
entryPrice: basePrice,
|
||||
targetPrice: direction === 'LONG' ? basePrice + variation : basePrice - variation,
|
||||
stopLoss: direction === 'LONG' ? basePrice - variation * 0.5 : basePrice + variation * 0.5,
|
||||
predictionData: {
|
||||
type,
|
||||
generatedAt: new Date().toISOString(),
|
||||
model: `${type}_v1.0`,
|
||||
inputs: {
|
||||
asset: input.asset,
|
||||
timeframe: input.timeframe,
|
||||
assetClass: input.assetClass,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async debitWallet(
|
||||
walletId: string,
|
||||
amount: number,
|
||||
tenantId: string,
|
||||
type: string,
|
||||
description: string,
|
||||
referenceId: string | null
|
||||
): Promise<string> {
|
||||
const response = await fetch(`${walletServiceConfig.baseUrl}/api/v1/wallets/${walletId}/debit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-Id': tenantId,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tenantId,
|
||||
amount,
|
||||
type,
|
||||
description,
|
||||
referenceType: 'PREDICTION_PURCHASE',
|
||||
referenceId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.message || 'Failed to debit wallet');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result.data.transactionId;
|
||||
}
|
||||
|
||||
private async checkVipAccess(userId: string, requiredTier: string, tenantId: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${vipServiceConfig.baseUrl}/api/v1/users/${userId}/subscription`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Tenant-Id': tenantId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success || !result.data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tierHierarchy = ['GOLD', 'PLATINUM', 'DIAMOND'];
|
||||
const userTierIndex = tierHierarchy.indexOf(result.data.tier);
|
||||
const requiredTierIndex = tierHierarchy.indexOf(requiredTier.toUpperCase());
|
||||
|
||||
return userTierIndex >= requiredTierIndex;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let predictionServiceInstance: PredictionService | null = null;
|
||||
|
||||
export function getPredictionService(): PredictionService {
|
||||
if (!predictionServiceInstance) {
|
||||
predictionServiceInstance = new PredictionService();
|
||||
}
|
||||
return predictionServiceInstance;
|
||||
}
|
||||
31
src/tools/index.ts
Normal file
31
src/tools/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Tool Registry Index
|
||||
* Exports all prediction tools and handlers
|
||||
*/
|
||||
|
||||
import { predictionToolSchemas, toolHandlers } from './prediction';
|
||||
|
||||
// Export all tool schemas
|
||||
export const allToolSchemas = {
|
||||
...predictionToolSchemas,
|
||||
};
|
||||
|
||||
// Export all handlers
|
||||
export const allToolHandlers = {
|
||||
...toolHandlers,
|
||||
};
|
||||
|
||||
// Get tool by name
|
||||
export function getToolSchema(name: string) {
|
||||
return (allToolSchemas as Record<string, unknown>)[name];
|
||||
}
|
||||
|
||||
// Get handler by name
|
||||
export function getToolHandler(name: string) {
|
||||
return allToolHandlers[name];
|
||||
}
|
||||
|
||||
// List all available tools
|
||||
export function listTools() {
|
||||
return Object.values(allToolSchemas);
|
||||
}
|
||||
510
src/tools/prediction.ts
Normal file
510
src/tools/prediction.ts
Normal file
@ -0,0 +1,510 @@
|
||||
/**
|
||||
* Prediction MCP Tools
|
||||
* ML Predictions marketplace tools
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { getPredictionService } from '../services/prediction.service';
|
||||
import { McpResponse, PredictionType, AssetClass, PredictionStatus, OutcomeResult } from '../types/prediction.types';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Validation schemas (aligned with DDL enums)
|
||||
const PredictionTypeSchema = z.enum(['AMD', 'RANGE', 'TPSL', 'ICT_SMC', 'STRATEGY_ENSEMBLE']);
|
||||
const AssetClassSchema = z.enum(['FOREX', 'CRYPTO', 'INDICES', 'COMMODITIES']);
|
||||
const PredictionStatusSchema = z.enum(['pending', 'delivered', 'expired', 'validated', 'invalidated']);
|
||||
const OutcomeResultSchema = z.enum(['pending', 'win', 'loss', 'partial', 'expired', 'cancelled']);
|
||||
|
||||
const CreatePackageSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
predictionType: PredictionTypeSchema,
|
||||
assetClasses: z.array(AssetClassSchema).min(1),
|
||||
predictionsCount: z.number().int().positive().max(1000),
|
||||
priceCredits: z.number().positive(),
|
||||
validityDays: z.number().int().positive().max(365),
|
||||
vipTierRequired: z.string().optional(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const PurchasePackageSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
walletId: z.string().uuid(),
|
||||
packageId: z.string().uuid(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const RequestPredictionSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
purchaseId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
asset: z.string().min(1).max(20),
|
||||
assetClass: AssetClassSchema,
|
||||
timeframe: z.string().min(1).max(10),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
});
|
||||
|
||||
const RecordOutcomeSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
predictionId: z.string().uuid(),
|
||||
result: OutcomeResultSchema,
|
||||
actualPrice: z.number().optional(),
|
||||
pnlPercent: z.number().optional(),
|
||||
pnlAbsolute: z.number().optional(),
|
||||
verificationSource: z.string().min(1).max(100),
|
||||
notes: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
const ListPackagesSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
predictionType: PredictionTypeSchema.optional(),
|
||||
assetClass: AssetClassSchema.optional(),
|
||||
activeOnly: z.boolean().optional(),
|
||||
limit: z.number().int().positive().max(100).optional(),
|
||||
offset: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
const ListPredictionsSchema = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid().optional(),
|
||||
purchaseId: z.string().uuid().optional(),
|
||||
predictionType: PredictionTypeSchema.optional(),
|
||||
assetClass: AssetClassSchema.optional(),
|
||||
status: PredictionStatusSchema.optional(),
|
||||
asset: z.string().optional(),
|
||||
limit: z.number().int().positive().max(100).optional(),
|
||||
offset: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
|
||||
// Tool schemas for MCP
|
||||
export const predictionToolSchemas = {
|
||||
// Packages
|
||||
prediction_create_package: {
|
||||
name: 'prediction_create_package',
|
||||
description: 'Create a new prediction package (admin operation)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
name: { type: 'string', description: 'Package name' },
|
||||
description: { type: 'string', description: 'Package description' },
|
||||
predictionType: { type: 'string', enum: ['AMD', 'RANGE', 'TPSL', 'ICT_SMC', 'STRATEGY_ENSEMBLE'] },
|
||||
assetClasses: { type: 'array', items: { type: 'string', enum: ['FOREX', 'CRYPTO', 'INDICES', 'COMMODITIES'] } },
|
||||
predictionsCount: { type: 'integer', minimum: 1, description: 'Number of predictions in package' },
|
||||
priceCredits: { type: 'number', minimum: 0, description: 'Price in credits' },
|
||||
validityDays: { type: 'integer', minimum: 1, description: 'Validity period in days' },
|
||||
vipTierRequired: { type: 'string', description: 'Required VIP tier (GOLD, PLATINUM, DIAMOND)' },
|
||||
},
|
||||
required: ['tenantId', 'name', 'predictionType', 'assetClasses', 'predictionsCount', 'priceCredits', 'validityDays'],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
|
||||
prediction_get_package: {
|
||||
name: 'prediction_get_package',
|
||||
description: 'Get prediction package details',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
packageId: { type: 'string', format: 'uuid', description: 'Package ID' },
|
||||
},
|
||||
required: ['tenantId', 'packageId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
prediction_list_packages: {
|
||||
name: 'prediction_list_packages',
|
||||
description: 'List available prediction packages',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
predictionType: { type: 'string', enum: ['AMD', 'RANGE', 'TPSL', 'ICT_SMC', 'STRATEGY_ENSEMBLE'] },
|
||||
assetClass: { type: 'string', enum: ['FOREX', 'CRYPTO', 'INDICES', 'COMMODITIES'] },
|
||||
activeOnly: { type: 'boolean', default: true },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
||||
offset: { type: 'integer', minimum: 0, default: 0 },
|
||||
},
|
||||
required: ['tenantId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
prediction_update_package_status: {
|
||||
name: 'prediction_update_package_status',
|
||||
description: 'Activate or deactivate a prediction package',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
packageId: { type: 'string', format: 'uuid', description: 'Package ID' },
|
||||
isActive: { type: 'boolean', description: 'Active status' },
|
||||
},
|
||||
required: ['tenantId', 'packageId', 'isActive'],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
|
||||
// Purchases
|
||||
prediction_purchase_package: {
|
||||
name: 'prediction_purchase_package',
|
||||
description: 'Purchase a prediction package. Debits wallet and grants prediction credits.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
userId: { type: 'string', format: 'uuid', description: 'User ID' },
|
||||
walletId: { type: 'string', format: 'uuid', description: 'Wallet to debit' },
|
||||
packageId: { type: 'string', format: 'uuid', description: 'Package to purchase' },
|
||||
},
|
||||
required: ['tenantId', 'userId', 'walletId', 'packageId'],
|
||||
},
|
||||
riskLevel: 'HIGH',
|
||||
},
|
||||
|
||||
prediction_get_purchase: {
|
||||
name: 'prediction_get_purchase',
|
||||
description: 'Get purchase details',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
purchaseId: { type: 'string', format: 'uuid', description: 'Purchase ID' },
|
||||
},
|
||||
required: ['tenantId', 'purchaseId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
prediction_get_user_purchases: {
|
||||
name: 'prediction_get_user_purchases',
|
||||
description: 'Get user purchase history',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
userId: { type: 'string', format: 'uuid', description: 'User ID' },
|
||||
activeOnly: { type: 'boolean', default: true, description: 'Only show active purchases' },
|
||||
},
|
||||
required: ['tenantId', 'userId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
// Predictions
|
||||
prediction_request: {
|
||||
name: 'prediction_request',
|
||||
description: 'Request a prediction for a specific asset. Uses one prediction credit from purchase.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
purchaseId: { type: 'string', format: 'uuid', description: 'Purchase ID to use' },
|
||||
userId: { type: 'string', format: 'uuid', description: 'User ID' },
|
||||
asset: { type: 'string', description: 'Asset symbol (e.g., EURUSD, BTCUSD)' },
|
||||
assetClass: { type: 'string', enum: ['FOREX', 'CRYPTO', 'INDICES', 'COMMODITIES'] },
|
||||
timeframe: { type: 'string', description: 'Timeframe (e.g., 1H, 4H, 1D)' },
|
||||
},
|
||||
required: ['tenantId', 'purchaseId', 'userId', 'asset', 'assetClass', 'timeframe'],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
|
||||
prediction_get: {
|
||||
name: 'prediction_get',
|
||||
description: 'Get prediction details with outcome if available',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
predictionId: { type: 'string', format: 'uuid', description: 'Prediction ID' },
|
||||
},
|
||||
required: ['tenantId', 'predictionId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
prediction_list: {
|
||||
name: 'prediction_list',
|
||||
description: 'List predictions with filters',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
userId: { type: 'string', format: 'uuid', description: 'Filter by user' },
|
||||
purchaseId: { type: 'string', format: 'uuid', description: 'Filter by purchase' },
|
||||
predictionType: { type: 'string', enum: ['AMD', 'RANGE', 'TPSL', 'ICT_SMC', 'STRATEGY_ENSEMBLE'] },
|
||||
assetClass: { type: 'string', enum: ['FOREX', 'CRYPTO', 'INDICES', 'COMMODITIES'] },
|
||||
status: { type: 'string', enum: ['pending', 'delivered', 'expired', 'validated', 'invalidated'] },
|
||||
asset: { type: 'string', description: 'Filter by asset' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
|
||||
offset: { type: 'integer', minimum: 0, default: 0 },
|
||||
},
|
||||
required: ['tenantId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
// Outcomes
|
||||
prediction_record_outcome: {
|
||||
name: 'prediction_record_outcome',
|
||||
description: 'Record the outcome of a prediction for validation',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
predictionId: { type: 'string', format: 'uuid', description: 'Prediction ID' },
|
||||
result: { type: 'string', enum: ['pending', 'win', 'loss', 'partial', 'expired', 'cancelled'] },
|
||||
actualPrice: { type: 'number', description: 'Actual price at verification' },
|
||||
pnlPercent: { type: 'number', description: 'PnL percentage' },
|
||||
pnlAbsolute: { type: 'number', description: 'PnL in absolute terms' },
|
||||
verificationSource: { type: 'string', description: 'Source of verification' },
|
||||
notes: { type: 'string', description: 'Additional notes' },
|
||||
},
|
||||
required: ['tenantId', 'predictionId', 'result', 'verificationSource'],
|
||||
},
|
||||
riskLevel: 'MEDIUM',
|
||||
},
|
||||
|
||||
prediction_get_outcome: {
|
||||
name: 'prediction_get_outcome',
|
||||
description: 'Get outcome for a prediction',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
predictionId: { type: 'string', format: 'uuid', description: 'Prediction ID' },
|
||||
},
|
||||
required: ['tenantId', 'predictionId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
|
||||
// Statistics
|
||||
prediction_get_user_stats: {
|
||||
name: 'prediction_get_user_stats',
|
||||
description: 'Get prediction statistics for a user',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
|
||||
userId: { type: 'string', format: 'uuid', description: 'User ID' },
|
||||
},
|
||||
required: ['tenantId', 'userId'],
|
||||
},
|
||||
riskLevel: 'LOW',
|
||||
},
|
||||
};
|
||||
|
||||
// Tool handlers
|
||||
async function handleCreatePackage(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const input = CreatePackageSchema.parse(params);
|
||||
const service = getPredictionService();
|
||||
const pkg = await service.createPackage(input);
|
||||
return { success: true, data: pkg };
|
||||
} catch (error) {
|
||||
logger.error('Error creating package', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'CREATE_PACKAGE_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetPackage(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, packageId } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
packageId: z.string().uuid(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const pkg = await service.getPackage(packageId, tenantId);
|
||||
if (!pkg) {
|
||||
return { success: false, error: 'Package not found', code: 'PACKAGE_NOT_FOUND' };
|
||||
}
|
||||
return { success: true, data: pkg };
|
||||
} catch (error) {
|
||||
logger.error('Error getting package', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'GET_PACKAGE_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleListPackages(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const filter = ListPackagesSchema.parse(params);
|
||||
const service = getPredictionService();
|
||||
const packages = await service.listPackages(filter);
|
||||
return { success: true, data: packages };
|
||||
} catch (error) {
|
||||
logger.error('Error listing packages', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'LIST_PACKAGES_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdatePackageStatus(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, packageId, isActive } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
packageId: z.string().uuid(),
|
||||
isActive: z.boolean(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const pkg = await service.updatePackageStatus(packageId, tenantId, isActive);
|
||||
return { success: true, data: pkg };
|
||||
} catch (error) {
|
||||
logger.error('Error updating package status', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'UPDATE_PACKAGE_STATUS_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePurchasePackage(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const input = PurchasePackageSchema.parse(params);
|
||||
const service = getPredictionService();
|
||||
const purchase = await service.purchasePackage(input);
|
||||
logger.info('Package purchased', { purchaseId: purchase.id, packageId: input.packageId });
|
||||
return { success: true, data: purchase };
|
||||
} catch (error) {
|
||||
logger.error('Error purchasing package', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'PURCHASE_PACKAGE_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetPurchase(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, purchaseId } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
purchaseId: z.string().uuid(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const purchase = await service.getPurchase(purchaseId, tenantId);
|
||||
if (!purchase) {
|
||||
return { success: false, error: 'Purchase not found', code: 'PURCHASE_NOT_FOUND' };
|
||||
}
|
||||
return { success: true, data: purchase };
|
||||
} catch (error) {
|
||||
logger.error('Error getting purchase', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'GET_PURCHASE_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetUserPurchases(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, userId, activeOnly } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
activeOnly: z.boolean().optional(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const purchases = await service.getUserPurchases(userId, tenantId, activeOnly);
|
||||
return { success: true, data: purchases };
|
||||
} catch (error) {
|
||||
logger.error('Error getting user purchases', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'GET_USER_PURCHASES_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRequestPrediction(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const input = RequestPredictionSchema.parse(params);
|
||||
const service = getPredictionService();
|
||||
const prediction = await service.requestPrediction(input);
|
||||
return { success: true, data: prediction };
|
||||
} catch (error) {
|
||||
logger.error('Error requesting prediction', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'REQUEST_PREDICTION_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetPrediction(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, predictionId } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
predictionId: z.string().uuid(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const prediction = await service.getPrediction(predictionId, tenantId);
|
||||
if (!prediction) {
|
||||
return { success: false, error: 'Prediction not found', code: 'PREDICTION_NOT_FOUND' };
|
||||
}
|
||||
return { success: true, data: prediction };
|
||||
} catch (error) {
|
||||
logger.error('Error getting prediction', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'GET_PREDICTION_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleListPredictions(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const filter = ListPredictionsSchema.parse(params);
|
||||
const service = getPredictionService();
|
||||
const predictions = await service.listPredictions(filter);
|
||||
return { success: true, data: predictions };
|
||||
} catch (error) {
|
||||
logger.error('Error listing predictions', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'LIST_PREDICTIONS_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRecordOutcome(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const input = RecordOutcomeSchema.parse(params);
|
||||
const service = getPredictionService();
|
||||
const outcome = await service.recordOutcome(input);
|
||||
return { success: true, data: outcome };
|
||||
} catch (error) {
|
||||
logger.error('Error recording outcome', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'RECORD_OUTCOME_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetOutcome(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, predictionId } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
predictionId: z.string().uuid(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const outcome = await service.getOutcome(predictionId, tenantId);
|
||||
if (!outcome) {
|
||||
return { success: false, error: 'Outcome not found', code: 'OUTCOME_NOT_FOUND' };
|
||||
}
|
||||
return { success: true, data: outcome };
|
||||
} catch (error) {
|
||||
logger.error('Error getting outcome', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'GET_OUTCOME_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGetUserStats(params: unknown): Promise<McpResponse> {
|
||||
try {
|
||||
const { tenantId, userId } = z.object({
|
||||
tenantId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
}).parse(params);
|
||||
const service = getPredictionService();
|
||||
const stats = await service.getUserStats(userId, tenantId);
|
||||
return { success: true, data: stats };
|
||||
} catch (error) {
|
||||
logger.error('Error getting user stats', { error });
|
||||
return { success: false, error: (error as Error).message, code: 'GET_USER_STATS_ERROR' };
|
||||
}
|
||||
}
|
||||
|
||||
// Tool handlers map
|
||||
export const toolHandlers: Record<string, (params: unknown) => Promise<McpResponse>> = {
|
||||
prediction_create_package: handleCreatePackage,
|
||||
prediction_get_package: handleGetPackage,
|
||||
prediction_list_packages: handleListPackages,
|
||||
prediction_update_package_status: handleUpdatePackageStatus,
|
||||
prediction_purchase_package: handlePurchasePackage,
|
||||
prediction_get_purchase: handleGetPurchase,
|
||||
prediction_get_user_purchases: handleGetUserPurchases,
|
||||
prediction_request: handleRequestPrediction,
|
||||
prediction_get: handleGetPrediction,
|
||||
prediction_list: handleListPredictions,
|
||||
prediction_record_outcome: handleRecordOutcome,
|
||||
prediction_get_outcome: handleGetOutcome,
|
||||
prediction_get_user_stats: handleGetUserStats,
|
||||
};
|
||||
210
src/types/prediction.types.ts
Normal file
210
src/types/prediction.types.ts
Normal file
@ -0,0 +1,210 @@
|
||||
/**
|
||||
* ML Predictions Types
|
||||
*/
|
||||
|
||||
// Prediction types based on ML models
|
||||
export type PredictionType = 'AMD' | 'RANGE' | 'TPSL' | 'ICT_SMC' | 'STRATEGY_ENSEMBLE';
|
||||
|
||||
// Asset classes
|
||||
export type AssetClass = 'FOREX' | 'CRYPTO' | 'INDICES' | 'COMMODITIES';
|
||||
|
||||
// Prediction package status
|
||||
export type PackageStatus = 'ACTIVE' | 'INACTIVE' | 'DEPRECATED';
|
||||
|
||||
// Purchase status
|
||||
export type PurchaseStatus = 'PENDING' | 'COMPLETED' | 'FAILED' | 'REFUNDED';
|
||||
|
||||
// Prediction status (matches DDL: ml.prediction_status)
|
||||
export type PredictionStatus = 'pending' | 'delivered' | 'expired' | 'validated' | 'invalidated';
|
||||
|
||||
// Outcome result (matches DDL: ml.outcome_status)
|
||||
export type OutcomeResult = 'pending' | 'win' | 'loss' | 'partial' | 'expired' | 'cancelled';
|
||||
|
||||
// Prediction package interface
|
||||
export interface PredictionPackage {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
predictionType: PredictionType;
|
||||
assetClasses: AssetClass[];
|
||||
predictionsCount: number;
|
||||
priceCredits: number;
|
||||
validityDays: number;
|
||||
vipTierRequired: string | null;
|
||||
isActive: boolean;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Package purchase interface
|
||||
export interface PackagePurchase {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
walletId: string;
|
||||
packageId: string;
|
||||
status: PurchaseStatus;
|
||||
amountPaid: number;
|
||||
predictionsRemaining: number;
|
||||
predictionsUsed: number;
|
||||
expiresAt: Date;
|
||||
walletTransactionId: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// Purchase with package details
|
||||
export interface PurchaseWithPackage extends PackagePurchase {
|
||||
packageName: string;
|
||||
predictionType: PredictionType;
|
||||
totalPredictions: number;
|
||||
}
|
||||
|
||||
// Prediction interface
|
||||
export interface Prediction {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
purchaseId: string;
|
||||
userId: string;
|
||||
predictionType: PredictionType;
|
||||
asset: string;
|
||||
assetClass: AssetClass;
|
||||
timeframe: string;
|
||||
direction: 'LONG' | 'SHORT' | 'NEUTRAL';
|
||||
entryPrice: number | null;
|
||||
targetPrice: number | null;
|
||||
stopLoss: number | null;
|
||||
confidence: number;
|
||||
status: PredictionStatus;
|
||||
expiresAt: Date;
|
||||
deliveredAt: Date | null;
|
||||
predictionData: Record<string, unknown>;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Prediction with outcome
|
||||
export interface PredictionWithOutcome extends Prediction {
|
||||
outcome?: PredictionOutcome;
|
||||
}
|
||||
|
||||
// Prediction outcome interface
|
||||
export interface PredictionOutcome {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
predictionId: string;
|
||||
result: OutcomeResult;
|
||||
actualPrice: number | null;
|
||||
pnlPercent: number | null;
|
||||
pnlAbsolute: number | null;
|
||||
verifiedAt: Date;
|
||||
verificationSource: string;
|
||||
notes: string | null;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
// Create package input
|
||||
export interface CreatePackageInput {
|
||||
tenantId: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
predictionType: PredictionType;
|
||||
assetClasses: AssetClass[];
|
||||
predictionsCount: number;
|
||||
priceCredits: number;
|
||||
validityDays: number;
|
||||
vipTierRequired?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Purchase package input
|
||||
export interface PurchasePackageInput {
|
||||
tenantId: string;
|
||||
userId: string;
|
||||
walletId: string;
|
||||
packageId: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Request prediction input
|
||||
export interface RequestPredictionInput {
|
||||
tenantId: string;
|
||||
purchaseId: string;
|
||||
userId: string;
|
||||
asset: string;
|
||||
assetClass: AssetClass;
|
||||
timeframe: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Record outcome input
|
||||
export interface RecordOutcomeInput {
|
||||
tenantId: string;
|
||||
predictionId: string;
|
||||
result: OutcomeResult;
|
||||
actualPrice?: number;
|
||||
pnlPercent?: number;
|
||||
pnlAbsolute?: number;
|
||||
verificationSource: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
// Prediction stats
|
||||
export interface PredictionStats {
|
||||
totalPredictions: number;
|
||||
winCount: number;
|
||||
lossCount: number;
|
||||
partialCount: number;
|
||||
expiredCount: number;
|
||||
winRate: number;
|
||||
averagePnlPercent: number;
|
||||
byType: {
|
||||
predictionType: PredictionType;
|
||||
total: number;
|
||||
wins: number;
|
||||
winRate: number;
|
||||
}[];
|
||||
byAssetClass: {
|
||||
assetClass: AssetClass;
|
||||
total: number;
|
||||
wins: number;
|
||||
winRate: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
// Pagination
|
||||
export interface PaginationParams {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// List packages filter
|
||||
export interface ListPackagesFilter extends PaginationParams {
|
||||
tenantId: string;
|
||||
predictionType?: PredictionType;
|
||||
assetClass?: AssetClass;
|
||||
activeOnly?: boolean;
|
||||
}
|
||||
|
||||
// List predictions filter
|
||||
export interface ListPredictionsFilter extends PaginationParams {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
purchaseId?: string;
|
||||
predictionType?: PredictionType;
|
||||
assetClass?: AssetClass;
|
||||
status?: PredictionStatus;
|
||||
asset?: string;
|
||||
}
|
||||
|
||||
// MCP Response format
|
||||
export interface McpResponse {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
code?: string;
|
||||
}
|
||||
38
src/utils/logger.ts
Normal file
38
src/utils/logger.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Winston Logger Configuration
|
||||
*/
|
||||
|
||||
import winston from 'winston';
|
||||
import { serverConfig } from '../config';
|
||||
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.printf(({ level, message, timestamp, ...meta }) => {
|
||||
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
||||
return `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}`;
|
||||
})
|
||||
);
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: serverConfig.logLevel,
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
logFormat
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Log unhandled rejections
|
||||
process.on('unhandledRejection', (reason: unknown) => {
|
||||
logger.error('Unhandled Rejection:', { reason });
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error: Error) => {
|
||||
logger.error('Uncaught Exception:', { error: error.message, stack: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user