From 486bfa167073467927e402d951ae925c35e5efef Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Fri, 16 Jan 2026 08:33:15 -0600 Subject: [PATCH] =?UTF-8?q?Migraci=C3=B3n=20desde=20trading-platform/apps/?= =?UTF-8?q?mcp-predictions=20-=20Est=C3=A1ndar=20multi-repo=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .env.example | 27 + Dockerfile | 18 + README.md | 100 ++++ package.json | 37 ++ src/config.ts | 76 +++ src/index.ts | 304 +++++++++++ src/middleware/auth.middleware.ts | 96 ++++ src/middleware/index.ts | 1 + src/services/prediction.service.ts | 820 +++++++++++++++++++++++++++++ src/tools/index.ts | 31 ++ src/tools/prediction.ts | 510 ++++++++++++++++++ src/types/prediction.types.ts | 210 ++++++++ src/utils/logger.ts | 38 ++ tsconfig.json | 19 + 14 files changed, 2287 insertions(+) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 package.json create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/middleware/auth.middleware.ts create mode 100644 src/middleware/index.ts create mode 100644 src/services/prediction.service.ts create mode 100644 src/tools/index.ts create mode 100644 src/tools/prediction.ts create mode 100644 src/types/prediction.types.ts create mode 100644 src/utils/logger.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..abb77fc --- /dev/null +++ b/.env.example @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9190b4 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..24dc627 --- /dev/null +++ b/README.md @@ -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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..f86f3a9 --- /dev/null +++ b/package.json @@ -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 +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8af27ff --- /dev/null +++ b/src/config.ts @@ -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 { + 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 { + await client.query(`SET LOCAL app.current_tenant_id = '${tenantId}'`); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..38b5be4 --- /dev/null +++ b/src/index.ts @@ -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; diff --git a/src/middleware/auth.middleware.ts b/src/middleware/auth.middleware.ts new file mode 100644 index 0000000..4672884 --- /dev/null +++ b/src/middleware/auth.middleware.ts @@ -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(); +} diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 0000000..22f5128 --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1 @@ +export { authMiddleware, optionalAuthMiddleware, adminMiddleware } from './auth.middleware'; diff --git a/src/services/prediction.service.ts b/src/services/prediction.service.ts new file mode 100644 index 0000000..bc08638 --- /dev/null +++ b/src/services/prediction.service.ts @@ -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 { + const client = await this.pool.connect(); + try { + await setTenantContext(client, input.tenantId); + + const packageId = uuidv4(); + const result = await client.query( + `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 { + const client = await this.pool.connect(); + try { + await setTenantContext(client, tenantId); + const result = await client.query( + `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 { + 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( + `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 { + const client = await this.pool.connect(); + try { + await setTenantContext(client, tenantId); + const result = await client.query( + `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 { + 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( + `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 { + const client = await this.pool.connect(); + try { + await setTenantContext(client, tenantId); + const result = await client.query( + `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 { + 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( + `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 { + 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( + `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 { + const client = await this.pool.connect(); + try { + await setTenantContext(client, tenantId); + + const predResult = await client.query( + `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( + `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 { + 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( + `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 { + 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( + `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 { + const client = await this.pool.connect(); + try { + await setTenantContext(client, tenantId); + const result = await client.query( + `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 { + 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 { + const result = await client.query( + `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 { + const result = await client.query( + `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 { + const result = await client.query( + `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 { + 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 { + 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; +} diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..66b9088 --- /dev/null +++ b/src/tools/index.ts @@ -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)[name]; +} + +// Get handler by name +export function getToolHandler(name: string) { + return allToolHandlers[name]; +} + +// List all available tools +export function listTools() { + return Object.values(allToolSchemas); +} diff --git a/src/tools/prediction.ts b/src/tools/prediction.ts new file mode 100644 index 0000000..352c222 --- /dev/null +++ b/src/tools/prediction.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 Promise> = { + 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, +}; diff --git a/src/types/prediction.types.ts b/src/types/prediction.types.ts new file mode 100644 index 0000000..db37160 --- /dev/null +++ b/src/types/prediction.types.ts @@ -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; + 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; + 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; + metadata: Record; + 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; + 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; +} + +// Purchase package input +export interface PurchasePackageInput { + tenantId: string; + userId: string; + walletId: string; + packageId: string; + metadata?: Record; +} + +// Request prediction input +export interface RequestPredictionInput { + tenantId: string; + purchaseId: string; + userId: string; + asset: string; + assetClass: AssetClass; + timeframe: string; + metadata?: Record; +} + +// 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; +} diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..0c3e691 --- /dev/null +++ b/src/utils/logger.ts @@ -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); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..98d4c73 --- /dev/null +++ b/tsconfig.json @@ -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"] +}