Files
makemd/server/src/services/FXHedgingService.ts

202 lines
6.2 KiB
TypeScript
Raw Normal View History

import db from '../config/database';
import { logger } from '../utils/logger';
export interface FXRate {
pair: string; // e.g., 'USD/CNY'
rate: number;
volatility: number; // 波动率
lastUpdated: Date;
}
export interface HedgingOrder {
id: string;
tenantId: string;
pair: string;
amount: number;
strikeRate: number;
expiryDate: Date;
status: 'PENDING' | 'EXECUTED' | 'EXPIRED' | 'CANCELLED';
}
/**
* [BIZ_FIN_03] (Auto FX Hedging Execution)
* [BIZ_FIN_12] (FX Engine)
* @description
*/
export class FXHedgingService {
private static readonly RATES_TABLE = 'cf_fx_rates';
private static readonly HEDGING_TABLE = 'cf_fx_hedging_orders';
/**
*
*/
static async initTable() {
const hasRatesTable = await db.schema.hasTable(this.RATES_TABLE);
if (!hasRatesTable) {
console.log(`📦 Creating ${this.RATES_TABLE} table...`);
await db.schema.createTable(this.RATES_TABLE, (table) => {
table.string('pair', 16).primary();
table.decimal('rate', 10, 4);
table.float('volatility').defaultTo(0.01);
table.timestamp('last_updated').defaultTo(db.fn.now());
});
}
const hasHedgingTable = await db.schema.hasTable(this.HEDGING_TABLE);
if (!hasHedgingTable) {
console.log(`📦 Creating ${this.HEDGING_TABLE} table...`);
await db.schema.createTable(this.HEDGING_TABLE, (table) => {
table.string('id', 64).primary();
table.string('tenant_id', 64).index();
table.string('pair', 16);
table.decimal('amount', 15, 2);
table.decimal('strike_rate', 10, 4);
table.timestamp('expiry_date');
table.string('status', 16).defaultTo('PENDING');
table.string('trace_id', 64);
table.timestamps(true, true);
});
}
}
/**
* (BIZ_FIN_03)
*/
static async getDynamicSafetyMargin(pair: string = 'USD/CNY'): Promise<number> {
try {
const rateInfo = await db(this.RATES_TABLE).where({ pair }).first();
if (!rateInfo) return 0.03;
const baseMargin = 0.02;
const volatilityFactor = rateInfo.volatility * 0.5;
return baseMargin + volatilityFactor;
} catch (err: any) {
return 0.03;
}
}
/**
* (BIZ_FIN_03)
*/
static async autoHedge(params: {
tenantId: string;
pair: string;
amount: number;
traceId: string;
}): Promise<string | null> {
const { tenantId, pair, amount, traceId } = params;
if (amount < 1000) return null;
const rateInfo = await db(this.RATES_TABLE).where({ pair }).first();
const strikeRate = rateInfo ? rateInfo.rate : 7.25;
const hedgingId = `HEDGE-${Date.now()}`;
await db(this.HEDGING_TABLE).insert({
id: hedgingId,
tenant_id: tenantId,
pair,
amount,
strike_rate: strikeRate,
expiry_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
status: 'PENDING',
trace_id: traceId,
created_at: new Date(),
updated_at: new Date()
});
return hedgingId;
}
/**
*
*/
static async updateRates(pair: string, rate: number, volatility: number): Promise<void> {
const existing = await db(this.RATES_TABLE).where({ pair }).first();
if (existing) {
await db(this.RATES_TABLE).where({ pair }).update({ rate, volatility, last_updated: new Date() });
} else {
await db(this.RATES_TABLE).insert({ pair, rate, volatility, last_updated: new Date() });
}
}
/**
* [BIZ_FIN_12]
*/
static async lockRate(tenantId: string, pair: string, amount: number, durationDays: number = 7): Promise<string> {
const rateInfo = await db(this.RATES_TABLE).where({ pair }).first();
if (!rateInfo) throw new Error(`Rate info not found for ${pair}`);
const lockId = `LOCK-${Date.now()}`;
await db(this.HEDGING_TABLE).insert({
id: lockId,
tenant_id: tenantId,
pair,
amount,
strike_rate: rateInfo.rate,
expiry_date: new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000),
status: 'PENDING',
trace_id: `lock-${Date.now()}`,
created_at: new Date(),
updated_at: new Date()
});
return lockId;
}
/**
* [BIZ_FIN_12]
*/
static async calculateGainLoss(tenantId: string): Promise<{ totalGainLoss: number; details: any[] }> {
const executedOrders = await db(this.HEDGING_TABLE).where({ tenant_id: tenantId, status: 'EXECUTED' });
let totalGainLoss = 0;
const details = [];
for (const order of executedOrders) {
const currentRateInfo = await db(this.RATES_TABLE).where({ pair: order.pair }).first();
if (currentRateInfo) {
const gainLoss = (order.strike_rate - currentRateInfo.rate) * order.amount;
totalGainLoss += gainLoss;
details.push({
orderId: order.id,
pair: order.pair,
strikeRate: order.strike_rate,
currentRate: currentRateInfo.rate,
gainLoss: Number(gainLoss.toFixed(2))
});
}
}
return { totalGainLoss: Number(totalGainLoss.toFixed(2)), details };
}
/**
* [BIZ_FIN_12]
*/
static async executeMaturedOrders(tenantId: string) {
logger.info(`[FX] Checking matured orders for Tenant: ${tenantId}`);
const matured = await db(this.HEDGING_TABLE)
.where({ tenant_id: tenantId, status: 'PENDING' })
.andWhere('expiry_date', '<', new Date());
for (const order of matured) {
await db(this.HEDGING_TABLE).where({ id: order.id }).update({
status: 'EXECUTED',
updated_at: new Date()
});
logger.info(`[FX] Executed matured order ${order.id}`);
}
}
/**
* [BIZ_FIN_12] ()
*/
static async syncRates() {
const pairs = ['USD/CNY', 'EUR/USD', 'GBP/USD'];
for (const pair of pairs) {
const baseRate = pair === 'USD/CNY' ? 7.2 : 1.1;
const rate = baseRate + (Math.random() - 0.5) * 0.1;
const volatility = 0.01 + Math.random() * 0.05;
await this.updateRates(pair, rate, volatility);
}
logger.info('[FX] Market rates synchronized.');
}
}