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 +488 -477
- package/lib/templates/holding-card.html +379 -0
- package/lib/templates/stock-chart.html +2 -2
- package/lib/templates/trade-result.html +560 -0
- package/package.json +4 -1
- package/readme.md +78 -95
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
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
|
358
|
-
const
|
|
359
|
-
|
|
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
|
-
|
|
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
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
const
|
|
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.
|
|
463
|
-
const dayLower = dayBase * 0.
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
|
874
|
-
const
|
|
875
|
-
const
|
|
876
|
-
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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(
|
|
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;
|