From 59acea33fe44e6b2b55e256c0af423e2416bf350 Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 4 Feb 2026 18:42:38 -0800 Subject: [PATCH] =?UTF-8?q?feat(trading):=20K=E7=BA=BF=E5=9B=BE=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=91=A8/=E6=9C=88/=E5=B9=B4=E5=91=A8=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端周期选项增加「周」「月」「年」 - 后端 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 --- .../src/api/controllers/price.controller.ts | 2 +- .../src/application/services/price.service.ts | 39 ++++++++++++++++--- .../pages/trading/trading_page.dart | 3 +- .../providers/trading_providers.dart | 6 ++- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/backend/services/trading-service/src/api/controllers/price.controller.ts b/backend/services/trading-service/src/api/controllers/price.controller.ts index eebe6ffd..5c67c269 100644 --- a/backend/services/trading-service/src/api/controllers/price.controller.ts +++ b/backend/services/trading-service/src/api/controllers/price.controller.ts @@ -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' }) diff --git a/backend/services/trading-service/src/application/services/price.service.ts b/backend/services/trading-service/src/application/services/price.service.ts index 0c89c8c0..437b29cf 100644 --- a/backend/services/trading-service/src/application/services/price.service.ts +++ b/backend/services/trading-service/src/application/services/price.service.ts @@ -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 = { @@ -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); diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index e301640f..56dc26f1 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -42,7 +42,8 @@ class _TradingPageState extends ConsumerState { bool _isEditingAmount = false; // 正在编辑金额(防止循环更新) bool _isFullScreen = false; // K线图全屏状态 - final List _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日']; + // K线周期选项:分钟级 → 小时级 → 日/周/月/年 + final List _timeRanges = ['1分', '5分', '15分', '30分', '1时', '4时', '日', '周', '月', '年']; @override void dispose() { diff --git a/frontend/mining-app/lib/presentation/providers/trading_providers.dart b/frontend/mining-app/lib/presentation/providers/trading_providers.dart index e9298ec0..5a833cc0 100644 --- a/frontend/mining-app/lib/presentation/providers/trading_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/trading_providers.dart @@ -34,7 +34,8 @@ final buyEnabledProvider = FutureProvider((ref) async { // K线周期选择 final selectedKlinePeriodProvider = StateProvider((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'; }