Migración desde trading-platform/apps/mcp-investment - 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:11 -06:00
commit ce711aa6d4
14 changed files with 2478 additions and 0 deletions

26
.env.example Normal file
View File

@ -0,0 +1,26 @@
# MCP INVESTMENT SERVER CONFIGURATION
PORT=3093
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
# Investment Config
MIN_ALLOCATION_AMOUNT=100
MAX_ALLOCATION_AMOUNT=100000
MIN_WITHDRAWAL_AMOUNT=10
PROFIT_DISTRIBUTION_FREQUENCY_DAYS=30
ALLOCATION_LOCK_PERIOD_DAYS=7
EARLY_WITHDRAWAL_PENALTY_PERCENT=5

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 investment -u 1001
COPY --from=builder --chown=investment:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=investment:nodejs /app/dist ./dist
COPY --from=builder --chown=investment:nodejs /app/package.json ./
USER investment
EXPOSE 3093
HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:3093/health || exit 1
CMD ["node", "dist/index.js"]

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# MCP Investment Server
Investment/Agent Allocation MCP Server for the Trading Platform. Manages Money Manager agents (Atlas, Orion, Nova) allocations and profit distributions.
## Agents
| Agent | Risk Level | Target Return | Max Drawdown | Mgmt Fee | Perf Fee |
|-------|------------|---------------|--------------|----------|----------|
| **Atlas** | Conservative | 8%/yr | 5% | 1.5% | 15% |
| **Orion** | Moderate | 15%/yr | 12% | 2.0% | 20% |
| **Nova** | Aggressive | 25%/yr | 25% | 2.5% | 25% |
## Quick Start
```bash
npm install
cp .env.example .env
npm run dev
```
## API Endpoints
### Agents
- `GET /api/v1/agents` - List all agents
- `GET /api/v1/agents/:agentType` - Get agent config
- `GET /api/v1/agents/:agentType/performance` - Agent performance metrics
- `GET /api/v1/agents/:agentType/distributions` - Profit distribution history
### Allocations
- `POST /api/v1/allocations` - Create allocation (debits wallet)
- `GET /api/v1/allocations` - List allocations
- `GET /api/v1/allocations/:id` - Get allocation
- `POST /api/v1/allocations/:id/fund` - Add funds
- `POST /api/v1/allocations/:id/withdraw` - Withdraw funds
- `PATCH /api/v1/allocations/:id/status` - Update status
- `GET /api/v1/allocations/:id/transactions` - Transaction history
### User
- `GET /api/v1/users/:userId/allocations` - User allocations
- `GET /api/v1/users/:userId/investment-summary` - Portfolio summary
### Admin
- `POST /api/v1/admin/distribute-profits` - Distribute profits
## MCP Tools (14)
| Tool | Description |
|------|-------------|
| `investment_list_agents` | List agents |
| `investment_get_agent` | Get agent config |
| `investment_create_allocation` | Create allocation |
| `investment_get_allocation` | Get allocation |
| `investment_list_allocations` | List allocations |
| `investment_get_user_allocations` | User allocations |
| `investment_fund_allocation` | Add funds |
| `investment_withdraw` | Withdraw funds |
| `investment_update_status` | Update status |
| `investment_get_transactions` | Get transactions |
| `investment_distribute_profits` | Distribute profits |
| `investment_get_distributions` | Distribution history |
| `investment_get_user_summary` | User summary |
| `investment_get_agent_performance` | Agent metrics |
## Allocation Status
- `pending` - Awaiting activation
- `active` - Active and trading
- `paused` - Temporarily paused
- `liquidating` - Liquidation in progress
- `closed` - Closed/fully withdrawn
## Features
- 7-day lock period for new allocations
- 5% early withdrawal penalty
- Automatic profit distribution to allocations
- Management and performance fee deduction
- Integration with Wallet service for funding/withdrawals
## License
UNLICENSED - Private

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "@trading-platform/mcp-investment",
"version": "1.0.0",
"description": "MCP Server for Investment/PAMM - Money Manager Agent Allocations",
"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/express": "^4.17.21",
"@types/node": "^20.10.0",
"@types/pg": "^8.10.9",
"@types/uuid": "^9.0.7",
"@types/cors": "^2.8.17",
"@types/jsonwebtoken": "^9.0.5",
"typescript": "^5.3.2",
"ts-node-dev": "^2.0.0"
},
"engines": { "node": ">=18.0.0" },
"private": true
}

103
src/config.ts Normal file
View File

@ -0,0 +1,103 @@
/**
* MCP Investment Server Configuration
* Agent allocations and profit distribution
*/
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 || '3093', 10),
env: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
};
// Wallet service configuration
export const walletServiceConfig = {
baseUrl: process.env.WALLET_SERVICE_URL || 'http://localhost:3090',
timeout: parseInt(process.env.WALLET_SERVICE_TIMEOUT || '10000', 10),
};
// Investment configuration
export const investmentConfig = {
// Minimum allocation amount per agent
minAllocationAmount: parseFloat(process.env.MIN_ALLOCATION_AMOUNT || '100'),
// Maximum allocation amount per agent
maxAllocationAmount: parseFloat(process.env.MAX_ALLOCATION_AMOUNT || '100000'),
// Minimum withdrawal amount
minWithdrawalAmount: parseFloat(process.env.MIN_WITHDRAWAL_AMOUNT || '10'),
// Profit distribution frequency (days)
profitDistributionFrequencyDays: parseInt(process.env.PROFIT_DISTRIBUTION_FREQUENCY_DAYS || '30', 10),
// Lock period for new allocations (days)
allocationLockPeriodDays: parseInt(process.env.ALLOCATION_LOCK_PERIOD_DAYS || '7', 10),
// Early withdrawal penalty percentage
earlyWithdrawalPenaltyPercent: parseFloat(process.env.EARLY_WITHDRAWAL_PENALTY_PERCENT || '5'),
// Agent types
agentTypes: ['ATLAS', 'ORION', 'NOVA'] as const,
};
// Agent default configurations
export const agentDefaults = {
ATLAS: {
name: 'Atlas',
riskLevel: 'conservative',
targetReturnPercent: 8,
maxDrawdownPercent: 5,
managementFeePercent: 1.5,
performanceFeePercent: 15,
},
ORION: {
name: 'Orion',
riskLevel: 'moderate',
targetReturnPercent: 15,
maxDrawdownPercent: 12,
managementFeePercent: 2.0,
performanceFeePercent: 20,
},
NOVA: {
name: 'Nova',
riskLevel: 'aggressive',
targetReturnPercent: 25,
maxDrawdownPercent: 25,
managementFeePercent: 2.5,
performanceFeePercent: 25,
},
};
// 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}'`);
}

298
src/index.ts Normal file
View File

