feat(trading): K线图增加周/月/年周期
- 前端周期选项增加「周」「月」「年」 - 后端 parsePeriodToMinutes 增加 1w/1M/1y 映射 - getPeriodStart 对日历周期按自然边界对齐: - 1w → 周一 00:00 UTC(ISO 周标准) - 1M → 月初1日 00:00 UTC - 1y → 年初1月1日 00:00 UTC - 固定周期(1m~1d)仍使用 epoch 模运算,不受影响 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
c24f383501
commit
59acea33fe
|
|
@ -57,7 +57,7 @@ export class PriceController {
|
|||
name: 'period',
|
||||
required: false,
|
||||
type: String,
|
||||
description: 'K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d',
|
||||
description: 'K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w, 1M, 1y',
|
||||
example: '1h',
|
||||
})
|
||||
@ApiQuery({ name: 'limit', required: false, type: Number, description: '返回数量,默认100' })
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ export class PriceService {
|
|||
* 获取K线数据(OHLC格式)
|
||||
* 将价格快照按周期聚合为K线数据
|
||||
*
|
||||
* @param period K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d
|
||||
* @param period K线周期: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w, 1M, 1y
|
||||
* @param limit 返回数量
|
||||
* @param before 获取此时间之前的K线数据(用于加载更多历史)
|
||||
*/
|
||||
|
|
@ -311,7 +311,7 @@ export class PriceService {
|
|||
}
|
||||
|
||||
// 按周期聚合为K线
|
||||
const klines = this.aggregateToKlines(snapshots, periodMinutes);
|
||||
const klines = this.aggregateToKlines(snapshots, periodMinutes, period);
|
||||
|
||||
// 返回最近的limit条
|
||||
return klines.slice(-limit);
|
||||
|
|
@ -319,6 +319,8 @@ export class PriceService {
|
|||
|
||||
/**
|
||||
* 解析周期字符串为分钟数
|
||||
* 对于日历周期(1w/1M/1y),返回的是近似值,仅用于计算查询的时间范围;
|
||||
* 实际的 K 线分组由 getPeriodStart() 按日历边界对齐。
|
||||
*/
|
||||
private parsePeriodToMinutes(period: string): number {
|
||||
const periodMap: Record<string, number> = {
|
||||
|
|
@ -328,7 +330,10 @@ export class PriceService {
|
|||
'30m': 30,
|
||||
'1h': 60,
|
||||
'4h': 240,
|
||||
'1d': 1440,
|
||||
'1d': 1440, // 1天 = 24h
|
||||
'1w': 10080, // 1周 ≈ 7天(近似值,实际按周一对齐)
|
||||
'1M': 43200, // 1月 ≈ 30天(近似值,实际按自然月对齐)
|
||||
'1y': 525600, // 1年 ≈ 365天(近似值,实际按自然年对齐)
|
||||
};
|
||||
return periodMap[period] || 60;
|
||||
}
|
||||
|
|
@ -347,6 +352,7 @@ export class PriceService {
|
|||
minuteBurnRate: Money;
|
||||
}>,
|
||||
periodMinutes: number,
|
||||
period: string,
|
||||
): Array<{
|
||||
time: string;
|
||||
open: string;
|
||||
|
|
@ -367,7 +373,7 @@ export class PriceService {
|
|||
|
||||
// 按周期分组
|
||||
for (const snapshot of snapshots) {
|
||||
const periodStart = this.getPeriodStart(snapshot.snapshotTime, periodMinutes);
|
||||
const periodStart = this.getPeriodStart(snapshot.snapshotTime, periodMinutes, period);
|
||||
const key = periodStart.getTime();
|
||||
|
||||
if (!klineMap.has(key)) {
|
||||
|
|
@ -418,8 +424,31 @@ export class PriceService {
|
|||
|
||||
/**
|
||||
* 获取周期的起始时间
|
||||
*
|
||||
* 固定周期(1m ~ 1d):基于 Unix epoch 的模运算,确保相同周期的快照分到同一组。
|
||||
* 日历周期(1w / 1M / 1y):按日历边界对齐,使 K 线与自然周/月/年一致:
|
||||
* - 1w:对齐到本周一 00:00 UTC(ISO 周标准)
|
||||
* - 1M:对齐到本月1日 00:00 UTC
|
||||
* - 1y:对齐到本年1月1日 00:00 UTC
|
||||
*/
|
||||
private getPeriodStart(time: Date, periodMinutes: number): Date {
|
||||
private getPeriodStart(time: Date, periodMinutes: number, period: string): Date {
|
||||
if (period === '1w') {
|
||||
// 周K线:对齐到周一 00:00 UTC
|
||||
const d = new Date(Date.UTC(time.getUTCFullYear(), time.getUTCMonth(), time.getUTCDate()));
|
||||
const day = d.getUTCDay(); // 0=Sun, 1=Mon, ..., 6=Sat
|
||||
const diff = day === 0 ? 6 : day - 1; // 周日距周一6天,周一距周一0天
|
||||
d.setUTCDate(d.getUTCDate() - diff);
|
||||
return d;
|
||||
}
|
||||
if (period === '1M') {
|
||||
// 月K线:对齐到月初1日 00:00 UTC
|
||||
return new Date(Date.UTC(time.getUTCFullYear(), time.getUTCMonth(), 1));
|
||||
}
|
||||
if (period === '1y') {
|
||||
// 年K线:对齐到年初1月1日 00:00 UTC
|
||||
return new Date(Date.UTC(time.getUTCFullYear(), 0, 1));
|
||||
}
|
||||
// 固定周期(1m, 5m, 15m, 30m, 1h, 4h, 1d):epoch 模运算
|
||||
const msPerPeriod = periodMinutes * 60 * 1000;
|
||||
const periodStart = Math.floor(time.getTime() / msPerPeriod) * msPerPeriod;
|
||||
return new Date(periodStart);
|
||||
|
|
|
|||
|
|
@ -42,7 +42,8 @@ class _TradingPageState extends ConsumerState<TradingPage> {
|
|||
bool _isEditingAmount = false; // 正在编辑金额(防止循环更新)
|
||||
bool _isFullScreen = false; // K线图全屏状态
|
||||
|
||||
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日'];
|
||||
// K线周期选项:分钟级 → 小时级 → 日/周/月/年
|
||||
final List<String> _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日', '周', '月', '年'];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ final buyEnabledProvider = FutureProvider<bool>((ref) async {
|
|||
// K线周期选择
|
||||
final selectedKlinePeriodProvider = StateProvider<String>((ref) => '1h');
|
||||
|
||||
// 周期字符串映射到API参数
|
||||
// 前端显示文本 → 后端API参数 的映射
|
||||
// 日历周期(1w/1M/1y)在后端按自然周一/月初/年初对齐
|
||||
String _periodToApiParam(String period) {
|
||||
const periodMap = {
|
||||
'1分': '1m',
|
||||
|
|
@ -44,6 +45,9 @@ String _periodToApiParam(String period) {
|
|||
'1时': '1h',
|
||||
'4时': '4h',
|
||||
'日': '1d',
|
||||
'周': '1w', // 周K线,按周一对齐
|
||||
'月': '1M', // 月K线,按自然月对齐
|
||||
'年': '1y', // 年K线,按自然年对齐
|
||||
};
|
||||
return periodMap[period] ?? '1h';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue