koishi-plugin-monetary-bourse 2.0.0-Alpha.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -107,17 +107,11 @@ function apply(ctx, config) {
107
107
  });
108
108
  let wasMarketOpen = false;
109
109
  let dailyOpenPrice = null;
110
- let macroWaveCount = 7;
111
- let macroWeeklyAmplitudeRatio = 0.08;
112
- let nextMacroSwitchTime = null;
113
110
  let __testNow = null;
114
111
  ctx.setInterval(async () => {
115
112
  const isOpen = await isMarketOpen();
116
113
  if (isOpen && !wasMarketOpen) {
117
- switchKLinePattern("自动开市");
118
114
  dailyOpenPrice = currentPrice;
119
- const hours = 6 + Math.floor(Math.random() * 19);
120
- nextMacroSwitchTime = new Date(Date.now() + hours * 3600 * 1e3);
121
115
  }
122
116
  wasMarketOpen = isOpen;
123
117
  if (!isOpen) return;
@@ -268,101 +262,327 @@ function apply(ctx, config) {
268
262
  }
269
263
  __name(pay, "pay");
270
264
  const kLinePatterns = {
271
- // 1. 早盘冲高回落:开盘上涨,午后回落
272
- morningRally: /* @__PURE__ */ __name((p) => {
273
- if (p < 0.3) return Math.sin(p / 0.3 * Math.PI / 2) * 1;
274
- return Math.cos((p - 0.3) / 0.7 * Math.PI / 2) * 0.6;
275
- }, "morningRally"),
276
- // 2. 早盘低开高走:开盘下跌,之后持续上涨
277
- vShape: /* @__PURE__ */ __name((p) => {
278
- if (p < 0.25) return -Math.sin(p / 0.25 * Math.PI / 2) * 0.8;
279
- return -0.8 + (p - 0.25) / 0.75 * 1.6;
280
- }, "vShape"),
281
- // 3. 倒V型:持续上涨后快速下跌
282
- invertedV: /* @__PURE__ */ __name((p) => {
283
- if (p < 0.6) return Math.sin(p / 0.6 * Math.PI / 2) * 1;
284
- return Math.cos((p - 0.6) / 0.4 * Math.PI / 2) * 1;
285
- }, "invertedV"),
286
- // 4. 震荡整理:小幅波动,无明显方向
287
- consolidation: /* @__PURE__ */ __name((p) => {
288
- return Math.sin(p * Math.PI * 4) * 0.3 + Math.sin(p * Math.PI * 7) * 0.15;
289
- }, "consolidation"),
290
- // 5. 阶梯上涨:分段上涨,有回调
291
- stairUp: /* @__PURE__ */ __name((p) => {
292
- const step = Math.floor(p * 4);
293
- const inStep = p * 4 % 1;
294
- const base = step * 0.25;
295
- const stepMove = inStep < 0.7 ? Math.sin(inStep / 0.7 * Math.PI / 2) * 0.3 : 0.3 - (inStep - 0.7) / 0.3 * 0.1;
296
- return base + stepMove;
297
- }, "stairUp"),
298
- // 6. 阶梯下跌:分段下跌,有反弹
299
- stairDown: /* @__PURE__ */ __name((p) => {
300
- const step = Math.floor(p * 4);
301
- const inStep = p * 4 % 1;
302
- const base = -step * 0.25;
303
- const stepMove = inStep < 0.7 ? -Math.sin(inStep / 0.7 * Math.PI / 2) * 0.3 : -0.3 + (inStep - 0.7) / 0.3 * 0.1;
304
- return base + stepMove;
305
- }, "stairDown"),
306
- // 7. 尾盘拉升:前期平稳,尾盘快速上涨
307
- lateRally: /* @__PURE__ */ __name((p) => {
308
- if (p < 0.7) return Math.sin(p / 0.7 * Math.PI * 2) * 0.2;
309
- return (p - 0.7) / 0.3 * 1;
310
- }, "lateRally"),
311
- // 8. 尾盘跳水:前期平稳或上涨,尾盘快速下跌
312
- lateDive: /* @__PURE__ */ __name((p) => {
313
- if (p < 0.7) return Math.sin(p / 0.7 * Math.PI / 2) * 0.4;
314
- return 0.4 - (p - 0.7) / 0.3 * 1.2;
315
- }, "lateDive"),
316
- // 9. W底:双底形态
317
- doubleBottom: /* @__PURE__ */ __name((p) => {
318
- if (p < 0.25) return -Math.sin(p / 0.25 * Math.PI / 2) * 0.8;
319
- if (p < 0.5) return -0.8 + Math.sin((p - 0.25) / 0.25 * Math.PI / 2) * 0.5;
320
- if (p < 0.75) return -0.3 - Math.sin((p - 0.5) / 0.25 * Math.PI / 2) * 0.5;
321
- return -0.8 + (p - 0.75) / 0.25 * 1.2;
322
- }, "doubleBottom"),
323
- // 10. M顶:双顶形态
324
- doubleTop: /* @__PURE__ */ __name((p) => {
325
- if (p < 0.25) return Math.sin(p / 0.25 * Math.PI / 2) * 0.8;
326
- if (p < 0.5) return 0.8 - Math.sin((p - 0.25) / 0.25 * Math.PI / 2) * 0.5;
327
- if (p < 0.75) return 0.3 + Math.sin((p - 0.5) / 0.25 * Math.PI / 2) * 0.5;
328
- return 0.8 - (p - 0.75) / 0.25 * 1.2;
329
- }, "doubleTop"),
330
- // 11. 单边上涨
331
- bullish: /* @__PURE__ */ __name((p) => {
332
- return Math.sin(p * Math.PI / 2) * 0.8 + Math.sin(p * Math.PI * 3) * 0.1;
333
- }, "bullish"),
334
- // 12. 单边下跌
335
- bearish: /* @__PURE__ */ __name((p) => {
336
- return -Math.sin(p * Math.PI / 2) * 0.8 + Math.sin(p * Math.PI * 3) * 0.1;
337
- }, "bearish")
265
+ // ==================== 看涨模型 (8种) ====================
266
+ bullish_steady: {
267
+ fn: /* @__PURE__ */ __name((p) => Math.sin(p * Math.PI / 2) * 0.8 + Math.sin(p * Math.PI * 3) * 0.08, "fn"),
268
+ category: "bullish",
269
+ name: "单边上涨",
270
+ description: "持续稳健上涨",
271
+ endBias: 0.8
272
+ },
273
+ bullish_v_reversal: {
274
+ fn: /* @__PURE__ */ __name((p) => {
275
+ if (p < 0.25) return -Math.sin(p / 0.25 * Math.PI / 2) * 0.6;
276
+ return -0.6 + (p - 0.25) / 0.75 * 1.4;
277
+ }, "fn"),
278
+ category: "bullish",
279
+ name: "V型反转",
280
+ description: "快速下跌后强势反弹",
281
+ endBias: 0.8
282
+ },
283
+ bullish_stair: {
284
+ fn: /* @__PURE__ */ __name((p) => {
285
+ const step = Math.floor(p * 4);
286
+ const inStep = p * 4 % 1;
287
+ const base = step * 0.22;
288
+ const stepMove = inStep < 0.7 ? Math.sin(inStep / 0.7 * Math.PI / 2) * 0.25 : 0.25 - (inStep - 0.7) / 0.3 * 0.08;
289
+ return base + stepMove;
290
+ }, "fn"),
291
+ category: "bullish",
292
+ name: "阶梯上涨",
293
+ description: "分阶段上涨,每段有小回调",
294
+ endBias: 0.72
295
+ },
296
+ bullish_late_rally: {
297
+ fn: /* @__PURE__ */ __name((p) => {
298
+ if (p < 0.7) return Math.sin(p / 0.7 * Math.PI * 2) * 0.15;
299
+ return (p - 0.7) / 0.3 * 0.9;
300
+ }, "fn"),
301
+ category: "bullish",
302
+ name: "尾盘拉升",
303
+ description: "前期平稳,尾盘急拉",
304
+ endBias: 0.9
305
+ },
306
+ bullish_double_bottom: {
307
+ fn: /* @__PURE__ */ __name((p) => {
308
+ if (p < 0.25) return -Math.sin(p / 0.25 * Math.PI / 2) * 0.5;
309
+ if (p < 0.5) return -0.5 + Math.sin((p - 0.25) / 0.25 * Math.PI / 2) * 0.35;
310
+ if (p < 0.75) return -0.15 - Math.sin((p - 0.5) / 0.25 * Math.PI / 2) * 0.35;
311
+ return -0.5 + (p - 0.75) / 0.25 * 1.1;
312
+ }, "fn"),
313
+ category: "bullish",
314
+ name: "W底突破",
315
+ description: "双底确认后持续上涨",
316
+ endBias: 0.6
317
+ },
318
+ bullish_gap_up: {
319
+ fn: /* @__PURE__ */ __name((p) => {
320
+ if (p < 0.1) return p / 0.1 * 0.4;
321
+ return 0.4 + Math.sin((p - 0.1) / 0.9 * Math.PI / 2) * 0.4 + Math.sin(p * Math.PI * 4) * 0.05;
322
+ }, "fn"),
323
+ category: "bullish",
324
+ name: "跳空高开",
325
+ description: "跳空高开后震荡上行",
326
+ endBias: 0.8
327
+ },
328
+ bullish_three_soldiers: {
329
+ fn: /* @__PURE__ */ __name((p) => {
330
+ const phase = p * 3;
331
+ const segment = Math.floor(phase);
332
+ const inSegment = phase % 1;
333
+ if (segment === 0) return Math.sin(inSegment * Math.PI / 2) * 0.3;
334
+ if (segment === 1) return 0.3 + Math.sin(inSegment * Math.PI / 2) * 0.28;
335
+ return 0.58 + Math.sin(inSegment * Math.PI / 2) * 0.25;
336
+ }, "fn"),
337
+ category: "bullish",
338
+ name: "红三兵",
339
+ description: "连续三段上涨,渐次抬升",
340
+ endBias: 0.75
341
+ },
342
+ bullish_morning_dip: {
343
+ fn: /* @__PURE__ */ __name((p) => {
344
+ if (p < 0.2) return -Math.sin(p / 0.2 * Math.PI / 2) * 0.3;
345
+ return -0.3 + (p - 0.2) / 0.8 * 1.1;
346
+ }, "fn"),
347
+ category: "bullish",
348
+ name: "早盘低开高走",
349
+ description: "早盘低开后持续上涨",
350
+ endBias: 0.8
351
+ },
352
+ // ==================== 看跌模型 (8种) ====================
353
+ bearish_steady: {
354
+ fn: /* @__PURE__ */ __name((p) => -Math.sin(p * Math.PI / 2) * 0.8 + Math.sin(p * Math.PI * 3) * 0.08, "fn"),
355
+ category: "bearish",
356
+ name: "单边下跌",
357
+ description: "持续稳健下跌",
358
+ endBias: -0.8
359
+ },
360
+ bearish_inverted_v: {
361
+ fn: /* @__PURE__ */ __name((p) => {
362
+ if (p < 0.35) return Math.sin(p / 0.35 * Math.PI / 2) * 0.5;
363
+ return 0.5 - (p - 0.35) / 0.65 * 1.3;
364
+ }, "fn"),
365
+ category: "bearish",
366
+ name: "冲高回落",
367
+ description: "快速上涨后深度回落",
368
+ endBias: -0.8
369
+ },
370
+ bearish_stair: {
371
+ fn: /* @__PURE__ */ __name((p) => {
372
+ const step = Math.floor(p * 4);
373
+ const inStep = p * 4 % 1;
374
+ const base = -step * 0.22;
375
+ const stepMove = inStep < 0.7 ? -Math.sin(inStep / 0.7 * Math.PI / 2) * 0.25 : -0.25 + (inStep - 0.7) / 0.3 * 0.08;
376
+ return base + stepMove;
377
+ }, "fn"),
378
+ category: "bearish",
379
+ name: "阶梯下跌",
380
+ description: "分阶段下跌,每段有小反弹",
381
+ endBias: -0.72
382
+ },
383
+ bearish_late_dive: {
384
+ fn: /* @__PURE__ */ __name((p) => {
385
+ if (p < 0.7) return Math.sin(p / 0.7 * Math.PI / 2) * 0.25;
386
+ return 0.25 - (p - 0.7) / 0.3 * 1.15;
387
+ }, "fn"),
388
+ category: "bearish",
389
+ name: "尾盘跳水",
390
+ description: "前期平稳,尾盘急跌",
391
+ endBias: -0.9
392
+ },
393
+ bearish_double_top: {
394
+ fn: /* @__PURE__ */ __name((p) => {
395
+ if (p < 0.25) return Math.sin(p / 0.25 * Math.PI / 2) * 0.5;
396
+ if (p < 0.5) return 0.5 - Math.sin((p - 0.25) / 0.25 * Math.PI / 2) * 0.35;
397
+ if (p < 0.75) return 0.15 + Math.sin((p - 0.5) / 0.25 * Math.PI / 2) * 0.35;
398
+ return 0.5 - (p - 0.75) / 0.25 * 1.1;
399
+ }, "fn"),
400
+ category: "bearish",
401
+ name: "M顶回落",
402
+ description: "双顶确认后持续下跌",
403
+ endBias: -0.6
404
+ },
405
+ bearish_gap_down: {
406
+ fn: /* @__PURE__ */ __name((p) => {
407
+ if (p < 0.1) return -p / 0.1 * 0.4;
408
+ return -0.4 - Math.sin((p - 0.1) / 0.9 * Math.PI / 2) * 0.4 + Math.sin(p * Math.PI * 4) * 0.05;
409
+ }, "fn"),
410
+ category: "bearish",
411
+ name: "跳空低开",
412
+ description: "跳空低开后震荡下行",
413
+ endBias: -0.8
414
+ },
415
+ bearish_three_crows: {
416
+ fn: /* @__PURE__ */ __name((p) => {
417
+ const phase = p * 3;
418
+ const segment = Math.floor(phase);
419
+ const inSegment = phase % 1;
420
+ if (segment === 0) return -Math.sin(inSegment * Math.PI / 2) * 0.3;
421
+ if (segment === 1) return -0.3 - Math.sin(inSegment * Math.PI / 2) * 0.28;
422
+ return -0.58 - Math.sin(inSegment * Math.PI / 2) * 0.25;
423
+ }, "fn"),
424
+ category: "bearish",
425
+ name: "黑三鸦",
426
+ description: "连续三段下跌,渐次走低",
427
+ endBias: -0.75
428
+ },
429
+ bearish_morning_bounce: {
430
+ fn: /* @__PURE__ */ __name((p) => {
431
+ if (p < 0.2) return Math.sin(p / 0.2 * Math.PI / 2) * 0.3;
432
+ return 0.3 - (p - 0.2) / 0.8 * 1.1;
433
+ }, "fn"),
434
+ category: "bearish",
435
+ name: "早盘高开低走",
436
+ description: "早盘高开后持续下跌",
437
+ endBias: -0.8
438
+ },
439
+ // ==================== 中性模型 (9种) ====================
440
+ neutral_consolidation: {
441
+ fn: /* @__PURE__ */ __name((p) => Math.sin(p * Math.PI * 4) * 0.25 + Math.sin(p * Math.PI * 7) * 0.1, "fn"),
442
+ category: "neutral",
443
+ name: "横盘整理",
444
+ description: "窄幅震荡,无明显方向",
445
+ endBias: 0
446
+ },
447
+ neutral_wide_range: {
448
+ fn: /* @__PURE__ */ __name((p) => Math.sin(p * Math.PI * 2) * 0.5 + Math.sin(p * Math.PI * 5) * 0.15, "fn"),
449
+ category: "neutral",
450
+ name: "宽幅震荡",
451
+ description: "大幅波动但最终回归起点",
452
+ endBias: 0
453
+ },
454
+ neutral_converging: {
455
+ fn: /* @__PURE__ */ __name((p) => Math.sin(p * Math.PI * 6) * 0.4 * (1 - p), "fn"),
456
+ category: "neutral",
457
+ name: "收敛三角",
458
+ description: "波动逐渐收窄",
459
+ endBias: 0
460
+ },
461
+ neutral_diverging: {
462
+ fn: /* @__PURE__ */ __name((p) => Math.sin(p * Math.PI * 6) * 0.15 * (1 + p * 2), "fn"),
463
+ category: "neutral",
464
+ name: "发散三角",
465
+ description: "波动逐渐放大",
466
+ endBias: 0
467
+ },
468
+ neutral_box: {
469
+ fn: /* @__PURE__ */ __name((p) => {
470
+ const cycles = 3;
471
+ const phase = p * cycles % 1;
472
+ if (phase < 0.25) return phase / 0.25 * 0.35;
473
+ if (phase < 0.75) return 0.35 - (phase - 0.25) / 0.5 * 0.7;
474
+ return -0.35 + (phase - 0.75) / 0.25 * 0.35;
475
+ }, "fn"),
476
+ category: "neutral",
477
+ name: "箱体震荡",
478
+ description: "在固定区间内来回波动",
479
+ endBias: 0
480
+ },
481
+ neutral_up_down: {
482
+ fn: /* @__PURE__ */ __name((p) => {
483
+ if (p < 0.5) return Math.sin(p / 0.5 * Math.PI / 2) * 0.5;
484
+ return 0.5 - (p - 0.5) / 0.5 * 0.5;
485
+ }, "fn"),
486
+ category: "neutral",
487
+ name: "先涨后跌",
488
+ description: "上涨后回落至起点",
489
+ endBias: 0
490
+ },
491
+ neutral_down_up: {
492
+ fn: /* @__PURE__ */ __name((p) => {
493
+ if (p < 0.5) return -Math.sin(p / 0.5 * Math.PI / 2) * 0.5;
494
+ return -0.5 + (p - 0.5) / 0.5 * 0.5;
495
+ }, "fn"),
496
+ category: "neutral",
497
+ name: "先跌后涨",
498
+ description: "下跌后反弹至起点",
499
+ endBias: 0
500
+ },
501
+ neutral_slight_up: {
502
+ fn: /* @__PURE__ */ __name((p) => p * 0.15 + Math.sin(p * Math.PI * 5) * 0.12, "fn"),
503
+ category: "neutral",
504
+ name: "微涨震荡",
505
+ description: "小幅上涨伴随震荡",
506
+ endBias: 0.15
507
+ },
508
+ neutral_slight_down: {
509
+ fn: /* @__PURE__ */ __name((p) => -p * 0.15 + Math.sin(p * Math.PI * 5) * 0.12, "fn"),
510
+ category: "neutral",
511
+ name: "微跌震荡",
512
+ description: "小幅下跌伴随震荡",
513
+ endBias: -0.15
514
+ }
338
515
  };
339
- const patternNames = Object.keys(kLinePatterns);
340
- const patternChineseNames = {
341
- morningRally: "早盘冲高回落",
342
- vShape: "V型反转",
343
- invertedV: "倒V型",
344
- consolidation: "震荡整理",
345
- stairUp: "阶梯上涨",
346
- stairDown: "阶梯下跌",
347
- lateRally: "尾盘拉升",
348
- lateDive: "尾盘跳水",
349
- doubleBottom: "W底(双底)",
350
- doubleTop: "M顶(双顶)",
351
- bullish: "单边上涨",
352
- bearish: "单边下跌"
516
+ const patternsByCategory = {
517
+ bullish: [],
518
+ bearish: [],
519
+ neutral: []
353
520
  };
354
- let currentDayPattern = patternNames[Math.floor(Math.random() * patternNames.length)];
521
+ for (const [name2, pattern] of Object.entries(kLinePatterns)) {
522
+ patternsByCategory[pattern.category].push(name2);
523
+ }
524
+ const patternNames = Object.keys(kLinePatterns);
525
+ let currentPattern = patternNames[Math.floor(Math.random() * patternNames.length)];
526
+ let patternStartPrice = currentPrice;
355
527
  let lastPatternSwitchTime = /* @__PURE__ */ new Date();
356
528
  let nextPatternSwitchTime = new Date(Date.now() + (1 + Math.random() * 5) * 3600 * 1e3);
357
- function switchKLinePattern(reason) {
358
- const oldPattern = currentDayPattern;
359
- currentDayPattern = patternNames[Math.floor(Math.random() * patternNames.length)];
529
+ function selectPatternByExpectation(expectedPrice, curPrice, cycleProgress) {
530
+ const deviation = (expectedPrice - curPrice) / curPrice;
531
+ let bullishProb = 0.33, bearishProb = 0.33, neutralProb = 0.34;
532
+ const deviationThreshold = 0.05;
533
+ if (Math.abs(deviation) > deviationThreshold) {
534
+ const adjustmentStrength = Math.min(Math.abs(deviation) / 0.3, 1);
535
+ const maxBias = 0.45;
536
+ if (deviation > 0) {
537
+ bullishProb = 0.33 + adjustmentStrength * maxBias;
538
+ bearishProb = 0.33 - adjustmentStrength * maxBias * 0.7;
539
+ neutralProb = 1 - bullishProb - bearishProb;
540
+ } else {
541
+ bearishProb = 0.33 + adjustmentStrength * maxBias;
542
+ bullishProb = 0.33 - adjustmentStrength * maxBias * 0.7;
543
+ neutralProb = 1 - bullishProb - bearishProb;
544
+ }
545
+ } else {
546
+ neutralProb = 0.5;
547
+ bullishProb = 0.25;
548
+ bearishProb = 0.25;
549
+ }
550
+ if (cycleProgress > 0.8) {
551
+ const endBoost = (cycleProgress - 0.8) / 0.2 * 0.2;
552
+ if (deviation > 0) bullishProb += endBoost;
553
+ else if (deviation < 0) bearishProb += endBoost;
554
+ const total = bullishProb + bearishProb + neutralProb;
555
+ bullishProb /= total;
556
+ bearishProb /= total;
557
+ neutralProb /= total;
558
+ }
559
+ const rand = Math.random();
560
+ let category;
561
+ if (rand < bullishProb) category = "bullish";
562
+ else if (rand < bullishProb + bearishProb) category = "bearish";
563
+ else category = "neutral";
564
+ const patterns = patternsByCategory[category];
565
+ const selected = patterns[Math.floor(Math.random() * patterns.length)];
566
+ logger.info(`selectPatternByExpectation: deviation=${(deviation * 100).toFixed(2)}%, selected=${category}/${selected}`);
567
+ return selected;
568
+ }
569
+ __name(selectPatternByExpectation, "selectPatternByExpectation");
570
+ function switchKLinePattern(reason, expectedPrice, cycleProgress) {
571
+ const oldPattern = currentPattern;
572
+ if (expectedPrice !== void 0 && cycleProgress !== void 0) {
573
+ currentPattern = selectPatternByExpectation(expectedPrice, currentPrice, cycleProgress);
574
+ } else {
575
+ currentPattern = patternNames[Math.floor(Math.random() * patternNames.length)];
576
+ }
577
+ patternStartPrice = currentPrice;
360
578
  const now = /* @__PURE__ */ new Date();
361
579
  lastPatternSwitchTime = now;
362
580
  const minDuration = 1 * 3600 * 1e3;
363
581
  const randomDuration = Math.random() * 5 * 3600 * 1e3;
364
582
  nextPatternSwitchTime = new Date(now.getTime() + minDuration + randomDuration);
365
- logger.info(`${reason}切换K线模型: ${patternChineseNames[oldPattern]}(${oldPattern}) -> ${patternChineseNames[currentDayPattern]}(${currentDayPattern}), 下次随机切换: ${nextPatternSwitchTime.toLocaleString()}`);
583
+ const oldInfo = kLinePatterns[oldPattern];
584
+ const newInfo = kLinePatterns[currentPattern];
585
+ logger.info(`${reason}切换K线模型: ${oldInfo?.name || oldPattern} -> ${newInfo.name}(${currentPattern}), 下次随机切换: ${nextPatternSwitchTime.toLocaleString()}`);
366
586
  }
367
587
  __name(switchKLinePattern, "switchKLinePattern");
368
588
  async function updatePrice() {
@@ -407,21 +627,39 @@ function apply(ctx, config) {
407
627
  }, "createAutoState");
408
628
  if (needNewState) {
409
629
  await createAutoState();
410
- } else if (state.mode === "auto" && nextMacroSwitchTime && now >= nextMacroSwitchTime) {
411
- const hours = 6 + Math.floor(Math.random() * 19);
412
- nextMacroSwitchTime = new Date(now.getTime() + hours * 3600 * 1e3);
413
- await createAutoState();
414
- }
415
- const timeSinceLastSwitch = now.getTime() - lastPatternSwitchTime.getTime();
416
- const forceSwitchDuration = 30 * 3600 * 1e3;
417
- if (now >= nextPatternSwitchTime || timeSinceLastSwitch > forceSwitchDuration) {
418
- switchKLinePattern("随机时间");
419
630
  }
420
631
  const basePrice = state.startPrice;
421
632
  const targetPrice = state.targetPrice;
422
633
  const totalDuration = state.endTime.getTime() - state.lastCycleStart.getTime();
423
634
  const elapsed = now.getTime() - state.lastCycleStart.getTime();
424
635
  const cycleProgress = Math.max(0, Math.min(1, elapsed / totalDuration));
636
+ const timeSinceLastSwitch = now.getTime() - lastPatternSwitchTime.getTime();
637
+ const forceSwitchDuration = 30 * 3600 * 1e3;
638
+ if (now >= nextPatternSwitchTime || timeSinceLastSwitch > forceSwitchDuration) {
639
+ switchKLinePattern("随机时间", targetPrice, cycleProgress);
640
+ }
641
+ const patternDuration = nextPatternSwitchTime.getTime() - lastPatternSwitchTime.getTime();
642
+ const patternElapsed = now.getTime() - lastPatternSwitchTime.getTime();
643
+ const patternProgress = Math.max(0, Math.min(1, patternElapsed / patternDuration));
644
+ const pattern = kLinePatterns[currentPattern];
645
+ if (!pattern) {
646
+ logger.warn(`updatePrice: 未知的K线模型 ${currentPattern}`);
647
+ return;
648
+ }
649
+ const patternValue = pattern.fn(patternProgress);
650
+ const prevPatternValue = pattern.fn(Math.max(0, patternProgress - 0.02));
651
+ const patternDelta = patternValue - prevPatternValue;
652
+ const deviation = (targetPrice - currentPrice) / currentPrice;
653
+ const deviationMultiplier = 1 + Math.abs(deviation) * 2;
654
+ const patternReturn = patternDelta * 0.15 * deviationMultiplier;
655
+ const trackPrice = basePrice + (targetPrice - basePrice) * cycleProgress;
656
+ const trackDeviation = (trackPrice - currentPrice) / currentPrice;
657
+ const endPhaseBoost = cycleProgress > 0.8 ? (cycleProgress - 0.8) / 0.2 * 0.05 : 0;
658
+ const reversionStrength = 0.02 + endPhaseBoost;
659
+ const reversionReturn = trackDeviation * reversionStrength;
660
+ const u1 = Math.random();
661
+ const u2 = Math.random();
662
+ const normalRandom = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
425
663
  const dayStart = new Date(now);
426
664
  dayStart.setHours(config.openHour, 0, 0, 0);
427
665
  const dayEnd = new Date(now);
@@ -429,38 +667,17 @@ function apply(ctx, config) {
429
667
  const dayDuration = dayEnd.getTime() - dayStart.getTime();
430
668
  const dayElapsed = now.getTime() - dayStart.getTime();
431
669
  const dayProgress = Math.max(0, Math.min(1, dayElapsed / dayDuration));
432
- const expectedBase = basePrice + (targetPrice - basePrice) * cycleProgress;
433
- const wavePhaseForMean = 2 * Math.PI * macroWaveCount * cycleProgress;
434
- const weeklyAmplitudeRatioForMean = Math.min(Math.max(macroWeeklyAmplitudeRatio, 0.04), 0.12);
435
- const waveMeanBias = Math.sin(wavePhaseForMean) * weeklyAmplitudeRatioForMean;
436
- const expectedPrice = expectedBase * (1 + waveMeanBias);
437
- const deviation = (expectedPrice - currentPrice) / currentPrice;
438
- const meanReversionStrength = 0.05;
439
- const driftReturn = deviation * meanReversionStrength;
440
- const getVolatility = /* @__PURE__ */ __name((progress) => {
441
- const morningVol = Math.exp(-8 * progress);
442
- const afternoonVol = Math.exp(-8 * (1 - progress));
443
- const baseVol = 0.3;
444
- return baseVol + morningVol * 0.5 + afternoonVol * 0.4;
445
- }, "getVolatility");
446
- const volatility = getVolatility(dayProgress);
447
- const u1 = Math.random();
448
- const u2 = Math.random();
449
- const normalRandom = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
450
- const baseVolatilityPerTick = 25e-4;
451
- const randomReturn = normalRandom * baseVolatilityPerTick * volatility;
452
- const patternFn = kLinePatterns[currentDayPattern];
453
- const patternValue = patternFn(dayProgress);
454
- const prevPatternValue = patternFn(Math.max(0, dayProgress - 0.01));
455
- const patternTrend = (patternValue - prevPatternValue) * 0.8;
456
- const patternBias = patternTrend * 8e-3;
457
- const totalReturn = driftReturn + randomReturn + patternBias;
670
+ const morningVol = Math.exp(-8 * dayProgress);
671
+ const afternoonVol = Math.exp(-8 * (1 - dayProgress));
672
+ const volatility = 0.3 + morningVol * 0.5 + afternoonVol * 0.4;
673
+ const randomReturn = normalRandom * 35e-4 * volatility;
674
+ const totalReturn = patternReturn + reversionReturn + randomReturn;
458
675
  let newPrice = currentPrice * (1 + totalReturn);
459
676
  const dayBase = dailyOpenPrice ?? basePrice;
460
677
  const weekUpper = basePrice * 1.5;
461
678
  const weekLower = basePrice * 0.5;
462
- const dayUpper = dayBase * 1.5;
463
- const dayLower = dayBase * 0.5;
679
+ const dayUpper = dayBase * 1.3;
680
+ const dayLower = dayBase * 0.7;
464
681
  const upperLimit = Math.min(weekUpper, dayUpper);
465
682
  const lowerLimit = Math.max(weekLower, dayLower);
466
683
  if (newPrice > upperLimit * 0.95) {
@@ -515,6 +732,20 @@ function apply(ctx, config) {
515
732
  }
516
733
  }
517
734
  __name(processPendingTransactions, "processPendingTransactions");
735
+ async function getPriceHistory(limit = 100) {
736
+ const historyData = await ctx.database.get("bourse_history", {
737
+ stockId
738
+ }, {
739
+ sort: { time: "desc" },
740
+ limit
741
+ });
742
+ return historyData.reverse().map((h2) => ({
743
+ time: new Date(h2.time).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
744
+ price: h2.price,
745
+ timestamp: new Date(h2.time).getTime()
746
+ }));
747
+ }
748
+ __name(getPriceHistory, "getPriceHistory");
518
749
  ctx.command("stock [interval:string]", "查看股市行情").action(async ({ session }, interval) => {
519
750
  if (["buy", "sell", "my"].includes(interval)) {
520
751
  const parts = session.content.trim().split(/\s+/).slice(2);
@@ -606,16 +837,42 @@ function apply(ctx, config) {
606
837
  startTime,
607
838
  endTime
608
839
  });
840
+ const tradeMeta = freezeMinutes === 0 ? { status: "settled", pendingMinutes: 0, pendingEndTime: null } : { status: "pending", pendingMinutes: freezeMinutes, pendingEndTime: endTime.toLocaleString("zh-CN") };
609
841
  if (freezeMinutes === 0) {
610
842
  await processPendingTransactions();
611
- return `交易已完成!
612
- 花费: ${cost.toFixed(2)} ${config.currency}
613
- 股票已到账。`;
843
+ const priceHistory2 = await getPriceHistory();
844
+ const newHoldingData = await ctx.database.get("bourse_holding", { userId: visibleUserId, stockId });
845
+ const newHoldingAmount = newHoldingData.length > 0 ? newHoldingData[0].amount : amount;
846
+ return await renderTradeResultImage(
847
+ ctx,
848
+ "buy",
849
+ config.stockName,
850
+ amount,
851
+ currentPrice,
852
+ cost,
853
+ config.currency,
854
+ priceHistory2,
855
+ void 0,
856
+ newHoldingAmount,
857
+ tradeMeta
858
+ );
614
859
  }
615
- return `交易申请已提交!
616
- 花费: ${cost.toFixed(2)} ${config.currency}
617
- 冻结时间: ${freezeMinutes.toFixed(1)}分钟
618
- 股票将在解冻后到账。`;
860
+ const priceHistory = await getPriceHistory();
861
+ const existingHolding = await ctx.database.get("bourse_holding", { userId: visibleUserId, stockId });
862
+ const projectedHolding = (existingHolding.length > 0 ? existingHolding[0].amount : 0) + amount;
863
+ return await renderTradeResultImage(
864
+ ctx,
865
+ "buy",
866
+ config.stockName,
867
+ amount,
868
+ currentPrice,
869
+ cost,
870
+ config.currency,
871
+ priceHistory,
872
+ void 0,
873
+ projectedHolding,
874
+ tradeMeta
875
+ );
619
876
  });
620
877
  ctx.command("stock.sell <amount:number>", "卖出股票").userFields(["id"]).action(async ({ session }, amount) => {
621
878
  if (!amount || amount <= 0 || !Number.isInteger(amount)) return "请输入有效的卖出股数。";
@@ -676,16 +933,51 @@ function apply(ctx, config) {
676
933
  startTime,
677
934
  endTime
678
935
  });
936
+ const hasCostRecord = existingTotalCost > 0;
937
+ const profit = hasCostRecord ? Number((gain - soldCost).toFixed(2)) : null;
938
+ const profitPercent = hasCostRecord && soldCost > 0 ? Number((profit / soldCost * 100).toFixed(2)) : null;
939
+ const tradeMeta = freezeMinutes === 0 ? { status: "settled", pendingMinutes: 0, pendingEndTime: null } : { status: "pending", pendingMinutes: freezeMinutes, pendingEndTime: endTime.toLocaleString("zh-CN") };
679
940
  if (freezeMinutes === 0) {
680
941
  await processPendingTransactions();
681
- return `卖出已完成!
682
- 收益: ${gain.toFixed(2)} ${config.currency}
683
- 资金已到账。`;
942
+ const priceHistory2 = await getPriceHistory();
943
+ return await renderTradeResultImage(
944
+ ctx,
945
+ "sell",
946
+ config.stockName,
947
+ amount,
948
+ currentPrice,
949
+ gain,
950
+ config.currency,
951
+ priceHistory2,
952
+ {
953
+ avgBuyPrice: hasCostRecord ? avgCostPerShare : null,
954
+ buyCost: hasCostRecord ? soldCost : null,
955
+ profit,
956
+ profitPercent
957
+ },
958
+ void 0,
959
+ tradeMeta
960
+ );
684
961
  }
685
- return `卖出挂单已提交!
686
- 预计收益: ${gain.toFixed(2)} ${config.currency}
687
- 资金冻结: ${freezeMinutes.toFixed(1)}分钟
688
- 资金将在解冻后到账。`;
962
+ const priceHistory = await getPriceHistory();
963
+ return await renderTradeResultImage(
964
+ ctx,
965
+ "sell",
966
+ config.stockName,
967
+ amount,
968
+ currentPrice,
969
+ gain,
970
+ config.currency,
971
+ priceHistory,
972
+ {
973
+ avgBuyPrice: hasCostRecord ? avgCostPerShare : null,
974
+ buyCost: hasCostRecord ? soldCost : null,
975
+ profit,
976
+ profitPercent
977
+ },
978
+ void 0,
979
+ tradeMeta
980
+ );
689
981
  });
690
982
  ctx.command("stock.my", "我的持仓").action(async ({ session }) => {
691
983
  const userId = session.userId;
@@ -870,347 +1162,66 @@ function apply(ctx, config) {
870
1162
  是否继续波动:${moved ? "是" : "否(需检查)"}`;
871
1163
  });
872
1164
  async function renderHoldingImage(ctx2, username, holding, pending, currency) {
873
- const hasCostData = holding && holding.totalCost !== null;
874
- const isProfit = hasCostData ? holding.profit >= 0 : true;
875
- const profitColor = isProfit ? "#d93025" : "#188038";
876
- const profitSign = isProfit ? "+" : "";
877
- const profitSectionHtml = hasCostData ? `
878
- <div class="profit-section" style="background: ${isProfit ? "rgba(217, 48, 37, 0.08)" : "rgba(24, 128, 56, 0.08)"}">
879
- <div class="profit-label">盈亏</div>
880
- <div class="profit-value" style="color: ${profitColor}">
881
- ${profitSign}${holding.profit.toFixed(2)} ${currency}
882
- <span class="profit-percent">(${profitSign}${holding.profitPercent.toFixed(2)}%)</span>
883
- </div>
884
- </div>
885
- ` : `
886
- <div class="profit-section no-data" style="background: rgba(128, 128, 128, 0.08)">
887
- <div class="profit-label">盈亏</div>
888
- <div class="profit-value" style="color: #888">
889
- 暂无成本记录
890
- <span class="profit-hint">(新交易后将自动记录)</span>
891
- </div>
892
- </div>
893
- `;
894
- const holdingHtml = holding ? `
895
- <div class="section">
896
- <div class="section-title">📈 持仓详情</div>
897
- <div class="stock-card">
898
- <div class="stock-header">
899
- <div class="stock-name">${holding.stockName}</div>
900
- <div class="stock-amount">${holding.amount} 股</div>
901
- </div>
902
- <div class="stock-body">
903
- <div class="stat-row">
904
- <div class="stat-item">
905
- <div class="stat-label">现价</div>
906
- <div class="stat-value">${holding.currentPrice.toFixed(2)}</div>
907
- </div>
908
- <div class="stat-item">
909
- <div class="stat-label">成本价</div>
910
- <div class="stat-value">${hasCostData ? holding.avgCost.toFixed(2) : "--"}</div>
911
- </div>
912
- </div>
913
- <div class="stat-row">
914
- <div class="stat-item">
915
- <div class="stat-label">持仓成本</div>
916
- <div class="stat-value">${hasCostData ? holding.totalCost.toFixed(2) : "--"}</div>
917
- </div>
918
- <div class="stat-item">
919
- <div class="stat-label">市值</div>
920
- <div class="stat-value highlight">${holding.marketValue.toFixed(2)}</div>
921
- </div>
922
- </div>
923
- </div>
924
- ${profitSectionHtml}
925
- </div>
926
- </div>
927
- ` : `
928
- <div class="section">
929
- <div class="section-title">📈 持仓详情</div>
930
- <div class="empty-state">
931
- <div class="empty-icon">📭</div>
932
- <div class="empty-text">暂无持仓</div>
933
- </div>
934
- </div>
935
- `;
936
- const pendingHtml = pending.length > 0 ? `
937
- <div class="section">
938
- <div class="section-title">⏳ 进行中的交易</div>
939
- ${pending.map((p) => `
940
- <div class="pending-item ${p.typeClass}">
941
- <div class="pending-left">
942
- <span class="pending-type ${p.typeClass}">${p.type}</span>
943
- <span class="pending-amount">${p.amount} 股</span>
944
- </div>
945
- <div class="pending-center">
946
- <span class="pending-price">单价 ${p.price.toFixed(2)}</span>
947
- <span class="pending-cost">总额 ${p.cost.toFixed(2)}</span>
948
- </div>
949
- <div class="pending-right">
950
- <span class="pending-time">⏱ ${p.timeLeft}</span>
951
- </div>
952
- </div>
953
- `).join("")}
954
- </div>
955
- ` : "";
956
- const html = `
957
- <html>
958
- <head>
959
- <style>
960
- body {
961
- margin: 0;
962
- padding: 20px;
963
- font-family: 'Segoe UI', 'Microsoft YaHei', Roboto, sans-serif;
964
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
965
- width: 450px;
966
- box-sizing: border-box;
967
- }
968
- .card {
969
- background: white;
970
- padding: 25px;
971
- border-radius: 20px;
972
- box-shadow: 0 20px 40px rgba(0,0,0,0.15);
973
- }
974
- .header {
975
- display: flex;
976
- align-items: center;
977
- gap: 12px;
978
- margin-bottom: 20px;
979
- padding-bottom: 15px;
980
- border-bottom: 2px solid #f0f2f5;
981
- }
982
- .avatar {
983
- width: 48px;
984
- height: 48px;
985
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
986
- border-radius: 50%;
987
- display: flex;
988
- align-items: center;
989
- justify-content: center;
990
- color: white;
991
- font-size: 20px;
992
- font-weight: bold;
993
- }
994
- .user-info {
995
- flex: 1;
996
- }
997
- .username {
998
- font-size: 22px;
999
- font-weight: 700;
1000
- color: #1a1a1a;
1001
- }
1002
- .account-label {
1003
- font-size: 13px;
1004
- color: #888;
1005
- margin-top: 2px;
1006
- }
1007
- .section {
1008
- margin-bottom: 20px;
1009
- }
1010
- .section:last-child {
1011
- margin-bottom: 0;
1012
- }
1013
- .section-title {
1014
- font-size: 14px;
1015
- font-weight: 600;
1016
- color: #666;
1017
- margin-bottom: 12px;
1018
- text-transform: uppercase;
1019
- letter-spacing: 0.5px;
1020
- }
1021
- .stock-card {
1022
- background: #f8f9fc;
1023
- border-radius: 16px;
1024
- overflow: hidden;
1025
- }
1026
- .stock-header {
1027
- display: flex;
1028
- justify-content: space-between;
1029
- align-items: center;
1030
- padding: 16px 20px;
1031
- background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
1032
- color: white;
1033
- }
1034
- .stock-name {
1035
- font-size: 18px;
1036
- font-weight: 700;
1037
- }
1038
- .stock-amount {
1039
- font-size: 16px;
1040
- font-weight: 600;
1041
- background: rgba(255,255,255,0.2);
1042
- padding: 4px 12px;
1043
- border-radius: 20px;
1044
- }
1045
- .stock-body {
1046
- padding: 16px 20px;
1047
- }
1048
- .stat-row {
1049
- display: flex;
1050
- justify-content: space-between;
1051
- margin-bottom: 12px;
1052
- }
1053
- .stat-row:last-child {
1054
- margin-bottom: 0;
1055
- }
1056
- .stat-item {
1057
- text-align: center;
1058
- flex: 1;
1059
- }
1060
- .stat-label {
1061
- font-size: 12px;
1062
- color: #888;
1063
- margin-bottom: 4px;
1064
- }
1065
- .stat-value {
1066
- font-size: 18px;
1067
- font-weight: 700;
1068
- color: #333;
1069
- }
1070
- .stat-value.highlight {
1071
- color: #667eea;
1072
- }
1073
- .profit-section {
1074
- display: flex;
1075
- justify-content: space-between;
1076
- align-items: center;
1077
- padding: 16px 20px;
1078
- border-top: 1px solid #eee;
1079
- }
1080
- .profit-label {
1081
- font-size: 14px;
1082
- font-weight: 600;
1083
- color: #666;
1084
- }
1085
- .profit-value {
1086
- font-size: 22px;
1087
- font-weight: 800;
1088
- }
1089
- .profit-percent {
1090
- font-size: 14px;
1091
- font-weight: 600;
1092
- margin-left: 6px;
1093
- }
1094
- .profit-hint {
1095
- font-size: 12px;
1096
- font-weight: 400;
1097
- display: block;
1098
- margin-top: 4px;
1099
- }
1100
- .profit-section.no-data .profit-value {
1101
- font-size: 16px;
1102
- font-weight: 600;
1103
- }
1104
- .empty-state {
1105
- background: #f8f9fc;
1106
- border-radius: 16px;
1107
- padding: 40px 20px;
1108
- text-align: center;
1109
- }
1110
- .empty-icon {
1111
- font-size: 48px;
1112
- margin-bottom: 12px;
1113
- }
1114
- .empty-text {
1115
- font-size: 16px;
1116
- color: #888;
1117
- }
1118
- .pending-item {
1119
- display: flex;
1120
- justify-content: space-between;
1121
- align-items: center;
1122
- background: #f8f9fc;
1123
- border-radius: 12px;
1124
- padding: 14px 16px;
1125
- margin-bottom: 10px;
1126
- border-left: 4px solid #ccc;
1127
- }
1128
- .pending-item.buy {
1129
- border-left-color: #d93025;
1130
- }
1131
- .pending-item.sell {
1132
- border-left-color: #188038;
1133
- }
1134
- .pending-item:last-child {
1135
- margin-bottom: 0;
1136
- }
1137
- .pending-left {
1138
- display: flex;
1139
- align-items: center;
1140
- gap: 10px;
1141
- }
1142
- .pending-type {
1143
- font-size: 12px;
1144
- font-weight: 700;
1145
- padding: 3px 8px;
1146
- border-radius: 6px;
1147
- color: white;
1148
- }
1149
- .pending-type.buy {
1150
- background: #d93025;
1151
- }
1152
- .pending-type.sell {
1153
- background: #188038;
1154
- }
1155
- .pending-amount {
1156
- font-size: 15px;
1157
- font-weight: 600;
1158
- color: #333;
1159
- }
1160
- .pending-center {
1161
- display: flex;
1162
- flex-direction: column;
1163
- align-items: center;
1164
- gap: 2px;
1165
- }
1166
- .pending-price, .pending-cost {
1167
- font-size: 12px;
1168
- color: #666;
1169
- }
1170
- .pending-right {
1171
- text-align: right;
1172
- }
1173
- .pending-time {
1174
- font-size: 13px;
1175
- font-weight: 600;
1176
- color: #f39c12;
1177
- }
1178
- .footer {
1179
- margin-top: 20px;
1180
- padding-top: 15px;
1181
- border-top: 1px solid #f0f2f5;
1182
- text-align: center;
1183
- font-size: 11px;
1184
- color: #bbb;
1185
- }
1186
- </style>
1187
- </head>
1188
- <body>
1189
- <div class="card">
1190
- <div class="header">
1191
- <div class="avatar">${username.charAt(0).toUpperCase()}</div>
1192
- <div class="user-info">
1193
- <div class="username">${username}</div>
1194
- <div class="account-label">股票账户</div>
1195
- </div>
1196
- </div>
1197
- ${holdingHtml}
1198
- ${pendingHtml}
1199
- <div class="footer">
1200
- 数据更新于 ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}
1201
- </div>
1202
- </div>
1203
- </body>
1204
- </html>
1205
- `;
1165
+ const fs2 = require("fs");
1166
+ const path = require("path");
1167
+ const templatePath = path.join(__dirname, "templates", "holding-card.html");
1168
+ let template = fs2.readFileSync(templatePath, "utf-8");
1169
+ const data = {
1170
+ username,
1171
+ holding,
1172
+ pending,
1173
+ currency,
1174
+ updateTime: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN")
1175
+ };
1176
+ template = template.replace("{{DATA}}", JSON.stringify(data));
1206
1177
  const page = await ctx2.puppeteer.page();
1207
- await page.setContent(html);
1178
+ await page.setContent(template);
1208
1179
  const element = await page.$(".card");
1209
1180
  const imgBuf = await element?.screenshot({ encoding: "binary" });
1210
1181
  await page.close();
1211
1182
  return import_koishi.h.image(imgBuf, "image/png");
1212
1183
  }
1213
1184
  __name(renderHoldingImage, "renderHoldingImage");
1185
+ async function renderTradeResultImage(ctx2, tradeType, stockName, amount, tradePrice, totalCost, currency, priceHistory, sellInfo, newHolding, tradeMeta) {
1186
+ const fs2 = require("fs");
1187
+ const path = require("path");
1188
+ const templatePath = path.join(__dirname, "templates", "trade-result.html");
1189
+ let template = fs2.readFileSync(templatePath, "utf-8");
1190
+ const tradeIndex = priceHistory.length - 1;
1191
+ const status = tradeMeta?.status ?? "settled";
1192
+ const pendingMinutes = tradeMeta?.pendingMinutes ?? 0;
1193
+ const pendingEndTime = tradeMeta?.pendingEndTime ?? null;
1194
+ const data = {
1195
+ tradeType,
1196
+ stockName,
1197
+ amount,
1198
+ tradePrice,
1199
+ totalCost,
1200
+ currency,
1201
+ tradeTime: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN"),
1202
+ prices: priceHistory.map((d) => d.price),
1203
+ timestamps: priceHistory.map((d) => d.timestamp),
1204
+ tradeIndex,
1205
+ // 卖出额外信息
1206
+ avgBuyPrice: sellInfo?.avgBuyPrice ?? null,
1207
+ buyCost: sellInfo?.buyCost ?? null,
1208
+ profit: sellInfo?.profit ?? null,
1209
+ profitPercent: sellInfo?.profitPercent ?? null,
1210
+ // 买入后持仓
1211
+ newHolding: newHolding ?? amount,
1212
+ status,
1213
+ pendingMinutes,
1214
+ pendingEndTime
1215
+ };
1216
+ template = template.replace("{{DATA}}", JSON.stringify(data));
1217
+ const page = await ctx2.puppeteer.page();
1218
+ await page.setContent(template);
1219
+ const element = await page.$(".card");
1220
+ const imgBuf = await element?.screenshot({ encoding: "binary" });
1221
+ await page.close();
1222
+ return import_koishi.h.image(imgBuf, "image/png");
1223
+ }
1224
+ __name(renderTradeResultImage, "renderTradeResultImage");
1214
1225
  async function renderStockImage(ctx2, data, name2, viewLabel, current, high, low) {
1215
1226
  if (data.length < 2) return "数据不足,无法绘制走势图。";
1216
1227
  const startPrice = data[0].price;