@ -0,0 +1,298 @@
/**
* MCP Investment Server
* Agent allocations and profit distribution
*/
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, 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-investment', 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
// ============================================================================
// Agent configs (public info, but tenant-scoped)
const agentsRouter = express.Router();
agentsRouter.use(authMiddleware);
agentsRouter.get('/', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_list_agents({ tenantId: req.tenantId });
res.json(result);
});
agentsRouter.get('/:agentType', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_agent({
tenantId: req.tenantId,
agentType: req.params.agentType,
});
res.json(result);
});
agentsRouter.get('/:agentType/performance', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_agent_performance({
tenantId: req.tenantId,
agentType: req.params.agentType,
periodDays: req.query.periodDays ? parseInt(req.query.periodDays as string, 10) : undefined,
});
res.json(result);
});
agentsRouter.get('/:agentType/distributions', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_distributions({
tenantId: req.tenantId,
agentType: req.params.agentType,
limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined,
});
res.json(result);
});
app.use('/api/v1/agents', agentsRouter);
// Allocations - Protected
const allocationsRouter = express.Router();
allocationsRouter.use(authMiddleware);
// Create allocation
allocationsRouter.post('/', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_create_allocation({
...req.body,
tenantId: req.tenantId,
userId: req.userId,
});
res.status(result.success ? 201 : 400).json(result);
});
// List allocations
allocationsRouter.get('/', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_list_allocations({
tenantId: req.tenantId,
userId: req.query.userId || req.userId, // Default to current user
agentType: req.query.agentType,
status: req.query.status,
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 allocation
allocationsRouter.get('/:allocationId', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_allocation({
tenantId: req.tenantId,
allocationId: req.params.allocationId,
});
res.json(result);
});
// Fund allocation
allocationsRouter.post('/:allocationId/fund', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_fund_allocation({
...req.body,
tenantId: req.tenantId,
allocationId: req.params.allocationId,
userId: req.userId,
});
res.json(result);
});
// Withdraw from allocation
allocationsRouter.post('/:allocationId/withdraw', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_withdraw({
...req.body,
tenantId: req.tenantId,
allocationId: req.params.allocationId,
userId: req.userId,
});
res.json(result);
});
// Update allocation status
allocationsRouter.patch('/:allocationId/status', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_update_status({
...req.body,
tenantId: req.tenantId,
allocationId: req.params.allocationId,
});
res.json(result);
});
// Get allocation transactions
allocationsRouter.get('/:allocationId/transactions', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_transactions({
tenantId: req.tenantId,
allocationId: req.params.allocationId,
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);
});
app.use('/api/v1/allocations', allocationsRouter);
// User endpoints - Protected
const userRouter = express.Router();
userRouter.use(authMiddleware);
// Get current user's allocations
userRouter.get('/me/allocations', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_user_allocations({
tenantId: req.tenantId,
userId: req.userId,
});
res.json(result);
});
// Get current user's investment summary
userRouter.get('/me/investment-summary', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_get_user_summary({
tenantId: req.tenantId,
userId: req.userId,
});
res.json(result);
});
// Get specific user's allocations (for admins or own data)
userRouter.get('/:userId/allocations', async (req: Request, res: Response) => {
// Users can only see their own allocations unless they're owners
if (req.params.userId !== req.userId && !req.isOwner) {
res.status(403).json({ success: false, error: 'Forbidden', code: 'ACCESS_DENIED' });
return;
}
const result = await allToolHandlers.investment_get_user_allocations({
tenantId: req.tenantId,
userId: req.params.userId,
});
res.json(result);
});
// Get specific user's investment summary
userRouter.get('/:userId/investment-summary', 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.investment_get_user_summary({
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);
// Distribute profits (admin only)
adminRouter.post('/distribute-profits', async (req: Request, res: Response) => {
const result = await allToolHandlers.investment_distribute_profits({
...req.body,
tenantId: req.tenantId,
distributedBy: req.userId,
});
res.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 Investment Server running on port ${serverConfig.port}`, {
env: serverConfig.env,
tools: Object.keys(allToolSchemas).length,
});
console.log(`
MCP INVESTMENT SERVER
Port: ${serverConfig.port}
Tools: ${String(Object.keys(allToolSchemas).length).padEnd(12)}
/api/v1/agents/* - Agent info
/api/v1/allocations/* - Investment allocations
/api/v1/users/* - User investments
/api/v1/admin/* - Admin operations
`);
});
export default app;

View File

@ -0,0 +1,114 @@
/**
* Auth Middleware for MCP Investment
* Verifies JWT tokens and sets user context
*/
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;
}
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;
} catch {
req.isAuthenticated = false;
}
next();
}
/**
* Admin-only middleware - requires owner or admin role
*/
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,997 @@
/**
* Investment Service
* Handles agent allocations, transactions, and profit distributions
*/
import { Pool, PoolClient } from 'pg';
import { v4 as uuidv4 } from 'uuid';
import Decimal from 'decimal.js';
import {
AgentType,
AllocationStatus,
AgentAllocation,
AllocationWithAgent,
AllocationTransaction,
AgentConfig,
ProfitDistribution,
CreateAllocationInput,
FundAllocationInput,
WithdrawAllocationInput,
UpdateAllocationStatusInput,
DistributeProfitsInput,
AllocationSummary,
AgentPerformanceMetrics,
ListAllocationsFilter,
AllocationTransactionType,
} from '../types/investment.types';
import { getPool, setTenantContext, investmentConfig, walletServiceConfig } from '../config';
import { logger } from '../utils/logger';
export class InvestmentService {
private pool: Pool;
constructor() {
this.pool = getPool();
}
// ============ Agent Configuration ============
async getAgentConfig(agentType: AgentType, tenantId: string): Promise<AgentConfig | null> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query<AgentConfig>(
`SELECT id, tenant_id as "tenantId", agent_type as "agentType", name, description,
risk_level as "riskLevel", is_active as "isActive",
min_allocation as "minAllocation", max_allocation as "maxAllocation",
target_return_percent as "targetReturnPercent",
max_drawdown_percent as "maxDrawdownPercent",
management_fee_percent as "managementFeePercent",
performance_fee_percent as "performanceFeePercent",
total_aum as "totalAum", total_allocations as "totalAllocations",
metadata, created_at as "createdAt", updated_at as "updatedAt"
FROM investment.agent_configs
WHERE agent_type = $1 AND tenant_id = $2`,
[agentType, tenantId]
);
return result.rows[0] || null;
} finally {
client.release();
}
}
async listAgentConfigs(tenantId: string): Promise<AgentConfig[]> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query<AgentConfig>(
`SELECT id, tenant_id as "tenantId", agent_type as "agentType", name, description,
risk_level as "riskLevel", is_active as "isActive",
min_allocation as "minAllocation", max_allocation as "maxAllocation",
target_return_percent as "targetReturnPercent",
max_drawdown_percent as "maxDrawdownPercent",
management_fee_percent as "managementFeePercent",
performance_fee_percent as "performanceFeePercent",
total_aum as "totalAum", total_allocations as "totalAllocations",
metadata, created_at as "createdAt", updated_at as "updatedAt"
FROM investment.agent_configs
WHERE tenant_id = $1 AND is_active = true
ORDER BY agent_type`,
[tenantId]
);
return result.rows;
} finally {
client.release();
}
}
// ============ Allocations ============
async createAllocation(input: CreateAllocationInput): Promise<AllocationWithAgent> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, input.tenantId);
// Validate agent config exists and is active
const agentConfig = await this.getAgentConfigInternal(client, input.agentType, input.tenantId);
if (!agentConfig) {
throw new Error(`Agent ${input.agentType} is not configured for this tenant`);
}
if (!agentConfig.isActive) {
throw new Error(`Agent ${input.agentType} is not active`);
}
// Validate amount
if (input.amount < agentConfig.minAllocation) {
throw new Error(`Minimum allocation for ${input.agentType} is ${agentConfig.minAllocation}`);
}
if (input.amount > agentConfig.maxAllocation) {
throw new Error(`Maximum allocation for ${input.agentType} is ${agentConfig.maxAllocation}`);
}
// Check for existing active allocation
const existingResult = await client.query(
`SELECT id FROM investment.agent_allocations
WHERE user_id = $1 AND agent_type = $2 AND status IN ('active', 'pending')
LIMIT 1`,
[input.userId, input.agentType]
);
if (existingResult.rows.length > 0) {
throw new Error(`User already has an active allocation with ${input.agentType}`);
}
// Debit wallet first
const walletTxId = await this.debitWallet(
input.walletId,
input.amount,
input.tenantId,
'AGENT_ALLOCATION',
`Initial allocation to ${input.agentType}`,
null
);
// Calculate lock expiration
const lockExpiresAt = new Date();
lockExpiresAt.setDate(lockExpiresAt.getDate() + investmentConfig.allocationLockPeriodDays);
// Create allocation
const allocationId = uuidv4();
const result = await client.query<AgentAllocation>(
`INSERT INTO investment.agent_allocations (
id, tenant_id, user_id, wallet_id, agent_type, status,
allocated_amount, current_value, total_deposited,
lock_expires_at, metadata
) VALUES ($1, $2, $3, $4, $5, 'active', $6, $6, $6, $7, $8)
RETURNING id, tenant_id as "tenantId", user_id as "userId", wallet_id as "walletId",
agent_type as "agentType", status, allocated_amount as "allocatedAmount",
current_value as "currentValue", total_deposited as "totalDeposited",
total_withdrawn as "totalWithdrawn", total_profit_distributed as "totalProfitDistributed",
total_fees_paid as "totalFeesPaid", unrealized_pnl as "unrealizedPnl",
realized_pnl as "realizedPnl", last_profit_distribution_at as "lastProfitDistributionAt",
lock_expires_at as "lockExpiresAt", metadata,
created_at as "createdAt", updated_at as "updatedAt"`,
[
allocationId,
input.tenantId,
input.userId,
input.walletId,
input.agentType,
input.amount,
lockExpiresAt,
input.metadata || {},
]
);
// Create initial transaction
await this.createTransaction(client, {
allocationId,
tenantId: input.tenantId,
type: 'INITIAL_FUNDING',
amount: input.amount,
balanceBefore: 0,
balanceAfter: input.amount,
walletTransactionId: walletTxId,
description: 'Initial allocation deposit',
});
// Update agent AUM
await client.query(
`UPDATE investment.agent_configs
SET total_aum = total_aum + $1, total_allocations = total_allocations + 1, updated_at = NOW()
WHERE agent_type = $2 AND tenant_id = $3`,
[input.amount, input.agentType, input.tenantId]
);
await client.query('COMMIT');
const allocation = result.rows[0];
return {
...allocation,
agentName: agentConfig.name,
agentRiskLevel: agentConfig.riskLevel,
agentTargetReturn: agentConfig.targetReturnPercent,
};
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async getAllocation(allocationId: string, tenantId: string): Promise<AllocationWithAgent | null> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query<AllocationWithAgent>(
`SELECT a.id, a.tenant_id as "tenantId", a.user_id as "userId", a.wallet_id as "walletId",
a.agent_type as "agentType", a.status, a.allocated_amount as "allocatedAmount",
a.current_value as "currentValue", a.total_deposited as "totalDeposited",
a.total_withdrawn as "totalWithdrawn", a.total_profit_distributed as "totalProfitDistributed",
a.total_fees_paid as "totalFeesPaid", a.unrealized_pnl as "unrealizedPnl",
a.realized_pnl as "realizedPnl", a.last_profit_distribution_at as "lastProfitDistributionAt",
a.lock_expires_at as "lockExpiresAt", a.metadata,
a.created_at as "createdAt", a.updated_at as "updatedAt",
c.name as "agentName", c.risk_level as "agentRiskLevel",
c.target_return_percent as "agentTargetReturn"
FROM investment.agent_allocations a
JOIN investment.agent_configs c ON a.agent_type = c.agent_type AND a.tenant_id = c.tenant_id
WHERE a.id = $1 AND a.tenant_id = $2`,
[allocationId, tenantId]
);
return result.rows[0] || null;
} finally {
client.release();
}
}
async listAllocations(filter: ListAllocationsFilter): Promise<AllocationWithAgent[]> {
const client = await this.pool.connect();
try {
await setTenantContext(client, filter.tenantId);
const conditions: string[] = ['a.tenant_id = $1'];
const params: unknown[] = [filter.tenantId];
let paramIndex = 2;
if (filter.userId) {
conditions.push(`a.user_id = $${paramIndex++}`);
params.push(filter.userId);
}
if (filter.agentType) {
conditions.push(`a.agent_type = $${paramIndex++}`);
params.push(filter.agentType);
}
if (filter.status) {
conditions.push(`a.status = $${paramIndex++}`);
params.push(filter.status);
}
const limit = filter.limit || 50;
const offset = filter.offset || 0;
const result = await client.query<AllocationWithAgent>(
`SELECT a.id, a.tenant_id as "tenantId", a.user_id as "userId", a.wallet_id as "walletId",
a.agent_type as "agentType", a.status, a.allocated_amount as "allocatedAmount",
a.current_value as "currentValue", a.total_deposited as "totalDeposited",
a.total_withdrawn as "totalWithdrawn", a.total_profit_distributed as "totalProfitDistributed",
a.total_fees_paid as "totalFeesPaid", a.unrealized_pnl as "unrealizedPnl",
a.realized_pnl as "realizedPnl", a.last_profit_distribution_at as "lastProfitDistributionAt",
a.lock_expires_at as "lockExpiresAt", a.metadata,
a.created_at as "createdAt", a.updated_at as "updatedAt",
c.name as "agentName", c.risk_level as "agentRiskLevel",
c.target_return_percent as "agentTargetReturn"
FROM investment.agent_allocations a
JOIN investment.agent_configs c ON a.agent_type = c.agent_type AND a.tenant_id = c.tenant_id
WHERE ${conditions.join(' AND ')}
ORDER BY a.created_at DESC
LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
[...params, limit, offset]
);
return result.rows;
} finally {
client.release();
}
}
async getUserAllocations(userId: string, tenantId: string): Promise<AllocationWithAgent[]> {
return this.listAllocations({ tenantId, userId });
}
async fundAllocation(input: FundAllocationInput): Promise<AllocationWithAgent> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, input.tenantId);
// Get allocation
const allocation = await this.getAllocationInternal(client, input.allocationId, input.tenantId);
if (!allocation) {
throw new Error('Allocation not found');
}
if (allocation.status !== 'active') {
throw new Error('Can only fund active allocations');
}
// Validate amount
if (input.amount < investmentConfig.minAllocationAmount) {
throw new Error(`Minimum funding amount is ${investmentConfig.minAllocationAmount}`);
}
// Check max allocation
const agentConfig = await this.getAgentConfigInternal(client, allocation.agentType, input.tenantId);
const newTotal = new Decimal(allocation.currentValue).plus(input.amount);
if (newTotal.greaterThan(agentConfig!.maxAllocation)) {
throw new Error(`Would exceed maximum allocation of ${agentConfig!.maxAllocation}`);
}
// Debit wallet
const walletTxId = await this.debitWallet(
allocation.walletId,
input.amount,
input.tenantId,
'AGENT_ALLOCATION',
input.description || `Additional funding to ${allocation.agentType}`,
allocation.id
);
// Update allocation
const result = await client.query<AgentAllocation>(
`UPDATE investment.agent_allocations
SET allocated_amount = allocated_amount + $1,
current_value = current_value + $1,
total_deposited = total_deposited + $1,
updated_at = NOW()
WHERE id = $2 AND tenant_id = $3
RETURNING *`,
[input.amount, input.allocationId, input.tenantId]
);
// Create transaction
await this.createTransaction(client, {
allocationId: input.allocationId,
tenantId: input.tenantId,
type: 'ADDITIONAL_FUNDING',
amount: input.amount,
balanceBefore: allocation.currentValue,
balanceAfter: newTotal.toNumber(),
walletTransactionId: walletTxId,
description: input.description || 'Additional deposit',
});
// Update agent AUM
await client.query(
`UPDATE investment.agent_configs
SET total_aum = total_aum + $1, updated_at = NOW()
WHERE agent_type = $2 AND tenant_id = $3`,
[input.amount, allocation.agentType, input.tenantId]
);
await client.query('COMMIT');
return (await this.getAllocation(input.allocationId, input.tenantId))!;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async withdrawFromAllocation(input: WithdrawAllocationInput): Promise<AllocationWithAgent> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, input.tenantId);
// Get allocation
const allocation = await this.getAllocationInternal(client, input.allocationId, input.tenantId);
if (!allocation) {
throw new Error('Allocation not found');
}
if (allocation.status !== 'active') {
throw new Error('Can only withdraw from active allocations');
}
// Validate amount
if (input.amount < investmentConfig.minWithdrawalAmount) {
throw new Error(`Minimum withdrawal amount is ${investmentConfig.minWithdrawalAmount}`);
}
if (input.amount > allocation.currentValue) {
throw new Error('Insufficient allocation balance');
}
let withdrawAmount = input.amount;
let penaltyAmount = 0;
// Check if under lock period
if (allocation.lockExpiresAt && new Date() < allocation.lockExpiresAt) {
penaltyAmount = new Decimal(input.amount)
.times(investmentConfig.earlyWithdrawalPenaltyPercent)
.dividedBy(100)
.toNumber();
withdrawAmount = new Decimal(input.amount).minus(penaltyAmount).toNumber();
logger.info('Early withdrawal penalty applied', { allocationId: input.allocationId, penaltyAmount });
}
const newBalance = new Decimal(allocation.currentValue).minus(input.amount).toNumber();
// Credit wallet if requested
let walletTxId: string | null = null;
if (input.withdrawToWallet !== false) {
walletTxId = await this.creditWallet(
allocation.walletId,
withdrawAmount,
input.tenantId,
'AGENT_WITHDRAWAL',
input.description || `Withdrawal from ${allocation.agentType}`,
allocation.id
);
}
// Update allocation
await client.query(
`UPDATE investment.agent_allocations
SET current_value = current_value - $1,
total_withdrawn = total_withdrawn + $2,
updated_at = NOW()
WHERE id = $3 AND tenant_id = $4`,
[input.amount, withdrawAmount, input.allocationId, input.tenantId]
);
// Create withdrawal transaction (PARTIAL or FULL based on remaining balance)
const isFullWithdrawal = newBalance <= 0;
await this.createTransaction(client, {
allocationId: input.allocationId,
tenantId: input.tenantId,
type: isFullWithdrawal ? 'FULL_WITHDRAWAL' : 'PARTIAL_WITHDRAWAL',
amount: -withdrawAmount,
balanceBefore: allocation.currentValue,
balanceAfter: newBalance,
walletTransactionId: walletTxId,
description: input.description || 'Withdrawal',
});
// Create fee transaction if penalty applicable
if (penaltyAmount > 0) {
await this.createTransaction(client, {
allocationId: input.allocationId,
tenantId: input.tenantId,
type: 'FEE_CHARGED',
amount: -penaltyAmount,
balanceBefore: allocation.currentValue,
balanceAfter: newBalance,
description: 'Early withdrawal penalty',
});
}
// Update agent AUM
await client.query(
`UPDATE investment.agent_configs
SET total_aum = total_aum - $1, updated_at = NOW()
WHERE agent_type = $2 AND tenant_id = $3`,
[input.amount, allocation.agentType, input.tenantId]
);
// Close allocation if fully withdrawn
if (newBalance <= 0) {
await client.query(
`UPDATE investment.agent_allocations
SET status = 'closed', updated_at = NOW()
WHERE id = $1 AND tenant_id = $2`,
[input.allocationId, input.tenantId]
);
await client.query(
`UPDATE investment.agent_configs
SET total_allocations = total_allocations - 1, updated_at = NOW()
WHERE agent_type = $1 AND tenant_id = $2`,
[allocation.agentType, input.tenantId]
);
}
await client.query('COMMIT');
return (await this.getAllocation(input.allocationId, input.tenantId))!;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async updateAllocationStatus(input: UpdateAllocationStatusInput): Promise<AllocationWithAgent> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, input.tenantId);
const allocation = await this.getAllocationInternal(client, input.allocationId, input.tenantId);
if (!allocation) {
throw new Error('Allocation not found');
}
// Validate status transition (lowercase to match DDL enum)
const validTransitions: Record<AllocationStatus, AllocationStatus[]> = {
pending: ['active', 'closed'],
active: ['paused', 'liquidating', 'closed'],
paused: ['active', 'closed'],
liquidating: ['closed'],
closed: [],
};
if (!validTransitions[allocation.status].includes(input.status)) {
throw new Error(`Cannot transition from ${allocation.status} to ${input.status}`);
}
await client.query(
`UPDATE investment.agent_allocations
SET status = $1, updated_at = NOW(),
metadata = metadata || $2
WHERE id = $3 AND tenant_id = $4`,
[
input.status,
JSON.stringify({ statusReason: input.reason, statusUpdatedAt: new Date().toISOString() }),
input.allocationId,
input.tenantId,
]
);
// Update allocation count if closing
if (input.status === 'closed' && allocation.status !== 'closed') {
await client.query(
`UPDATE investment.agent_configs
SET total_allocations = total_allocations - 1,
total_aum = total_aum - $1,
updated_at = NOW()
WHERE agent_type = $2 AND tenant_id = $3`,
[allocation.currentValue, allocation.agentType, input.tenantId]
);
}
await client.query('COMMIT');
return (await this.getAllocation(input.allocationId, input.tenantId))!;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// ============ Transactions ============
async getAllocationTransactions(
allocationId: string,
tenantId: string,
limit = 50,
offset = 0
): Promise<AllocationTransaction[]> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query<AllocationTransaction>(
`SELECT id, tenant_id as "tenantId", allocation_id as "allocationId",
type, amount, balance_before as "balanceBefore", balance_after as "balanceAfter",
wallet_transaction_id as "walletTransactionId", description, metadata,
created_at as "createdAt"
FROM investment.allocation_transactions
WHERE allocation_id = $1 AND tenant_id = $2
ORDER BY created_at DESC
LIMIT $3 OFFSET $4`,
[allocationId, tenantId, limit, offset]
);
return result.rows;
} finally {
client.release();
}
}
// ============ Profit Distribution ============
async distributeProfits(input: DistributeProfitsInput): Promise<ProfitDistribution> {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
await setTenantContext(client, input.tenantId);
const agentConfig = await this.getAgentConfigInternal(client, input.agentType, input.tenantId);
if (!agentConfig) {
throw new Error(`Agent ${input.agentType} not configured`);
}
// Get all active allocations for this agent
const allocationsResult = await client.query<AgentAllocation>(
`SELECT id, current_value as "currentValue", user_id as "userId", wallet_id as "walletId"
FROM investment.agent_allocations
WHERE agent_type = $1 AND tenant_id = $2 AND status = 'active'`,
[input.agentType, input.tenantId]
);
if (allocationsResult.rows.length === 0) {
throw new Error('No active allocations for this agent');
}
const totalAum = allocationsResult.rows.reduce(
(sum, a) => new Decimal(sum).plus(a.currentValue).toNumber(),
0
);
// Calculate fees
const managementFees = new Decimal(totalAum)
.times(agentConfig.managementFeePercent)
.dividedBy(100)
.dividedBy(12) // Monthly fee from annual rate
.toNumber();
let performanceFees = 0;
if (input.totalPnl > 0) {
performanceFees = new Decimal(input.totalPnl)
.times(agentConfig.performanceFeePercent)
.dividedBy(100)
.toNumber();
}
const netDistribution = new Decimal(input.totalPnl)
.minus(managementFees)
.minus(performanceFees)
.toNumber();
// Create distribution record
const distributionId = uuidv4();
const distributionResult = await client.query<ProfitDistribution>(
`INSERT INTO investment.profit_distributions (
id, tenant_id, agent_type, period_start, period_end, status,
total_pnl, management_fees, performance_fees, net_distribution,
allocations_count, processed_at
) VALUES ($1, $2, $3, $4, $5, 'COMPLETED', $6, $7, $8, $9, $10, NOW())
RETURNING id, tenant_id as "tenantId", agent_type as "agentType",
period_start as "periodStart", period_end as "periodEnd", status,
total_pnl as "totalPnl", management_fees as "managementFees",
performance_fees as "performanceFees", net_distribution as "netDistribution",
allocations_count as "allocationsCount", processed_at as "processedAt",
metadata, created_at as "createdAt"`,
[
distributionId,
input.tenantId,
input.agentType,
input.periodStart,
input.periodEnd,
input.totalPnl,
managementFees,
performanceFees,
netDistribution,
allocationsResult.rows.length,
]
);
// Distribute to each allocation proportionally
for (const allocation of allocationsResult.rows) {
const weight = new Decimal(allocation.currentValue).dividedBy(totalAum);
const allocationProfit = weight.times(netDistribution).toDecimalPlaces(4).toNumber();
const allocationFees = weight.times(managementFees).plus(weight.times(performanceFees)).toDecimalPlaces(4).toNumber();
const newValue = new Decimal(allocation.currentValue).plus(allocationProfit).toNumber();
// Update allocation
await client.query(
`UPDATE investment.agent_allocations
SET current_value = $1,
total_profit_distributed = total_profit_distributed + $2,
total_fees_paid = total_fees_paid + $3,
realized_pnl = realized_pnl + $2,
last_profit_distribution_at = NOW(),
updated_at = NOW()
WHERE id = $4 AND tenant_id = $5`,
[newValue, allocationProfit, allocationFees, allocation.id, input.tenantId]
);
// Create profit/loss distribution transaction
if (allocationProfit !== 0) {
await this.createTransaction(client, {
allocationId: allocation.id,
tenantId: input.tenantId,
type: allocationProfit > 0 ? 'PROFIT_REALIZED' : 'LOSS_REALIZED',
amount: allocationProfit,
balanceBefore: allocation.currentValue,
balanceAfter: newValue,
description: `P/L distribution for ${input.periodStart.toISOString().split('T')[0]} to ${input.periodEnd.toISOString().split('T')[0]}`,
metadata: { distributionId },
});
}
// Create fee transactions
if (allocationFees > 0) {
await this.createTransaction(client, {
allocationId: allocation.id,
tenantId: input.tenantId,
type: 'FEE_CHARGED',
amount: -allocationFees,
balanceBefore: allocation.currentValue,
balanceAfter: newValue,
description: 'Management and performance fees',
metadata: { distributionId },
});
}
}
await client.query('COMMIT');
logger.info('Profit distribution completed', {
distributionId,
agentType: input.agentType,
totalPnl: input.totalPnl,
netDistribution,
allocationsCount: allocationsResult.rows.length,
});
return distributionResult.rows[0];
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
async getProfitDistributions(
agentType: AgentType,
tenantId: string,
limit = 12
): Promise<ProfitDistribution[]> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const result = await client.query<ProfitDistribution>(
`SELECT id, tenant_id as "tenantId", agent_type as "agentType",
period_start as "periodStart", period_end as "periodEnd", status,
total_pnl as "totalPnl", management_fees as "managementFees",
performance_fees as "performanceFees", net_distribution as "netDistribution",
allocations_count as "allocationsCount", processed_at as "processedAt",
metadata, created_at as "createdAt"
FROM investment.profit_distributions
WHERE agent_type = $1 AND tenant_id = $2
ORDER BY period_end DESC
LIMIT $3`,
[agentType, tenantId, limit]
);
return result.rows;
} finally {
client.release();
}
}
// ============ Statistics ============
async getUserAllocationSummary(userId: string, tenantId: string): Promise<AllocationSummary> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const allocations = await this.listAllocations({ tenantId, userId, status: 'active' });
const totalAllocated = allocations.reduce(
(sum, a) => new Decimal(sum).plus(a.allocatedAmount).toNumber(),
0
);
const currentValue = allocations.reduce(
(sum, a) => new Decimal(sum).plus(a.currentValue).toNumber(),
0
);
const totalPnl = new Decimal(currentValue).minus(totalAllocated).toNumber();
const totalPnlPercent = totalAllocated > 0
? new Decimal(totalPnl).dividedBy(totalAllocated).times(100).toDecimalPlaces(2).toNumber()
: 0;
// Group by agent
const byAgent = new Map<AgentType, { count: number; totalValue: number; pnl: number }>();
for (const allocation of allocations) {
const existing = byAgent.get(allocation.agentType) || { count: 0, totalValue: 0, pnl: 0 };
byAgent.set(allocation.agentType, {
count: existing.count + 1,
totalValue: new Decimal(existing.totalValue).plus(allocation.currentValue).toNumber(),
pnl: new Decimal(existing.pnl).plus(allocation.realizedPnl).plus(allocation.unrealizedPnl).toNumber(),
});
}
return {
totalAllocated,
currentValue,
totalPnl,
totalPnlPercent,
allocationsByAgent: Array.from(byAgent.entries()).map(([agentType, data]) => ({
agentType,
...data,
})),
};
} finally {
client.release();
}
}
async getAgentPerformance(
agentType: AgentType,
tenantId: string,
periodDays = 30
): Promise<AgentPerformanceMetrics> {
const client = await this.pool.connect();
try {
await setTenantContext(client, tenantId);
const agentConfig = await this.getAgentConfigInternal(client, agentType, tenantId);
if (!agentConfig) {
throw new Error(`Agent ${agentType} not configured`);
}
// Get recent profit distributions
const periodStart = new Date();
periodStart.setDate(periodStart.getDate() - periodDays);
const distributionsResult = await client.query<{ totalPnl: number }>(
`SELECT COALESCE(SUM(total_pnl), 0) as "totalPnl"
FROM investment.profit_distributions
WHERE agent_type = $1 AND tenant_id = $2 AND period_end >= $3 AND status = 'COMPLETED'`,
[agentType, tenantId, periodStart]
);
const periodReturn = distributionsResult.rows[0]?.totalPnl || 0;
const periodReturnPercent = agentConfig.totalAum > 0
? new Decimal(periodReturn).dividedBy(agentConfig.totalAum).times(100).toDecimalPlaces(2).toNumber()
: 0;
return {
agentType,
periodDays,
totalAum: agentConfig.totalAum,
totalAllocations: agentConfig.totalAllocations,
periodReturn,
periodReturnPercent,
maxDrawdown: agentConfig.maxDrawdownPercent,
sharpeRatio: null, // Would require more historical data to calculate
winRate: null,
};
} finally {
client.release();
}
}
// ============ Private Helpers ============
private async getAgentConfigInternal(
client: PoolClient,
agentType: AgentType,
tenantId: string
): Promise<AgentConfig | null> {
const result = await client.query<AgentConfig>(
`SELECT id, tenant_id as "tenantId", agent_type as "agentType", name, description,
risk_level as "riskLevel", is_active as "isActive",
min_allocation as "minAllocation", max_allocation as "maxAllocation",
target_return_percent as "targetReturnPercent",
max_drawdown_percent as "maxDrawdownPercent",
management_fee_percent as "managementFeePercent",
performance_fee_percent as "performanceFeePercent",
total_aum as "totalAum", total_allocations as "totalAllocations",
metadata, created_at as "createdAt", updated_at as "updatedAt"
FROM investment.agent_configs
WHERE agent_type = $1 AND tenant_id = $2`,
[agentType, tenantId]
);
return result.rows[0] || null;
}
private async getAllocationInternal(
client: PoolClient,
allocationId: string,
tenantId: string
): Promise<AgentAllocation | null> {
const result = await client.query<AgentAllocation>(
`SELECT id, tenant_id as "tenantId", user_id as "userId", wallet_id as "walletId",
agent_type as "agentType", status, allocated_amount as "allocatedAmount",
current_value as "currentValue", total_deposited as "totalDeposited",
total_withdrawn as "totalWithdrawn", total_profit_distributed as "totalProfitDistributed",
total_fees_paid as "totalFeesPaid", unrealized_pnl as "unrealizedPnl",
realized_pnl as "realizedPnl", last_profit_distribution_at as "lastProfitDistributionAt",
lock_expires_at as "lockExpiresAt", metadata,
created_at as "createdAt", updated_at as "updatedAt"
FROM investment.agent_allocations
WHERE id = $1 AND tenant_id = $2`,
[allocationId, tenantId]
);
return result.rows[0] || null;
}
private async createTransaction(
client: PoolClient,
data: {
allocationId: string;
tenantId: string;
type: AllocationTransactionType;
amount: number;
balanceBefore: number;
balanceAfter: number;
walletTransactionId?: string | null;
description?: string;
metadata?: Record<string, unknown>;
}
): Promise<string> {
const txId = uuidv4();
await client.query(
`INSERT INTO investment.allocation_transactions (
id, tenant_id, allocation_id, type, amount, balance_before, balance_after,
wallet_transaction_id, description, metadata
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
txId,
data.tenantId,
data.allocationId,
data.type,
data.amount,
data.balanceBefore,
data.balanceAfter,
data.walletTransactionId || null,
data.description || null,
data.metadata || {},
]
);
return txId;
}
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: 'AGENT_ALLOCATION',
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 creditWallet(
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}/credit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-Id': tenantId,
},
body: JSON.stringify({
tenantId,
amount,
type,
description,
referenceType: 'AGENT_WITHDRAWAL',
referenceId,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || 'Failed to credit wallet');
}
const result = await response.json();
return result.data.transactionId;
}
}
// Singleton instance
let investmentServiceInstance: InvestmentService | null = null;
export function getInvestmentService(): InvestmentService {
if (!investmentServiceInstance) {
investmentServiceInstance = new InvestmentService();
}
return investmentServiceInstance;
}

