386 lines
11 KiB
TypeScript
386 lines
11 KiB
TypeScript
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);
|
||
}
|
||
}
|