koishi-plugin-monetary-bourse 1.0.0-alpha.1 → 1.0.0-alpha.2
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 +197 -18
- package/package.json +1 -1
- package/readme.md +86 -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) {
|
|
@@ -285,14 +389,14 @@ function apply(ctx, config) {
|
|
|
285
389
|
if (!state) {
|
|
286
390
|
needNewState = true;
|
|
287
391
|
} else {
|
|
288
|
-
const endTime = state.endTime || new Date(state.lastCycleStart.getTime() + 24 * 3600 * 1e3);
|
|
392
|
+
const endTime = state.endTime || new Date(state.lastCycleStart.getTime() + 7 * 24 * 3600 * 1e3);
|
|
289
393
|
if (now > endTime) {
|
|
290
394
|
needNewState = true;
|
|
291
395
|
}
|
|
292
396
|
}
|
|
293
397
|
if (needNewState) {
|
|
294
|
-
const durationHours = 24;
|
|
295
|
-
const fluctuation = 0.
|
|
398
|
+
const durationHours = 7 * 24;
|
|
399
|
+
const fluctuation = 0.3;
|
|
296
400
|
const targetRatio = 1 + (Math.random() * 2 - 1) * fluctuation;
|
|
297
401
|
const targetPrice = currentPrice * targetRatio;
|
|
298
402
|
const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
|
|
@@ -315,19 +419,35 @@ function apply(ctx, config) {
|
|
|
315
419
|
state = newState;
|
|
316
420
|
}
|
|
317
421
|
}
|
|
422
|
+
const timeSinceLastSwitch = now.getTime() - lastPatternSwitchTime.getTime();
|
|
423
|
+
const forceSwitchDuration = 30 * 3600 * 1e3;
|
|
424
|
+
if (now >= nextPatternSwitchTime || timeSinceLastSwitch > forceSwitchDuration) {
|
|
425
|
+
switchKLinePattern("随机时间");
|
|
426
|
+
}
|
|
318
427
|
const trend = state.trendFactor * 2;
|
|
319
|
-
const volatility = currentPrice *
|
|
428
|
+
const volatility = currentPrice * 3e-3 * (Math.random() * 2 - 1);
|
|
429
|
+
const dayStart = new Date(now);
|
|
430
|
+
dayStart.setHours(config.openHour, 0, 0, 0);
|
|
431
|
+
const dayEnd = new Date(now);
|
|
432
|
+
dayEnd.setHours(config.closeHour, 0, 0, 0);
|
|
433
|
+
const dayDuration = dayEnd.getTime() - dayStart.getTime();
|
|
434
|
+
const dayElapsed = now.getTime() - dayStart.getTime();
|
|
435
|
+
const dayProgress = Math.max(0, Math.min(1, dayElapsed / dayDuration));
|
|
436
|
+
const dailyAmplitude = state.startPrice * 0.05;
|
|
437
|
+
const patternFn = kLinePatterns[currentDayPattern];
|
|
438
|
+
const prevDayProgress = Math.max(0, (dayElapsed - 2 * 60 * 1e3) / dayDuration);
|
|
439
|
+
const patternDelta = (patternFn(dayProgress) - patternFn(prevDayProgress)) * dailyAmplitude;
|
|
320
440
|
const totalDuration = state.endTime.getTime() - state.lastCycleStart.getTime();
|
|
321
441
|
const elapsed = now.getTime() - state.lastCycleStart.getTime();
|
|
322
442
|
const prevElapsed = elapsed - 2 * 60 * 1e3;
|
|
323
|
-
const waveCount =
|
|
324
|
-
const
|
|
443
|
+
const waveCount = 7;
|
|
444
|
+
const weeklyAmplitude = state.startPrice * 0.08;
|
|
325
445
|
const getWaveValue = /* @__PURE__ */ __name((t) => {
|
|
326
446
|
const progress = t / totalDuration;
|
|
327
|
-
return
|
|
447
|
+
return weeklyAmplitude * (Math.sin(2 * Math.PI * waveCount * progress) * 0.7 + Math.sin(2 * Math.PI * waveCount * 2.5 * progress) * 0.3);
|
|
328
448
|
}, "getWaveValue");
|
|
329
449
|
const waveDelta = getWaveValue(elapsed) - getWaveValue(prevElapsed);
|
|
330
|
-
let newPrice = currentPrice + trend + volatility + waveDelta;
|
|
450
|
+
let newPrice = currentPrice + trend + volatility + patternDelta + waveDelta;
|
|
331
451
|
if (newPrice < 1) newPrice = 1;
|
|
332
452
|
currentPrice = newPrice;
|
|
333
453
|
await ctx.database.create("bourse_history", { stockId, price: newPrice, time: /* @__PURE__ */ new Date() });
|
|
@@ -541,6 +661,7 @@ function apply(ctx, config) {
|
|
|
541
661
|
});
|
|
542
662
|
ctx.command("bourse.admin.market <status>", "设置股市开关状态 (open/close/auto)", { authority: 3 }).action(async ({ session }, status) => {
|
|
543
663
|
if (!["open", "close", "auto"].includes(status)) return "无效状态,请使用 open, close, 或 auto";
|
|
664
|
+
const wasOpen = await isMarketOpen();
|
|
544
665
|
const key = "macro_state";
|
|
545
666
|
const existing = await ctx.database.get("bourse_state", { key });
|
|
546
667
|
if (existing.length === 0) {
|
|
@@ -558,8 +679,18 @@ function apply(ctx, config) {
|
|
|
558
679
|
} else {
|
|
559
680
|
await ctx.database.set("bourse_state", { key }, { marketOpenStatus: status });
|
|
560
681
|
}
|
|
682
|
+
if (status === "open" && !wasOpen) {
|
|
683
|
+
switchKLinePattern("管理员开市");
|
|
684
|
+
wasMarketOpen = true;
|
|
685
|
+
} else if (status === "close") {
|
|
686
|
+
wasMarketOpen = false;
|
|
687
|
+
}
|
|
561
688
|
return `股市状态已设置为: ${status}`;
|
|
562
689
|
});
|
|
690
|
+
ctx.command("stock.pattern", "管理员:强制切换K线模型", { authority: 3 }).action(() => {
|
|
691
|
+
switchKLinePattern("管理员手动");
|
|
692
|
+
return "已切换K线模型。";
|
|
693
|
+
});
|
|
563
694
|
async function renderStockImage(ctx2, data, name2, current, high, low) {
|
|
564
695
|
if (data.length < 2) return "数据不足,无法绘制走势图。";
|
|
565
696
|
const startPrice = data[0].price;
|
|
@@ -741,17 +872,65 @@ function apply(ctx, config) {
|
|
|
741
872
|
ctx.fillStyle = '#999';
|
|
742
873
|
ctx.font = '500 18px "Segoe UI", sans-serif';
|
|
743
874
|
|
|
744
|
-
|
|
875
|
+
// 动态计算标签间隔,防止重叠
|
|
876
|
+
// 使用最长的时间标签来估算宽度
|
|
877
|
+
let maxLabelWidth = 0;
|
|
878
|
+
for (let i = 0; i < times.length; i++) {
|
|
879
|
+
const w = ctx.measureText(times[i]).width;
|
|
880
|
+
if (w > maxLabelWidth) maxLabelWidth = w;
|
|
881
|
+
}
|
|
882
|
+
const labelWidth = maxLabelWidth + 40; // 加40px间距确保不重叠
|
|
883
|
+
const availableWidth = width - padding.left - padding.right;
|
|
884
|
+
const maxLabels = Math.max(2, Math.floor(availableWidth / labelWidth));
|
|
885
|
+
const labelCount = Math.min(maxLabels, 5); // 最多显示5个标签
|
|
886
|
+
const timeStep = Math.max(1, Math.ceil(times.length / labelCount));
|
|
887
|
+
|
|
888
|
+
// 选取要绘制的标签索引(均匀分布)
|
|
889
|
+
const labelIndices = [];
|
|
745
890
|
for (let i = 0; i < times.length; i += timeStep) {
|
|
891
|
+
labelIndices.push(i);
|
|
892
|
+
}
|
|
893
|
+
// 确保最后一个点在列表中
|
|
894
|
+
if (labelIndices[labelIndices.length - 1] !== times.length - 1) {
|
|
895
|
+
labelIndices.push(times.length - 1);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// 绘制标签,跳过重叠的
|
|
899
|
+
const drawnLabels = [];
|
|
900
|
+
for (const i of labelIndices) {
|
|
901
|
+
const x = getX(timestamps[i]);
|
|
902
|
+
const textWidth = ctx.measureText(times[i]).width;
|
|
903
|
+
|
|
904
|
+
// 根据textAlign计算实际占用的区域
|
|
905
|
+
let leftEdge, rightEdge;
|
|
906
|
+
if (i === 0) {
|
|
907
|
+
leftEdge = x;
|
|
908
|
+
rightEdge = x + textWidth;
|
|
909
|
+
} else if (i === times.length - 1) {
|
|
910
|
+
leftEdge = x - textWidth;
|
|
911
|
+
rightEdge = x;
|
|
912
|
+
} else {
|
|
913
|
+
leftEdge = x - textWidth / 2;
|
|
914
|
+
rightEdge = x + textWidth / 2;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// 检查是否与已绘制的标签重叠
|
|
918
|
+
let overlaps = false;
|
|
919
|
+
for (const drawn of drawnLabels) {
|
|
920
|
+
// 两个标签之间至少要有15px间隔
|
|
921
|
+
if (!(rightEdge + 15 < drawn.left || leftEdge - 15 > drawn.right)) {
|
|
922
|
+
overlaps = true;
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (overlaps) continue;
|
|
927
|
+
|
|
746
928
|
if (i === 0) ctx.textAlign = 'left';
|
|
747
|
-
else if (i
|
|
929
|
+
else if (i === times.length - 1) ctx.textAlign = 'right';
|
|
748
930
|
else ctx.textAlign = 'center';
|
|
749
931
|
|
|
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);
|
|
932
|
+
ctx.fillText(times[i], x, height - 10);
|
|
933
|
+
drawnLabels.push({ left: leftEdge, right: rightEdge });
|
|
755
934
|
}
|
|
756
935
|
|
|
757
936
|
</script>
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
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
|
+
## ✨ 特性
|
|
10
|
+
|
|
11
|
+
- **拟真股市算法**:包含长期趋势、随机波动和周期性波浪算法,模拟真实的股价走势。
|
|
12
|
+
- **可视化K线图**:使用 Puppeteer 渲染精美的实时、日线、周线走势图。
|
|
13
|
+
- **资金冻结机制**:交易并非即时到账,根据交易金额计算冻结时间,增加博弈乐趣。
|
|
14
|
+
- **宏观调控**:管理员可以手动干预股价走势,设定目标价格,系统将平滑过渡到目标价。
|
|
15
|
+
- **银行联动**:支持与 `koishi-plugin-monetary-bank` 联动,若现金不足可自动扣除银行活期存款(需安装对应插件)。
|
|
16
|
+
- **休市机制**:支持设置每日开市/休市时间,周末自动休市。
|
|
17
|
+
|
|
18
|
+
## 📦 依赖
|
|
19
|
+
|
|
20
|
+
本插件需要以下服务:
|
|
21
|
+
- `database`: 用于存储持仓、历史行情和挂单记录。
|
|
22
|
+
- `puppeteer`: 用于渲染股市行情图。
|
|
23
|
+
- `monetary`: (可选) 用于获取用户货币余额,实际上本插件直接操作 `monetary` 数据库表。
|
|
24
|
+
|
|
25
|
+
## 🔧 配置项
|
|
26
|
+
|
|
27
|
+
可以在控制台插件配置页进行设置:
|
|
28
|
+
|
|
29
|
+
### 基础设置
|
|
30
|
+
- **currency**: 货币单位名称(默认:`信用点`)。
|
|
31
|
+
- **stockName**: 股票名称(默认:`Koishi股份`)。
|
|
32
|
+
- **initialPrice**: 股票初始价格(默认:`1200`)。
|
|
33
|
+
- **maxHoldings**: 单人最大持仓限制(默认:`100000`)。
|
|
34
|
+
|
|
35
|
+
### 交易时间
|
|
36
|
+
- **openHour**: 每日开市时间(小时,0-23,默认 `8` 点)。
|
|
37
|
+
- **closeHour**: 每日休市时间(小时,0-23,默认 `23` 点)。
|
|
38
|
+
- **marketStatus**: 股市总开关,可选 `open` (强制开启)、`close` (强制关闭)、`auto` (自动按时间)。
|
|
39
|
+
|
|
40
|
+
### 冻结机制
|
|
41
|
+
为了防止高频刷钱,交易后资金/股票需要冻结一段时间。
|
|
42
|
+
- **freezeCostPerMinute**: 每多少货币金额计为1分钟冻结时间(默认 `100`)。
|
|
43
|
+
- **minFreezeTime**: 最小冻结时间(分钟,默认 `10`)。
|
|
44
|
+
- **maxFreezeTime**: 最大冻结时间(分钟,默认 `1440` 即24小时)。
|
|
45
|
+
|
|
46
|
+
### 宏观调控
|
|
47
|
+
- **enableManualControl**: 是否开启手动强力调控模式(覆盖自动波动)。
|
|
48
|
+
- **manualTargetPrice**: 手动模式下的目标价格。
|
|
49
|
+
- **manualDuration**: 手动调控周期(小时)。
|
|
50
|
+
|
|
51
|
+
## 🎮 指令说明
|
|
52
|
+
|
|
53
|
+
### 用户指令
|
|
54
|
+
|
|
55
|
+
- **`stock [interval]`**
|
|
56
|
+
- 查看股市行情。
|
|
57
|
+
- 参数 `interval`: 可选 `day` (日线)、`week` (周线),不填默认为实时走势(最近100条)。
|
|
58
|
+
- 示例:`stock` (查看实时), `stock day` (查看日线)。
|
|
59
|
+
|
|
60
|
+
- **`stock.buy <amount>`**
|
|
61
|
+
- 买入股票。
|
|
62
|
+
- 参数 `amount`: 购买股数(整数)。
|
|
63
|
+
- 说明:扣除现金(或银行活期),股票将在冻结时间结束后到账。
|
|
64
|
+
|
|
65
|
+
- **`stock.sell <amount>`**
|
|
66
|
+
- 卖出股票。
|
|
67
|
+
- 参数 `amount`: 卖出股数(整数)。
|
|
68
|
+
- 说明:扣除持仓,获得的资金将在冻结时间结束后到账。
|
|
69
|
+
|
|
70
|
+
- **`stock.my`**
|
|
71
|
+
- 查看我的账户。
|
|
72
|
+
- 显示当前持仓、市值以及正在进行中(冻结中)的买卖订单。
|
|
73
|
+
|
|
74
|
+
### 管理员指令 (权限等级 3)
|
|
75
|
+
|
|
76
|
+
- **`stock.control <price> [hours]`**
|
|
77
|
+
- 设置宏观调控目标。
|
|
78
|
+
- 说明:强行引导股价在指定时间内向目标价格移动。
|
|
79
|
+
- 示例:`stock.control 5000 12` (在12小时内让股价涨/跌到5000)。
|
|
80
|
+
|
|
81
|
+
- **`bourse.admin.market <status>`**
|
|
82
|
+
- 设置股市开关状态。
|
|
83
|
+
- 参数 `status`: `open` (开启), `close` (关闭), `auto` (自动)。
|
|
84
|
+
|
|
85
|
+
## 💡 常见问题
|
|
86
|
+
|
|
87
|
+
**Q: 为什么买了股票没有立刻到账?**
|
|
88
|
+
A: 本插件设计了 T+N 机制(基于金额的动态冻结)。使用 `stock.buy` 后,股票会进入“冻结中”状态,使用 `stock.my` 可以看到剩余解冻时间。卖出股票同理,资金需等待解冻。
|
|
89
|
+
|
|
90
|
+
**Q: 股价是如何波动的?**
|
|
91
|
+
A: 股价由三部分组成:长期趋势(宏观调控决定)、随机波动(模拟市场噪音)和正弦波浪(模拟周期性涨跌)。
|