31
src/tools/index.ts Normal file
View File

@ -0,0 +1,31 @@
/**
* Tool Registry Index
* Exports all investment tools and handlers
*/
import { investmentToolSchemas, toolHandlers } from './investment';
// Export all tool schemas
export const allToolSchemas = {
...investmentToolSchemas,
};
// 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);
}

513
src/tools/investment.ts Normal file
View File

@ -0,0 +1,513 @@
/**
* Investment MCP Tools
* Agent allocation and profit distribution tools
*/
import { z } from 'zod';
import { getInvestmentService } from '../services/investment.service';
import { McpResponse, AgentType, AllocationStatus } from '../types/investment.types';
import { logger } from '../utils/logger';
// Validation schemas (aligned with DDL enums)
const AgentTypeSchema = z.enum(['ATLAS', 'ORION', 'NOVA']);
const AllocationStatusSchema = z.enum(['pending', 'active', 'paused', 'liquidating', 'closed']);
const CreateAllocationSchema = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid(),
walletId: z.string().uuid(),
agentType: AgentTypeSchema,
amount: z.number().positive(),
metadata: z.record(z.unknown()).optional(),
});
const FundAllocationSchema = z.object({
tenantId: z.string().uuid(),
allocationId: z.string().uuid(),
amount: z.number().positive(),
description: z.string().optional(),
});
const WithdrawAllocationSchema = z.object({
tenantId: z.string().uuid(),
allocationId: z.string().uuid(),
amount: z.number().positive(),
withdrawToWallet: z.boolean().optional(),
description: z.string().optional(),
});
const UpdateStatusSchema = z.object({
tenantId: z.string().uuid(),
allocationId: z.string().uuid(),
status: AllocationStatusSchema,
reason: z.string().optional(),
});
const DistributeProfitsSchema = z.object({
tenantId: z.string().uuid(),
agentType: AgentTypeSchema,
periodStart: z.string().transform((s) => new Date(s)),
periodEnd: z.string().transform((s) => new Date(s)),
totalPnl: z.number(),
});
const ListAllocationsSchema = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid().optional(),
agentType: AgentTypeSchema.optional(),
status: AllocationStatusSchema.optional(),
limit: z.number().int().positive().max(100).optional(),
offset: z.number().int().nonnegative().optional(),
});
// Tool schemas for MCP
export const investmentToolSchemas = {
// Agent Configuration
investment_list_agents: {
name: 'investment_list_agents',
description: 'List all available investment agents (Atlas, Orion, Nova) with their configurations',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
},
required: ['tenantId'],
},
riskLevel: 'LOW',
},
investment_get_agent: {
name: 'investment_get_agent',
description: 'Get specific agent configuration and performance metrics',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
agentType: { type: 'string', enum: ['ATLAS', 'ORION', 'NOVA'], description: 'Agent type' },
},
required: ['tenantId', 'agentType'],
},
riskLevel: 'LOW',
},
// Allocations
investment_create_allocation: {
name: 'investment_create_allocation',
description: 'Create a new allocation to an investment agent. Debits amount from user wallet.',
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 from' },
agentType: { type: 'string', enum: ['ATLAS', 'ORION', 'NOVA'], description: 'Agent to allocate to' },
amount: { type: 'number', minimum: 0, description: 'Amount to allocate' },
metadata: { type: 'object', description: 'Optional metadata' },
},
required: ['tenantId', 'userId', 'walletId', 'agentType', 'amount'],
},
riskLevel: 'HIGH',
},
investment_get_allocation: {
name: 'investment_get_allocation',
description: 'Get details of a specific allocation',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
allocationId: { type: 'string', format: 'uuid', description: 'Allocation ID' },
},
required: ['tenantId', 'allocationId'],
},
riskLevel: 'LOW',
},
investment_list_allocations: {
name: 'investment_list_allocations',
description: 'List allocations with optional filters',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
userId: { type: 'string', format: 'uuid', description: 'Filter by user ID' },
agentType: { type: 'string', enum: ['ATLAS', 'ORION', 'NOVA'], description: 'Filter by agent' },
status: { type: 'string', enum: ['pending', 'active', 'paused', 'liquidating', 'closed'] },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
offset: { type: 'integer', minimum: 0, default: 0 },
},
required: ['tenantId'],
},
riskLevel: 'LOW',
},
investment_get_user_allocations: {
name: 'investment_get_user_allocations',
description: 'Get all allocations for a specific 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',
},
investment_fund_allocation: {
name: 'investment_fund_allocation',
description: 'Add more funds to an existing allocation. Debits from user wallet.',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
allocationId: { type: 'string', format: 'uuid', description: 'Allocation ID' },
amount: { type: 'number', minimum: 0, description: 'Amount to add' },
description: { type: 'string', description: 'Optional description' },
},
required: ['tenantId', 'allocationId', 'amount'],
},
riskLevel: 'HIGH',
},
investment_withdraw: {
name: 'investment_withdraw',
description: 'Withdraw funds from an allocation. Credits to user wallet. Early withdrawal may incur penalty.',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
allocationId: { type: 'string', format: 'uuid', description: 'Allocation ID' },
amount: { type: 'number', minimum: 0, description: 'Amount to withdraw' },
withdrawToWallet: { type: 'boolean', default: true, description: 'Credit to wallet' },
description: { type: 'string', description: 'Optional description' },
},
required: ['tenantId', 'allocationId', 'amount'],
},
riskLevel: 'HIGH',
},
investment_update_status: {
name: 'investment_update_status',
description: 'Update allocation status (pause, resume, close)',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
allocationId: { type: 'string', format: 'uuid', description: 'Allocation ID' },
status: { type: 'string', enum: ['pending', 'active', 'paused', 'liquidating', 'closed'] },
reason: { type: 'string', description: 'Reason for status change' },
},
required: ['tenantId', 'allocationId', 'status'],
},
riskLevel: 'MEDIUM',
},
// Transactions
investment_get_transactions: {
name: 'investment_get_transactions',
description: 'Get transaction history for an allocation',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
allocationId: { type: 'string', format: 'uuid', description: 'Allocation ID' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 50 },
offset: { type: 'integer', minimum: 0, default: 0 },
},
required: ['tenantId', 'allocationId'],
},
riskLevel: 'LOW',
},
// Profit Distribution
investment_distribute_profits: {
name: 'investment_distribute_profits',
description: 'Distribute profits to all allocations for an agent (admin operation)',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
agentType: { type: 'string', enum: ['ATLAS', 'ORION', 'NOVA'], description: 'Agent type' },
periodStart: { type: 'string', format: 'date-time', description: 'Period start date' },
periodEnd: { type: 'string', format: 'date-time', description: 'Period end date' },
totalPnl: { type: 'number', description: 'Total PnL for the period' },
},
required: ['tenantId', 'agentType', 'periodStart', 'periodEnd', 'totalPnl'],
},
riskLevel: 'CRITICAL',
},
investment_get_distributions: {
name: 'investment_get_distributions',
description: 'Get profit distribution history for an agent',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
agentType: { type: 'string', enum: ['ATLAS', 'ORION', 'NOVA'], description: 'Agent type' },
limit: { type: 'integer', minimum: 1, maximum: 24, default: 12 },
},
required: ['tenantId', 'agentType'],
},
riskLevel: 'LOW',
},
// Statistics
investment_get_user_summary: {
name: 'investment_get_user_summary',
description: 'Get investment summary for a user across all agents',
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',
},
investment_get_agent_performance: {
name: 'investment_get_agent_performance',
description: 'Get performance metrics for an agent',
inputSchema: {
type: 'object',
properties: {
tenantId: { type: 'string', format: 'uuid', description: 'Tenant ID' },
agentType: { type: 'string', enum: ['ATLAS', 'ORION', 'NOVA'], description: 'Agent type' },
periodDays: { type: 'integer', minimum: 1, maximum: 365, default: 30 },
},
required: ['tenantId', 'agentType'],
},
riskLevel: 'LOW',
},
};
// Tool handlers
async function handleListAgents(params: unknown): Promise<McpResponse> {
try {
const { tenantId } = z.object({ tenantId: z.string().uuid() }).parse(params);
const service = getInvestmentService();
const agents = await service.listAgentConfigs(tenantId);
return { success: true, data: agents };
} catch (error) {
logger.error('Error listing agents', { error });
return { success: false, error: (error as Error).message, code: 'LIST_AGENTS_ERROR' };
}
}
async function handleGetAgent(params: unknown): Promise<McpResponse> {
try {
const { tenantId, agentType } = z.object({
tenantId: z.string().uuid(),
agentType: AgentTypeSchema,
}).parse(params);
const service = getInvestmentService();
const agent = await service.getAgentConfig(agentType, tenantId);
if (!agent) {
return { success: false, error: 'Agent not found', code: 'AGENT_NOT_FOUND' };
}
return { success: true, data: agent };
} catch (error) {
logger.error('Error getting agent', { error });
return { success: false, error: (error as Error).message, code: 'GET_AGENT_ERROR' };
}
}
async function handleCreateAllocation(params: unknown): Promise<McpResponse> {
try {
const input = CreateAllocationSchema.parse(params);
const service = getInvestmentService();
const allocation = await service.createAllocation(input);
logger.info('Allocation created', { allocationId: allocation.id, agentType: input.agentType });
return { success: true, data: allocation };
} catch (error) {
logger.error('Error creating allocation', { error });
return { success: false, error: (error as Error).message, code: 'CREATE_ALLOCATION_ERROR' };
}
}
async function handleGetAllocation(params: unknown): Promise<McpResponse> {
try {
const { tenantId, allocationId } = z.object({
tenantId: z.string().uuid(),
allocationId: z.string().uuid(),
}).parse(params);
const service = getInvestmentService();
const allocation = await service.getAllocation(allocationId, tenantId);
if (!allocation) {
return { success: false, error: 'Allocation not found', code: 'ALLOCATION_NOT_FOUND' };
}
return { success: true, data: allocation };
} catch (error) {
logger.error('Error getting allocation', { error });
return { success: false, error: (error as Error).message, code: 'GET_ALLOCATION_ERROR' };
}
}
async function handleListAllocations(params: unknown): Promise<McpResponse> {
try {
const filter = ListAllocationsSchema.parse(params);
const service = getInvestmentService();
const allocations = await service.listAllocations(filter);
return { success: true, data: allocations };
} catch (error) {
logger.error('Error listing allocations', { error });
return { success: false, error: (error as Error).message, code: 'LIST_ALLOCATIONS_ERROR' };
}
}
async function handleGetUserAllocations(params: unknown): Promise<McpResponse> {
try {
const { tenantId, userId } = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid(),
}).parse(params);
const service = getInvestmentService();
const allocations = await service.getUserAllocations(userId, tenantId);
return { success: true, data: allocations };
} catch (error) {
logger.error('Error getting user allocations', { error });
return { success: false, error: (error as Error).message, code: 'GET_USER_ALLOCATIONS_ERROR' };
}
}
async function handleFundAllocation(params: unknown): Promise<McpResponse> {
try {
const input = FundAllocationSchema.parse(params);
const service = getInvestmentService();
const allocation = await service.fundAllocation(input);
logger.info('Allocation funded', { allocationId: input.allocationId, amount: input.amount });
return { success: true, data: allocation };
} catch (error) {
logger.error('Error funding allocation', { error });
return { success: false, error: (error as Error).message, code: 'FUND_ALLOCATION_ERROR' };
}
}
async function handleWithdraw(params: unknown): Promise<McpResponse> {
try {
const input = WithdrawAllocationSchema.parse(params);
const service = getInvestmentService();
const allocation = await service.withdrawFromAllocation(input);
logger.info('Withdrawal processed', { allocationId: input.allocationId, amount: input.amount });
return { success: true, data: allocation };
} catch (error) {
logger.error('Error withdrawing from allocation', { error });
return { success: false, error: (error as Error).message, code: 'WITHDRAW_ERROR' };
}
}
async function handleUpdateStatus(params: unknown): Promise<McpResponse> {
try {
const input = UpdateStatusSchema.parse(params);
const service = getInvestmentService();
const allocation = await service.updateAllocationStatus(input);
logger.info('Allocation status updated', { allocationId: input.allocationId, status: input.status });
return { success: true, data: allocation };
} catch (error) {
logger.error('Error updating allocation status', { error });
return { success: false, error: (error as Error).message, code: 'UPDATE_STATUS_ERROR' };
}
}
async function handleGetTransactions(params: unknown): Promise<McpResponse> {
try {
const { tenantId, allocationId, limit, offset } = z.object({
tenantId: z.string().uuid(),
allocationId: z.string().uuid(),
limit: z.number().int().positive().max(100).optional(),
offset: z.number().int().nonnegative().optional(),
}).parse(params);
const service = getInvestmentService();
const transactions = await service.getAllocationTransactions(allocationId, tenantId, limit, offset);
return { success: true, data: transactions };
} catch (error) {
logger.error('Error getting transactions', { error });
return { success: false, error: (error as Error).message, code: 'GET_TRANSACTIONS_ERROR' };
}
}
async function handleDistributeProfits(params: unknown): Promise<McpResponse> {
try {
const input = DistributeProfitsSchema.parse(params);
const service = getInvestmentService();
const distribution = await service.distributeProfits(input);
logger.info('Profits distributed', {
distributionId: distribution.id,
agentType: input.agentType,
totalPnl: input.totalPnl,
});
return { success: true, data: distribution };
} catch (error) {
logger.error('Error distributing profits', { error });
return { success: false, error: (error as Error).message, code: 'DISTRIBUTE_PROFITS_ERROR' };
}
}
async function handleGetDistributions(params: unknown): Promise<McpResponse> {
try {
const { tenantId, agentType, limit } = z.object({
tenantId: z.string().uuid(),
agentType: AgentTypeSchema,
limit: z.number().int().positive().max(24).optional(),
}).parse(params);
const service = getInvestmentService();
const distributions = await service.getProfitDistributions(agentType, tenantId, limit);
return { success: true, data: distributions };
} catch (error) {
logger.error('Error getting distributions', { error });
return { success: false, error: (error as Error).message, code: 'GET_DISTRIBUTIONS_ERROR' };
}
}
async function handleGetUserSummary(params: unknown): Promise<McpResponse> {
try {
const { tenantId, userId } = z.object({
tenantId: z.string().uuid(),
userId: z.string().uuid(),
}).parse(params);
const service = getInvestmentService();
const summary = await service.getUserAllocationSummary(userId, tenantId);
return { success: true, data: summary };
} catch (error) {
logger.error('Error getting user summary', { error });
return { success: false, error: (error as Error).message, code: 'GET_USER_SUMMARY_ERROR' };
}
}
async function handleGetAgentPerformance(params: unknown): Promise<McpResponse> {
try {
const { tenantId, agentType, periodDays } = z.object({
tenantId: z.string().uuid(),
agentType: AgentTypeSchema,
periodDays: z.number().int().positive().max(365).optional(),
}).parse(params);
const service = getInvestmentService();
const performance = await service.getAgentPerformance(agentType, tenantId, periodDays);
return { success: true, data: performance };
} catch (error) {
logger.error('Error getting agent performance', { error });
return { success: false, error: (error as Error).message, code: 'GET_AGENT_PERFORMANCE_ERROR' };
}
}
// Tool handlers map
export const toolHandlers: Record<string, (params: unknown) => Promise<McpResponse>> = {
investment_list_agents: handleListAgents,
investment_get_agent: handleGetAgent,
investment_create_allocation: handleCreateAllocation,
investment_get_allocation: handleGetAllocation,
investment_list_allocations: handleListAllocations,
investment_get_user_allocations: handleGetUserAllocations,
investment_fund_allocation: handleFundAllocation,
investment_withdraw: handleWithdraw,
investment_update_status: handleUpdateStatus,
investment_get_transactions: handleGetTransactions,
investment_distribute_profits: handleDistributeProfits,
investment_get_distributions: handleGetDistributions,
investment_get_user_summary: handleGetUserSummary,
investment_get_agent_performance: handleGetAgentPerformance,
};

