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:
rckrdmrd 2026-01-16 08:33:15 -06:00
commit 486bfa1670
14 changed files with 2287 additions and 0 deletions

27
.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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;

View 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
View File

@ -0,0 +1 @@
export { authMiddleware, optionalAuthMiddleware, adminMiddleware } from './auth.middleware';

View 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
View 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
View 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,
};

View 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
View 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
View 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"]
}