koishi-plugin-monetary-bourse 1.0.0-alpha.1 → 1.0.0
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 +203 -20
- package/package.json +1 -1
- package/readme.md +110 -2
package/lib/index.js
CHANGED
|
@@ -103,8 +103,14 @@ function apply(ctx, config) {
|
|
|
103
103
|
await ctx.database.create("bourse_history", { stockId, price: currentPrice, time: /* @__PURE__ */ new Date() });
|
|
104
104
|
}
|
|
105
105
|
});
|
|
106
|
+
let wasMarketOpen = false;
|
|
106
107
|
ctx.setInterval(async () => {
|
|
107
|
-
|
|
108
|
+
const isOpen = await isMarketOpen();
|
|
109
|
+
if (isOpen && !wasMarketOpen) {
|
|
110
|
+
switchKLinePattern("自动开市");
|
|
111
|
+
}
|
|
112
|
+
wasMarketOpen = isOpen;
|
|
113
|
+
if (!isOpen) return;
|
|
108
114
|
await updatePrice();
|
|
109
115
|
await processPendingTransactions();
|
|
110
116
|
const oneMonthAgo = new Date(Date.now() - 30 * 24 * 3600 * 1e3);
|
|
@@ -250,13 +256,111 @@ function apply(ctx, config) {
|
|
|
250
256
|
return { success: true };
|
|
251
257
|
}
|
|
252
258
|
__name(pay, "pay");
|
|
259
|
+
const kLinePatterns = {
|
|
260
|
+
// 1. 早盘冲高回落:开盘上涨,午后回落
|
|
261
|
+
morningRally: /* @__PURE__ */ __name((p) => {
|
|
262
|
+
if (p < 0.3) return Math.sin(p / 0.3 * Math.PI / 2) * 1;
|
|
263
|
+
return Math.cos((p - 0.3) / 0.7 * Math.PI / 2) * 0.6;
|
|
264
|
+
}, "morningRally"),
|
|
265
|
+
// 2. 早盘低开高走:开盘下跌,之后持续上涨
|
|
266
|
+
vShape: /* @__PURE__ */ __name((p) => {
|
|
267
|
+
if (p < 0.25) return -Math.sin(p / 0.25 * Math.PI / 2) * 0.8;
|
|
268
|
+
return -0.8 + (p - 0.25) / 0.75 * 1.6;
|
|
269
|
+
}, "vShape"),
|
|
270
|
+
// 3. 倒V型:持续上涨后快速下跌
|
|
271
|
+
invertedV: /* @__PURE__ */ __name((p) => {
|
|
272
|
+
if (p < 0.6) return Math.sin(p / 0.6 * Math.PI / 2) * 1;
|
|
273
|
+
return Math.cos((p - 0.6) / 0.4 * Math.PI / 2) * 1;
|
|
274
|
+
}, "invertedV"),
|
|
275
|
+
// 4. 震荡整理:小幅波动,无明显方向
|
|
276
|
+
consolidation: /* @__PURE__ */ __name((p) => {
|
|
277
|
+
return Math.sin(p * Math.PI * 4) * 0.3 + Math.sin(p * Math.PI * 7) * 0.15;
|
|
278
|
+
}, "consolidation"),
|
|
279
|
+
// 5. 阶梯上涨:分段上涨,有回调
|
|
280
|
+
stairUp: /* @__PURE__ */ __name((p) => {
|
|
281
|
+
const step = Math.floor(p * 4);
|
|
282
|
+
const inStep = p * 4 % 1;
|
|
283
|
+
const base = step * 0.25;
|
|
284
|
+
const stepMove = inStep < 0.7 ? Math.sin(inStep / 0.7 * Math.PI / 2) * 0.3 : 0.3 - (inStep - 0.7) / 0.3 * 0.1;
|
|
285
|
+
return base + stepMove;
|
|
286
|
+
}, "stairUp"),
|
|
287
|
+
// 6. 阶梯下跌:分段下跌,有反弹
|
|
288
|
+
stairDown: /* @__PURE__ */ __name((p) => {
|
|
289
|
+
const step = Math.floor(p * 4);
|
|
290
|
+
const inStep = p * 4 % 1;
|
|
291
|
+
const base = -step * 0.25;
|
|
292
|
+
const stepMove = inStep < 0.7 ? -Math.sin(inStep / 0.7 * Math.PI / 2) * 0.3 : -0.3 + (inStep - 0.7) / 0.3 * 0.1;
|
|
293
|
+
return base + stepMove;
|
|
294
|
+
}, "stairDown"),
|
|
295
|
+
// 7. 尾盘拉升:前期平稳,尾盘快速上涨
|
|
296
|
+
lateRally: /* @__PURE__ */ __name((p) => {
|
|
297
|
+
if (p < 0.7) return Math.sin(p / 0.7 * Math.PI * 2) * 0.2;
|
|
298
|
+
return (p - 0.7) / 0.3 * 1;
|
|
299
|
+
}, "lateRally"),
|
|
300
|
+
// 8. 尾盘跳水:前期平稳或上涨,尾盘快速下跌
|
|
301
|
+
lateDive: /* @__PURE__ */ __name((p) => {
|
|
302
|
+
if (p < 0.7) return Math.sin(p / 0.7 * Math.PI / 2) * 0.4;
|
|
303
|
+
return 0.4 - (p - 0.7) / 0.3 * 1.2;
|
|
304
|
+
}, "lateDive"),
|
|
305
|
+
// 9. W底:双底形态
|
|
306
|
+
doubleBottom: /* @__PURE__ */ __name((p) => {
|
|
307
|
+
if (p < 0.25) return -Math.sin(p / 0.25 * Math.PI / 2) * 0.8;
|
|
308
|
+
if (p < 0.5) return -0.8 + Math.sin((p - 0.25) / 0.25 * Math.PI / 2) * 0.5;
|
|
309
|
+
if (p < 0.75) return -0.3 - Math.sin((p - 0.5) / 0.25 * Math.PI / 2) * 0.5;
|
|
310
|
+
return -0.8 + (p - 0.75) / 0.25 * 1.2;
|
|
311
|
+
}, "doubleBottom"),
|
|
312
|
+
// 10. M顶:双顶形态
|
|
313
|
+
doubleTop: /* @__PURE__ */ __name((p) => {
|
|
314
|
+
if (p < 0.25) return Math.sin(p / 0.25 * Math.PI / 2) * 0.8;
|
|
315
|
+
if (p < 0.5) return 0.8 - Math.sin((p - 0.25) / 0.25 * Math.PI / 2) * 0.5;
|
|
316
|
+
if (p < 0.75) return 0.3 + Math.sin((p - 0.5) / 0.25 * Math.PI / 2) * 0.5;
|
|
317
|
+
return 0.8 - (p - 0.75) / 0.25 * 1.2;
|
|
318
|
+
}, "doubleTop"),
|
|
319
|
+
// 11. 单边上涨
|
|
320
|
+
bullish: /* @__PURE__ */ __name((p) => {
|
|
321
|
+
return Math.sin(p * Math.PI / 2) * 0.8 + Math.sin(p * Math.PI * 3) * 0.1;
|
|
322
|
+
}, "bullish"),
|
|
323
|
+
// 12. 单边下跌
|
|
324
|
+
bearish: /* @__PURE__ */ __name((p) => {
|
|
325
|
+
return -Math.sin(p * Math.PI / 2) * 0.8 + Math.sin(p * Math.PI * 3) * 0.1;
|
|
326
|
+
}, "bearish")
|
|
327
|
+
};
|
|
328
|
+
const patternNames = Object.keys(kLinePatterns);
|
|
329
|
+
const patternChineseNames = {
|
|
330
|
+
morningRally: "早盘冲高回落",
|
|
331
|
+
vShape: "V型反转",
|
|
332
|
+
invertedV: "倒V型",
|
|
333
|
+
consolidation: "震荡整理",
|
|
334
|
+
stairUp: "阶梯上涨",
|
|
335
|
+
stairDown: "阶梯下跌",
|
|
336
|
+
lateRally: "尾盘拉升",
|
|
337
|
+
lateDive: "尾盘跳水",
|
|
338
|
+
doubleBottom: "W底(双底)",
|
|
339
|
+
doubleTop: "M顶(双顶)",
|
|
340
|
+
bullish: "单边上涨",
|
|
341
|
+
bearish: "单边下跌"
|
|
342
|
+
};
|
|
343
|
+
let currentDayPattern = patternNames[Math.floor(Math.random() * patternNames.length)];
|
|
344
|
+
let lastPatternSwitchTime = /* @__PURE__ */ new Date();
|
|
345
|
+
let nextPatternSwitchTime = new Date(Date.now() + (1 + Math.random() * 5) * 3600 * 1e3);
|
|
346
|
+
function switchKLinePattern(reason) {
|
|
347
|
+
const oldPattern = currentDayPattern;
|
|
348
|
+
currentDayPattern = patternNames[Math.floor(Math.random() * patternNames.length)];
|
|
349
|
+
const now = /* @__PURE__ */ new Date();
|
|
350
|
+
lastPatternSwitchTime = now;
|
|
351
|
+
const minDuration = 1 * 3600 * 1e3;
|
|
352
|
+
const randomDuration = Math.random() * 5 * 3600 * 1e3;
|
|
353
|
+
nextPatternSwitchTime = new Date(now.getTime() + minDuration + randomDuration);
|
|
354
|
+
logger.info(`${reason}切换K线模型: ${patternChineseNames[oldPattern]}(${oldPattern}) -> ${patternChineseNames[currentDayPattern]}(${currentDayPattern}), 下次随机切换: ${nextPatternSwitchTime.toLocaleString()}`);
|
|
355
|
+
}
|
|
356
|
+
__name(switchKLinePattern, "switchKLinePattern");
|
|
253
357
|
async function updatePrice() {
|
|
254
358
|
let state = (await ctx.database.get("bourse_state", { key: "macro_state" }))[0];
|
|
255
359
|
const now = /* @__PURE__ */ new Date();
|
|
256
360
|
if (state) {
|
|
257
|
-
if (!state.lastCycleStart) state.lastCycleStart = new Date(Date.now() - 24 * 3600 * 1e3);
|
|
361
|
+
if (!state.lastCycleStart) state.lastCycleStart = new Date(Date.now() - 7 * 24 * 3600 * 1e3);
|
|
258
362
|
if (!(state.lastCycleStart instanceof Date)) state.lastCycleStart = new Date(state.lastCycleStart);
|
|
259
|
-
if (!state.endTime) state.endTime = new Date(state.lastCycleStart.getTime() + 24 * 3600 * 1e3);
|
|
363
|
+
if (!state.endTime) state.endTime = new Date(state.lastCycleStart.getTime() + 7 * 24 * 3600 * 1e3);
|
|
260
364
|
if (!(state.endTime instanceof Date)) state.endTime = new Date(state.endTime);
|
|
261
365
|
}
|
|
262
366
|
if (config.enableManualControl) {
|
|
@@ -276,7 +380,10 @@ function apply(ctx, config) {
|
|
|
276
380
|
endTime
|
|
277
381
|
};
|
|
278
382
|
if (!state) await ctx.database.create("bourse_state", newState);
|
|
279
|
-
else
|
|
383
|
+
else {
|
|
384
|
+
const { key, ...updateFields } = newState;
|
|
385
|
+
await ctx.database.set("bourse_state", { key: "macro_state" }, updateFields);
|
|
386
|
+
}
|
|
280
387
|
state = newState;
|
|
281
388
|
}
|
|
282
389
|
}
|
|
@@ -285,14 +392,14 @@ function apply(ctx, config) {
|
|
|
285
392
|
if (!state) {
|
|
286
393
|
needNewState = true;
|
|
287
394
|
} else {
|
|
288
|
-
const endTime = state.endTime || new Date(state.lastCycleStart.getTime() + 24 * 3600 * 1e3);
|
|
395
|
+
const endTime = state.endTime || new Date(state.lastCycleStart.getTime() + 7 * 24 * 3600 * 1e3);
|
|
289
396
|
if (now > endTime) {
|
|
290
397
|
needNewState = true;
|
|
291
398
|
}
|
|
292
399
|
}
|
|
293
400
|
if (needNewState) {
|
|
294
|
-
const durationHours = 24;
|
|
295
|
-
const fluctuation = 0.
|
|
401
|
+
const durationHours = 7 * 24;
|
|
402
|
+
const fluctuation = 0.3;
|
|
296
403
|
const targetRatio = 1 + (Math.random() * 2 - 1) * fluctuation;
|
|
297
404
|
const targetPrice = currentPrice * targetRatio;
|
|
298
405
|
const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
|
|
@@ -310,24 +417,41 @@ function apply(ctx, config) {
|
|
|
310
417
|
if (!state) {
|
|
311
418
|
await ctx.database.create("bourse_state", newState);
|
|
312
419
|
} else {
|
|
313
|
-
|
|
420
|
+
const { key, ...updateFields } = newState;
|
|
421
|
+
await ctx.database.set("bourse_state", { key: "macro_state" }, updateFields);
|
|
314
422
|
}
|
|
315
423
|
state = newState;
|
|
316
424
|
}
|
|
317
425
|
}
|
|
426
|
+
const timeSinceLastSwitch = now.getTime() - lastPatternSwitchTime.getTime();
|
|
427
|
+
const forceSwitchDuration = 30 * 3600 * 1e3;
|
|
428
|
+
if (now >= nextPatternSwitchTime || timeSinceLastSwitch > forceSwitchDuration) {
|
|
429
|
+
switchKLinePattern("随机时间");
|
|
430
|
+
}
|
|
318
431
|
const trend = state.trendFactor * 2;
|
|
319
|
-
const volatility = currentPrice *
|
|
432
|
+
const volatility = currentPrice * 3e-3 * (Math.random() * 2 - 1);
|
|
433
|
+
const dayStart = new Date(now);
|
|
434
|
+
dayStart.setHours(config.openHour, 0, 0, 0);
|
|
435
|
+
const dayEnd = new Date(now);
|
|
436
|
+
dayEnd.setHours(config.closeHour, 0, 0, 0);
|
|
437
|
+
const dayDuration = dayEnd.getTime() - dayStart.getTime();
|
|
438
|
+
const dayElapsed = now.getTime() - dayStart.getTime();
|
|
439
|
+
const dayProgress = Math.max(0, Math.min(1, dayElapsed / dayDuration));
|
|
440
|
+
const dailyAmplitude = state.startPrice * 0.05;
|
|
441
|
+
const patternFn = kLinePatterns[currentDayPattern];
|
|
442
|
+
const prevDayProgress = Math.max(0, (dayElapsed - 2 * 60 * 1e3) / dayDuration);
|
|
443
|
+
const patternDelta = (patternFn(dayProgress) - patternFn(prevDayProgress)) * dailyAmplitude;
|
|
320
444
|
const totalDuration = state.endTime.getTime() - state.lastCycleStart.getTime();
|
|
321
445
|
const elapsed = now.getTime() - state.lastCycleStart.getTime();
|
|
322
446
|
const prevElapsed = elapsed - 2 * 60 * 1e3;
|
|
323
|
-
const waveCount =
|
|
324
|
-
const
|
|
447
|
+
const waveCount = 7;
|
|
448
|
+
const weeklyAmplitude = state.startPrice * 0.08;
|
|
325
449
|
const getWaveValue = /* @__PURE__ */ __name((t) => {
|
|
326
450
|
const progress = t / totalDuration;
|
|
327
|
-
return
|
|
451
|
+
return weeklyAmplitude * (Math.sin(2 * Math.PI * waveCount * progress) * 0.7 + Math.sin(2 * Math.PI * waveCount * 2.5 * progress) * 0.3);
|
|
328
452
|
}, "getWaveValue");
|
|
329
453
|
const waveDelta = getWaveValue(elapsed) - getWaveValue(prevElapsed);
|
|
330
|
-
let newPrice = currentPrice + trend + volatility + waveDelta;
|
|
454
|
+
let newPrice = currentPrice + trend + volatility + patternDelta + waveDelta;
|
|
331
455
|
if (newPrice < 1) newPrice = 1;
|
|
332
456
|
currentPrice = newPrice;
|
|
333
457
|
await ctx.database.create("bourse_history", { stockId, price: newPrice, time: /* @__PURE__ */ new Date() });
|
|
@@ -541,6 +665,7 @@ function apply(ctx, config) {
|
|
|
541
665
|
});
|
|
542
666
|
ctx.command("bourse.admin.market <status>", "设置股市开关状态 (open/close/auto)", { authority: 3 }).action(async ({ session }, status) => {
|
|
543
667
|
if (!["open", "close", "auto"].includes(status)) return "无效状态,请使用 open, close, 或 auto";
|
|
668
|
+
const wasOpen = await isMarketOpen();
|
|
544
669
|
const key = "macro_state";
|
|
545
670
|
const existing = await ctx.database.get("bourse_state", { key });
|
|
546
671
|
if (existing.length === 0) {
|
|
@@ -558,8 +683,18 @@ function apply(ctx, config) {
|
|
|
558
683
|
} else {
|
|
559
684
|
await ctx.database.set("bourse_state", { key }, { marketOpenStatus: status });
|
|
560
685
|
}
|
|
686
|
+
if (status === "open" && !wasOpen) {
|
|
687
|
+
switchKLinePattern("管理员开市");
|
|
688
|
+
wasMarketOpen = true;
|
|
689
|
+
} else if (status === "close") {
|
|
690
|
+
wasMarketOpen = false;
|
|
691
|
+
}
|
|
561
692
|
return `股市状态已设置为: ${status}`;
|
|
562
693
|
});
|
|
694
|
+
ctx.command("stock.pattern", "管理员:强制切换K线模型", { authority: 3 }).action(() => {
|
|
695
|
+
switchKLinePattern("管理员手动");
|
|
696
|
+
return "已切换K线模型。";
|
|
697
|
+
});
|
|
563
698
|
async function renderStockImage(ctx2, data, name2, current, high, low) {
|
|
564
699
|
if (data.length < 2) return "数据不足,无法绘制走势图。";
|
|
565
700
|
const startPrice = data[0].price;
|
|
@@ -741,17 +876,65 @@ function apply(ctx, config) {
|
|
|
741
876
|
ctx.fillStyle = '#999';
|
|
742
877
|
ctx.font = '500 18px "Segoe UI", sans-serif';
|
|
743
878
|
|
|
744
|
-
|
|
879
|
+
// 动态计算标签间隔,防止重叠
|
|
880
|
+
// 使用最长的时间标签来估算宽度
|
|
881
|
+
let maxLabelWidth = 0;
|
|
882
|
+
for (let i = 0; i < times.length; i++) {
|
|
883
|
+
const w = ctx.measureText(times[i]).width;
|
|
884
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
885
|
+
}
|
|
886
|
+
const labelWidth = maxLabelWidth + 40; // 加40px间距确保不重叠
|
|
887
|
+
const availableWidth = width - padding.left - padding.right;
|
|
888
|
+
const maxLabels = Math.max(2, Math.floor(availableWidth / labelWidth));
|
|
889
|
+
const labelCount = Math.min(maxLabels, 5); // 最多显示5个标签
|
|
890
|
+
const timeStep = Math.max(1, Math.ceil(times.length / labelCount));
|
|
891
|
+
|
|
892
|
+
// 选取要绘制的标签索引(均匀分布)
|
|
893
|
+
const labelIndices = [];
|
|
745
894
|
for (let i = 0; i < times.length; i += timeStep) {
|
|
895
|
+
labelIndices.push(i);
|
|
896
|
+
}
|
|
897
|
+
// 确保最后一个点在列表中
|
|
898
|
+
if (labelIndices[labelIndices.length - 1] !== times.length - 1) {
|
|
899
|
+
labelIndices.push(times.length - 1);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// 绘制标签,跳过重叠的
|
|
903
|
+
const drawnLabels = [];
|
|
904
|
+
for (const i of labelIndices) {
|
|
905
|
+
const x = getX(timestamps[i]);
|
|
906
|
+
const textWidth = ctx.measureText(times[i]).width;
|
|
907
|
+
|
|
908
|
+
// 根据textAlign计算实际占用的区域
|
|
909
|
+
let leftEdge, rightEdge;
|
|
910
|
+
if (i === 0) {
|
|
911
|
+
leftEdge = x;
|
|
912
|
+
rightEdge = x + textWidth;
|
|
913
|
+
} else if (i === times.length - 1) {
|
|
914
|
+
leftEdge = x - textWidth;
|
|
915
|
+
rightEdge = x;
|
|
916
|
+
} else {
|
|
917
|
+
leftEdge = x - textWidth / 2;
|
|
918
|
+
rightEdge = x + textWidth / 2;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// 检查是否与已绘制的标签重叠
|
|
922
|
+
let overlaps = false;
|
|
923
|
+
for (const drawn of drawnLabels) {
|
|
924
|
+
// 两个标签之间至少要有15px间隔
|
|
925
|
+
if (!(rightEdge + 15 < drawn.left || leftEdge - 15 > drawn.right)) {
|
|
926
|
+
overlaps = true;
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (overlaps) continue;
|
|
931
|
+
|
|
746
932
|
if (i === 0) ctx.textAlign = 'left';
|
|
747
|
-
else if (i
|
|
933
|
+
else if (i === times.length - 1) ctx.textAlign = 'right';
|
|
748
934
|
else ctx.textAlign = 'center';
|
|
749
935
|
|
|
750
|
-
ctx.fillText(times[i],
|
|
751
|
-
|
|
752
|
-
if ((times.length - 1) % timeStep !== 0) {
|
|
753
|
-
ctx.textAlign = 'right';
|
|
754
|
-
ctx.fillText(times[times.length-1], getX(timestamps[times.length-1]), height - 10);
|
|
936
|
+
ctx.fillText(times[i], x, height - 10);
|
|
937
|
+
drawnLabels.push({ left: leftEdge, right: rightEdge });
|
|
755
938
|
}
|
|
756
939
|
|
|
757
940
|
</script>
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,6 +2,114 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/koishi-plugin-monetary-bourse)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
为 Koishi 提供基于 `monetary` 通用货币系统的股票交易所功能。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
本插件模拟了一个具备**自动宏观调控**、**丰富日内走势形态**和**资金冻结机制**的拟真股票市场。用户可以使用机器人通用的货币(如信用点)进行股票买卖、炒股理财。
|
|
8
|
+
|
|
9
|
+
## ✨ 特性 (Alpha 2 更新)
|
|
10
|
+
|
|
11
|
+
- **📈 拟真 K 线引擎**:
|
|
12
|
+
- 内置 **12 种日内走势形态**(如:早盘冲高回落、V型反转、尾盘拉升、M顶/W底等)。
|
|
13
|
+
- 走势不再是单纯的随机波动,每天自动或随机切换不同的操盘剧本,大幅提升观察乐趣。
|
|
14
|
+
- 结合 **周级波浪** 与 **宏观趋势**,模拟真实市场的多周期叠加效应。
|
|
15
|
+
- **📊 精美 K 线渲染**:使用 Puppeteer 渲染实时/日/周线图,Alpha 2 版本优化了坐标轴标签算法,防止文字重叠,观感更佳。
|
|
16
|
+
- **❄️ 资金冻结机制**:交易采用 T+0 但资金/股票非即时到账模式(根据金额计算冻结时间),增加博弈深度。
|
|
17
|
+
- **🏦 银行联动**:支持与 `koishi-plugin-monetary-bank` 联动,现金不足时自动扣除银行活期存款。
|
|
18
|
+
- **🕹️ 宏观调控**:管理员可手动干预股价目标,或强制切换当天的 K 线剧本。
|
|
19
|
+
|
|
20
|
+
## 更新记录
|
|
21
|
+
|
|
22
|
+
- **1.0.0(正式版)**:
|
|
23
|
+
- 修复:解决长期运行时触发的 `TypeError: cannot modify primary key`(更新 `bourse_state` 时排除主键字段,仅按条件写入非主键字段),提高数据库兼容性与稳定性。
|
|
24
|
+
|
|
25
|
+
- **Alpha.2 (功能增强 — K 线多样化)**:
|
|
26
|
+
- 在 Alpha.1 的基础上,扩展并完善了 K 线引擎,新增 12 种日内走势剧本、周级波浪叠加以及坐标轴标签冲突修复等视觉与体验改进。
|
|
27
|
+
- 该版本引入了更丰富的日内走势(例如早盘冲高回落、V 型反转、尾盘拉升、M 顶/W 底等),并改进了 Puppeteer 渲染逻辑以减少横轴标签重叠。
|
|
28
|
+
- 致命问题说明:长期运行时可能触发 `TypeError: cannot modify primary key`(根因:更新持久化 `bourse_state` 时不小心将主键字段包含在更新负载中,某些数据库实现会拒绝修改主键),该问题在后续修复版本中已处理,请在生产环境中尽量升级至稳定版本。
|
|
29
|
+
|
|
30
|
+
- **Alpha.1 (初始 Alpha 版本)**:
|
|
31
|
+
- 发布基础功能:买卖、资金冻结、挂单、历史行情存储以及 `stock` 家族指令(`stock` / `stock.buy` / `stock.sell` / `stock.my`)。
|
|
32
|
+
- 支持与 `monetary` 与 `monetary-bank` 的基本联动,提供最小可用的股票交易所模拟能力。
|
|
33
|
+
|
|
34
|
+
## 📦 依赖
|
|
35
|
+
|
|
36
|
+
本插件需要以下服务:
|
|
37
|
+
- `database`: 用于存储持仓、历史行情和挂单记录。
|
|
38
|
+
- `puppeteer`: 用于渲染股市行情图。
|
|
39
|
+
- `monetary`: (可选) 用于获取用户货币余额(本插件直接操作数据库表,monetary 插件需安装以建立表结构)。
|
|
40
|
+
|
|
41
|
+
## 🔧 配置项
|
|
42
|
+
|
|
43
|
+
可以在控制台插件配置页进行设置:
|
|
44
|
+
|
|
45
|
+
### 基础设置
|
|
46
|
+
- **currency**: 货币单位名称(默认:`信用点`)。
|
|
47
|
+
- **stockName**: 股票名称(默认:`Koishi股份`)。
|
|
48
|
+
- **initialPrice**: 股票初始价格(默认:`1200`)。
|
|
49
|
+
- **maxHoldings**: 单人最大持仓限制(默认:`100000`)。
|
|
50
|
+
|
|
51
|
+
### 交易时间
|
|
52
|
+
- **openHour**: 每日开市时间(小时,0-23,默认 `8` 点)。
|
|
53
|
+
- **closeHour**: 每日休市时间(小时,0-23,默认 `23` 点)。
|
|
54
|
+
- **marketStatus**: 股市总开关,可选 `open` (强制开启)、`close` (强制关闭)、`auto` (自动按时间)。
|
|
55
|
+
|
|
56
|
+
### 冻结机制
|
|
57
|
+
- **freezeCostPerMinute**: 每多少货币金额计为1分钟冻结时间(默认 `100`)。
|
|
58
|
+
- **minFreezeTime**: 最小冻结时间(分钟,默认 `10`)。
|
|
59
|
+
- **maxFreezeTime**: 最大冻结时间(分钟,默认 `1440` 即24小时)。
|
|
60
|
+
|
|
61
|
+
### 宏观调控
|
|
62
|
+
- **enableManualControl**: 是否开启手动强力调控模式(覆盖自动波动)。
|
|
63
|
+
- **manualTargetPrice**: 手动模式下的目标价格。
|
|
64
|
+
- **manualDuration**: 手动调控周期(小时)。
|
|
65
|
+
|
|
66
|
+
## 🎮 指令说明
|
|
67
|
+
|
|
68
|
+
### 用户指令
|
|
69
|
+
|
|
70
|
+
- **`stock [interval]`**
|
|
71
|
+
- 查看股市行情。
|
|
72
|
+
- 参数 `interval`: 可选 `day` (日线)、`week` (周线),不填默认为实时走势(最近100条)。
|
|
73
|
+
- 示例:`stock` (查看实时), `stock day` (查看日线)。
|
|
74
|
+
|
|
75
|
+
- **`stock.buy <amount>`**
|
|
76
|
+
- 买入股票。
|
|
77
|
+
- 参数 `amount`: 购买股数(整数)。
|
|
78
|
+
- 说明:扣除现金(优先)或银行活期,股票将在冻结时间结束后到账。
|
|
79
|
+
|
|
80
|
+
- **`stock.sell <amount>`**
|
|
81
|
+
- 卖出股票。
|
|
82
|
+
- 参数 `amount`: 卖出股数(整数)。
|
|
83
|
+
- 说明:扣除持仓,获得的资金将在冻结时间结束后到账。
|
|
84
|
+
|
|
85
|
+
- **`stock.my`**
|
|
86
|
+
- 查看我的账户。
|
|
87
|
+
- 显示当前持仓、市值以及正在进行中(冻结中)的买卖订单。
|
|
88
|
+
|
|
89
|
+
### 管理员指令 (权限等级 3)
|
|
90
|
+
|
|
91
|
+
- **`stock.control <price> [hours]`**
|
|
92
|
+
- 设置宏观调控目标。
|
|
93
|
+
- 说明:强行引导股价在指定时间内向目标价格移动。
|
|
94
|
+
- 示例:`stock.control 5000 12` (在12小时内让股价涨/跌到5000)。
|
|
95
|
+
|
|
96
|
+
- **`stock.pattern`** *(Alpha 2 新增)*
|
|
97
|
+
- 强制切换 K 线模型。
|
|
98
|
+
- 说明:手动随机切换当前使用的日内走势剧本(如从“单边下跌”切换为“尾盘拉升”)。
|
|
99
|
+
|
|
100
|
+
- **`bourse.admin.market <status>`**
|
|
101
|
+
- 设置股市开关状态。
|
|
102
|
+
- 参数 `status`: `open` (开启), `close` (关闭), `auto` (自动)。
|
|
103
|
+
- 说明:手动开市时会自动重置并切换一个新的日内 K 线形态。
|
|
104
|
+
|
|
105
|
+
## 💡 常见问题
|
|
106
|
+
|
|
107
|
+
**Q: 为什么买了股票没有立刻到账?**
|
|
108
|
+
A: 本插件设计了基于交易金额的动态冻结机制。交易额越大,冻结时间越长(可配置)。请使用 `stock.my` 查看剩余解冻时间。
|
|
109
|
+
|
|
110
|
+
**Q: 股价是如何波动的?**
|
|
111
|
+
A: 股价由四部分叠加而成:
|
|
112
|
+
1. **宏观趋势**:根据自动或手动的目标价格计算的线性趋势。
|
|
113
|
+
2. **周级波浪**:7天为一个大周期的正弦复合波浪。
|
|
114
|
+
3. **日内形态**:每天随机从 12 种剧本中选择多种(如 V 型、倒 V 型、阶梯上涨等)。
|
|
115
|
+
4. **随机噪音**:微小的随机波动。
|