View File

@ -0,0 +1,201 @@
/**
* Investment/Agent Allocation Types
*/
// Agent types
export type AgentType = 'ATLAS' | 'ORION' | 'NOVA';
// Allocation status (matches DDL: investment.allocation_status)
export type AllocationStatus = 'pending' | 'active' | 'paused' | 'liquidating' | 'closed';
// Transaction types (matches DDL: investment.allocation_tx_type)
export type AllocationTransactionType =
| 'INITIAL_FUNDING'
| 'ADDITIONAL_FUNDING'
| 'PARTIAL_WITHDRAWAL'
| 'FULL_WITHDRAWAL'
| 'PROFIT_REALIZED'
| 'LOSS_REALIZED'
| 'FEE_CHARGED'
| 'REBALANCE';
// Profit distribution status
export type ProfitDistributionStatus = 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED';
// Agent configuration interface
export interface AgentConfig {
id: string;
tenantId: string;
agentType: AgentType;
name: string;
description: string | null;
riskLevel: string;
isActive: boolean;
minAllocation: number;
maxAllocation: number;
targetReturnPercent: number;
maxDrawdownPercent: number;
managementFeePercent: number;
performanceFeePercent: number;
totalAum: number;
totalAllocations: number;
metadata: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
// Agent allocation interface
export interface AgentAllocation {
id: string;
tenantId: string;
userId: string;
walletId: string;
agentType: AgentType;
status: AllocationStatus;
allocatedAmount: number;
currentValue: number;
totalDeposited: number;
totalWithdrawn: number;
totalProfitDistributed: number;
totalFeesPaid: number;
unrealizedPnl: number;
realizedPnl: number;
lastProfitDistributionAt: Date | null;
lockExpiresAt: Date | null;
metadata: Record<string, unknown>;
createdAt: Date;
updatedAt: Date;
}
// Allocation transaction interface
export interface AllocationTransaction {
id: string;
tenantId: string;
allocationId: string;
type: AllocationTransactionType;
amount: number;
balanceBefore: number;
balanceAfter: number;
walletTransactionId: string | null;
description: string | null;
metadata: Record<string, unknown>;
createdAt: Date;
}
// Profit distribution interface
export interface ProfitDistribution {
id: string;
tenantId: string;
agentType: AgentType;
periodStart: Date;
periodEnd: Date;
status: ProfitDistributionStatus;
totalPnl: number;
managementFees: number;
performanceFees: number;
netDistribution: number;
allocationsCount: number;
processedAt: Date | null;
metadata: Record<string, unknown>;
createdAt: Date;
}
// Allocation with agent info
export interface AllocationWithAgent extends AgentAllocation {
agentName: string;
agentRiskLevel: string;
agentTargetReturn: number;
}
// Create allocation input
export interface CreateAllocationInput {
tenantId: string;
userId: string;
walletId: string;
agentType: AgentType;
amount: number;
metadata?: Record<string, unknown>;
}
// Fund allocation input
export interface FundAllocationInput {
tenantId: string;
allocationId: string;
amount: number;
description?: string;
}
// Withdraw from allocation input
export interface WithdrawAllocationInput {
tenantId: string;
allocationId: string;
amount: number;
withdrawToWallet?: boolean;
description?: string;
}
// Update allocation status input
export interface UpdateAllocationStatusInput {
tenantId: string;
allocationId: string;
status: AllocationStatus;
reason?: string;
}
// Distribute profits input
export interface DistributeProfitsInput {
tenantId: string;
agentType: AgentType;
periodStart: Date;
periodEnd: Date;
totalPnl: number;
}
// Allocation summary
export interface AllocationSummary {
totalAllocated: number;
currentValue: number;
totalPnl: number;
totalPnlPercent: number;
allocationsByAgent: {
agentType: AgentType;
count: number;
totalValue: number;
pnl: number;
}[];
}
// Agent performance metrics
export interface AgentPerformanceMetrics {
agentType: AgentType;
periodDays: number;
totalAum: number;
totalAllocations: number;
periodReturn: number;
periodReturnPercent: number;
maxDrawdown: number;
sharpeRatio: number | null;
winRate: number | null;
}
// Pagination
export interface PaginationParams {
limit?: number;
offset?: number;
}
// List allocations filter
export interface ListAllocationsFilter extends PaginationParams {
tenantId: string;
userId?: string;
agentType?: AgentType;
status?: AllocationStatus;
}
// 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"]
}