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:
commit
ce711aa6d4
26
.env.example
Normal file
26
.env.example
Normal 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
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build && npm prune --production
|
||||
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN addgroup -g 1001 -S nodejs && adduser -S 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
82
README.md
Normal 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
37
package.json
Normal 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
103
src/config.ts
Normal 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
298
src/index.ts
Normal 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;
|
||||
114
src/middleware/auth.middleware.ts
Normal file
114
src/middleware/auth.middleware.ts
Normal 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
1
src/middleware/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { authMiddleware, optionalAuthMiddleware, adminMiddleware } from './auth.middleware';
|
||||
997
src/services/investment.service.ts
Normal file
997
src/services/investment.service.ts
Normal 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
31
src/tools/index.ts
Normal 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
513
src/tools/investment.ts
Normal 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,
|
||||
};
|
||||
201
src/types/investment.types.ts
Normal file
201
src/types/investment.types.ts
Normal 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
38
src/utils/logger.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Winston Logger Configuration
|
||||
*/
|
||||
|
||||
import winston from 'winston';
|
||||
import { serverConfig } from '../config';
|
||||
|
||||
const logFormat = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||
winston.format.errors({ stack: true }),
|
||||
winston.format.printf(({ level, message, timestamp, ...meta }) => {
|
||||
const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : '';
|
||||
return `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}`;
|
||||
})
|
||||
);
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
level: serverConfig.logLevel,
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.colorize(),
|
||||
logFormat
|
||||
),
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Log unhandled rejections
|
||||
process.on('unhandledRejection', (reason: unknown) => {
|
||||
logger.error('Unhandled Rejection:', { reason });
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error: Error) => {
|
||||
logger.error('Uncaught Exception:', { error: error.message, stack: error.stack });
|
||||
process.exit(1);
|
||||
});
|
||||
19
tsconfig.json
Normal file
19
tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user