rwadurian/backend/services/trading-service/src/application/services/price.service.ts

386 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger } from '@nestjs/common';
import { TradingCalculatorService } from '../../domain/services/trading-calculator.service';
import { BlackHoleRepository } from '../../infrastructure/persistence/repositories/black-hole.repository';
import { SharePoolRepository } from '../../infrastructure/persistence/repositories/share-pool.repository';
import { CirculationPoolRepository } from '../../infrastructure/persistence/repositories/circulation-pool.repository';
import { PriceSnapshotRepository } from '../../infrastructure/persistence/repositories/price-snapshot.repository';
import { TradingConfigRepository } from '../../infrastructure/persistence/repositories/trading-config.repository';
import { Money } from '../../domain/value-objects/money.vo';
import Decimal from 'decimal.js';
export interface PriceInfo {
price: string;
greenPoints: string;
blackHoleAmount: string;
circulationPool: string;
effectiveDenominator: string;
burnMultiplier: string;
minuteBurnRate: string;
snapshotTime: Date;
}
@Injectable()
export class PriceService {
private readonly logger = new Logger(PriceService.name);
private readonly calculator = new TradingCalculatorService();
constructor(
private readonly blackHoleRepository: BlackHoleRepository,
private readonly sharePoolRepository: SharePoolRepository,
private readonly circulationPoolRepository: CirculationPoolRepository,
private readonly priceSnapshotRepository: PriceSnapshotRepository,
private readonly tradingConfigRepository: TradingConfigRepository,
) {}
/**
* 获取当前价格信息
*/
async getCurrentPrice(): Promise<PriceInfo> {
const [sharePool, blackHole, circulationPool, config] = await Promise.all([
this.sharePoolRepository.getPool(),
this.blackHoleRepository.getBlackHole(),
this.circulationPoolRepository.getPool(),
this.tradingConfigRepository.getConfig(),
]);
const greenPoints = sharePool?.greenPoints || Money.zero();
const blackHoleAmount = blackHole?.totalBurned || Money.zero();
const circulationPoolAmount = circulationPool?.totalShares || Money.zero();
// 计算价格
const price = this.calculator.calculatePrice(greenPoints, blackHoleAmount, circulationPoolAmount);
// 计算有效分母
const effectiveDenominator = this.calculator.calculateEffectiveDenominator(
blackHoleAmount,
circulationPoolAmount,
);
// 计算销毁倍数
const burnMultiplier = this.calculator.calculateSellBurnMultiplier(
blackHoleAmount,
circulationPoolAmount,
);
// 获取当前每分钟销毁率
const minuteBurnRate = config?.minuteBurnRate || Money.zero();
return {
price: price.toFixed(18),
greenPoints: greenPoints.toFixed(8),
blackHoleAmount: blackHoleAmount.toFixed(8),
circulationPool: circulationPoolAmount.toFixed(8),
effectiveDenominator: effectiveDenominator.toFixed(8),
burnMultiplier: burnMultiplier.toFixed(18),
minuteBurnRate: minuteBurnRate.toFixed(18),
snapshotTime: new Date(),
};
}
/**
* 获取当前销毁倍数
*/
async getCurrentBurnMultiplier(): Promise<Decimal> {
const [blackHole, circulationPool] = await Promise.all([
this.blackHoleRepository.getBlackHole(),
this.circulationPoolRepository.getPool(),
]);
const blackHoleAmount = blackHole?.totalBurned || Money.zero();
const circulationPoolAmount = circulationPool?.totalShares || Money.zero();
return this.calculator.calculateSellBurnMultiplier(blackHoleAmount, circulationPoolAmount);
}
/**
* 计算卖出销毁量
*/
async calculateSellBurn(sellQuantity: Money): Promise<{
burnQuantity: Money;
burnMultiplier: Decimal;
effectiveQuantity: Money;
}> {
const burnMultiplier = await this.getCurrentBurnMultiplier();
const burnQuantity = this.calculator.calculateSellBurnAmount(sellQuantity, burnMultiplier);
const effectiveQuantity = new Money(sellQuantity.value.plus(burnQuantity.value));
return {
burnQuantity,
burnMultiplier,
effectiveQuantity,
};
}
/**
* 计算卖出交易额
*/
async calculateSellAmount(sellQuantity: Money): Promise<{
amount: Money;
burnQuantity: Money;
effectiveQuantity: Money;
price: Money;
}> {
const priceInfo = await this.getCurrentPrice();
const price = new Money(priceInfo.price);
const { burnQuantity, effectiveQuantity } = await this.calculateSellBurn(sellQuantity);
const amount = this.calculator.calculateSellAmount(sellQuantity, burnQuantity, price);
return {
amount,
burnQuantity,
effectiveQuantity,
price,
};
}
/**
* 创建价格快照
*/
async createSnapshot(): Promise<void> {
try {
const [sharePool, blackHole, circulationPool, config] = await Promise.all([
this.sharePoolRepository.getPool(),
this.blackHoleRepository.getBlackHole(),
this.circulationPoolRepository.getPool(),
this.tradingConfigRepository.getConfig(),
]);
const greenPoints = sharePool?.greenPoints || Money.zero();
const blackHoleAmount = blackHole?.totalBurned || Money.zero();
const circulationPoolAmount = circulationPool?.totalShares || Money.zero();
const price = this.calculator.calculatePrice(greenPoints, blackHoleAmount, circulationPoolAmount);
const effectiveDenominator = this.calculator.calculateEffectiveDenominator(
blackHoleAmount,
circulationPoolAmount,
);
const minuteBurnRate = config?.minuteBurnRate || Money.zero();
const snapshotTime = new Date();
snapshotTime.setSeconds(0, 0);
await this.priceSnapshotRepository.createSnapshot({
snapshotTime,
price,
greenPoints,
blackHoleAmount,
circulationPool: circulationPoolAmount,
effectiveDenominator,
minuteBurnRate,
});
this.logger.debug(`Price snapshot created: ${price.toFixed(18)}`);
} catch (error) {
this.logger.error('Failed to create price snapshot', error);
}
}
/**
* 获取价格历史
*/
async getPriceHistory(
startTime: Date,
endTime: Date,
limit: number = 1440,
): Promise<
Array<{
time: Date;
price: string;
greenPoints: string;
blackHoleAmount: string;
circulationPool: string;
}>
> {
const snapshots = await this.priceSnapshotRepository.getPriceHistory(startTime, endTime, limit);
return snapshots.map((s) => ({
time: s.snapshotTime,
price: s.price.toFixed(18),
greenPoints: s.greenPoints.toFixed(8),
blackHoleAmount: s.blackHoleAmount.toFixed(8),
circulationPool: s.circulationPool.toFixed(8),
}));
}
/**
* 获取最新价格快照
*/
async getLatestSnapshot(): Promise<PriceInfo | null> {
const snapshot = await this.priceSnapshotRepository.getLatestSnapshot();
if (!snapshot) {
return null;
}
const burnMultiplier = await this.getCurrentBurnMultiplier();
return {
price: snapshot.price.toFixed(18),
greenPoints: snapshot.greenPoints.toFixed(8),
blackHoleAmount: snapshot.blackHoleAmount.toFixed(8),
circulationPool: snapshot.circulationPool.toFixed(8),
effectiveDenominator: snapshot.effectiveDenominator.toFixed(8),
burnMultiplier: burnMultiplier.toFixed(18),
minuteBurnRate: snapshot.minuteBurnRate.toFixed(18),
snapshotTime: snapshot.snapshotTime,
};
}
/**
* 获取K线数据OHLC格式
* 将价格快照按周期聚合为K线数据
*
* @param period K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d
* @param limit 返回数量
* @param before 获取此时间之前的K线数据用于加载更多历史
*/
async getKlines(
period: string = '1h',
limit: number = 100,
before?: Date,
): Promise<
Array<{
time: string;
open: string;
high: string;
low: string;
close: string;
volume: string;
}>
> {
// 解析周期为分钟数
const periodMinutes = this.parsePeriodToMinutes(period);
// 计算时间范围
// 如果指定了before则获取before之前的数据否则获取到当前时间的数据
const endTime = before ? before : new Date();
const startTime = new Date(endTime.getTime() - periodMinutes * limit * 60 * 1000);
// 获取原始快照数据
const snapshots = await this.priceSnapshotRepository.getPriceHistory(
startTime,
endTime,
periodMinutes * limit, // 获取足够多的数据点
);
if (snapshots.length === 0) {
return [];
}
// 按周期聚合为K线
const klines = this.aggregateToKlines(snapshots, periodMinutes);
// 返回最近的limit条
return klines.slice(-limit);
}
/**
* 解析周期字符串为分钟数
*/
private parsePeriodToMinutes(period: string): number {
const periodMap: Record<string, number> = {
'1m': 1,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'4h': 240,
'1d': 1440,
};
return periodMap[period] || 60;
}
/**
* 将快照数据聚合为K线
*/
private aggregateToKlines(
snapshots: Array<{
snapshotTime: Date;
price: Money;
greenPoints: Money;
blackHoleAmount: Money;
circulationPool: Money;
effectiveDenominator: Money;
minuteBurnRate: Money;
}>,
periodMinutes: number,
): Array<{
time: string;
open: string;
high: string;
low: string;
close: string;
volume: string;
}> {
if (snapshots.length === 0) return [];
const klineMap = new Map<
number,
{
time: Date;
prices: Decimal[];
}
>();
// 按周期分组
for (const snapshot of snapshots) {
const periodStart = this.getPeriodStart(snapshot.snapshotTime, periodMinutes);
const key = periodStart.getTime();
if (!klineMap.has(key)) {
klineMap.set(key, {
time: periodStart,
prices: [],
});
}
klineMap.get(key)!.prices.push(snapshot.price.value);
}
// 转换为K线格式
const klines: Array<{
time: string;
open: string;
high: string;
low: string;
close: string;
volume: string;
}> = [];
const sortedKeys = Array.from(klineMap.keys()).sort((a, b) => a - b);
for (const key of sortedKeys) {
const data = klineMap.get(key)!;
const prices = data.prices;
if (prices.length === 0) continue;
const open = prices[0];
const close = prices[prices.length - 1];
const high = Decimal.max(...prices);
const low = Decimal.min(...prices);
klines.push({
time: data.time.toISOString(),
open: open.toFixed(18),
high: high.toFixed(18),
low: low.toFixed(18),
close: close.toFixed(18),
volume: '0', // 该系统无交易量概念
});
}
return klines;
}
/**
* 获取周期的起始时间
*/
private getPeriodStart(time: Date, periodMinutes: number): Date {
const msPerPeriod = periodMinutes * 60 * 1000;
const periodStart = Math.floor(time.getTime() / msPerPeriod) * msPerPeriod;
return new Date(periodStart);
}
}