koishi-plugin-monetary-bourse 1.1.1 → 1.1.2-Alpha.5

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.
Files changed (3) hide show
  1. package/lib/index.js +44 -26
  2. package/package.json +1 -1
  3. package/readme.md +31 -4
package/lib/index.js CHANGED
@@ -379,14 +379,13 @@ function apply(ctx, config) {
379
379
  const durationHours = 7 * 24;
380
380
  const fluctuation = 0.3;
381
381
  const targetRatio = 1 + (Math.random() * 2 - 1) * fluctuation;
382
- const targetPrice = currentPrice * targetRatio;
382
+ let targetPrice = currentPrice * targetRatio;
383
+ const upperTarget = currentPrice * 1.5;
384
+ const lowerTarget = currentPrice * 0.5;
385
+ targetPrice = Math.max(lowerTarget, Math.min(upperTarget, targetPrice));
383
386
  const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
384
387
  const minutes = durationHours * 60;
385
388
  const trendFactor = (targetPrice - currentPrice) / minutes;
386
- macroWaveCount = 5 + Math.floor(Math.random() * 6);
387
- macroWeeklyAmplitudeRatio = 0.06 + Math.random() * 0.06;
388
- const hours = 6 + Math.floor(Math.random() * 19);
389
- nextMacroSwitchTime = new Date(now.getTime() + hours * 3600 * 1e3);
390
389
  const newState = {
391
390
  key: "macro_state",
392
391
  lastCycleStart: now,
@@ -406,6 +405,8 @@ function apply(ctx, config) {
406
405
  if (needNewState) {
407
406
  await createAutoState();
408
407
  } else if (state.mode === "auto" && nextMacroSwitchTime && now >= nextMacroSwitchTime) {
408
+ const hours = 6 + Math.floor(Math.random() * 19);
409
+ nextMacroSwitchTime = new Date(now.getTime() + hours * 3600 * 1e3);
409
410
  await createAutoState();
410
411
  }
411
412
  const timeSinceLastSwitch = now.getTime() - lastPatternSwitchTime.getTime();
@@ -413,8 +414,11 @@ function apply(ctx, config) {
413
414
  if (now >= nextPatternSwitchTime || timeSinceLastSwitch > forceSwitchDuration) {
414
415
  switchKLinePattern("随机时间");
415
416
  }
416
- const trend = state.trendFactor * 2;
417
- const volatility = currentPrice * 3e-3 * (Math.random() * 2 - 1);
417
+ const basePrice = state.startPrice;
418
+ const totalDuration = state.endTime.getTime() - state.lastCycleStart.getTime();
419
+ const elapsed = Math.min(totalDuration, now.getTime() - state.lastCycleStart.getTime());
420
+ const cycleProgress = elapsed / totalDuration;
421
+ const trendPrice = basePrice + (state.targetPrice - basePrice) * cycleProgress;
418
422
  const dayStart = new Date(now);
419
423
  dayStart.setHours(config.openHour, 0, 0, 0);
420
424
  const dayEnd = new Date(now);
@@ -422,27 +426,23 @@ function apply(ctx, config) {
422
426
  const dayDuration = dayEnd.getTime() - dayStart.getTime();
423
427
  const dayElapsed = now.getTime() - dayStart.getTime();
424
428
  const dayProgress = Math.max(0, Math.min(1, dayElapsed / dayDuration));
425
- const dailyAmplitude = state.startPrice * 0.05;
429
+ const dailyAmplitude = basePrice * 0.07;
426
430
  const patternFn = kLinePatterns[currentDayPattern];
427
- const prevDayProgress = Math.max(0, (dayElapsed - 2 * 60 * 1e3) / dayDuration);
428
- const patternDelta = (patternFn(dayProgress) - patternFn(prevDayProgress)) * dailyAmplitude;
429
- const totalDuration = state.endTime.getTime() - state.lastCycleStart.getTime();
430
- const elapsed = now.getTime() - state.lastCycleStart.getTime();
431
- const prevElapsed = elapsed - 2 * 60 * 1e3;
432
- const waveCount = macroWaveCount;
433
- const weeklyAmplitude = state.startPrice * macroWeeklyAmplitudeRatio;
434
- const getWaveValue = /* @__PURE__ */ __name((t) => {
435
- const progress = t / totalDuration;
436
- return weeklyAmplitude * (Math.sin(2 * Math.PI * waveCount * progress) * 0.7 + Math.sin(2 * Math.PI * waveCount * 2.5 * progress) * 0.3);
437
- }, "getWaveValue");
438
- const waveDelta = getWaveValue(elapsed) - getWaveValue(prevElapsed);
439
- let newPrice = currentPrice + trend + volatility + patternDelta + waveDelta;
431
+ const patternOffset = patternFn(dayProgress) * dailyAmplitude;
432
+ const waveFrequency = macroWaveCount;
433
+ const weeklyAmplitude = basePrice * macroWeeklyAmplitudeRatio;
434
+ const waveOffset = weeklyAmplitude * Math.sin(2 * Math.PI * waveFrequency * cycleProgress);
435
+ const noise = basePrice * 4e-3 * (Math.random() * 2 - 1);
436
+ let newPrice = trendPrice + patternOffset + waveOffset + noise;
437
+ const dayBase = dailyOpenPrice ?? basePrice;
438
+ const weekUpper = basePrice * 1.5;
439
+ const weekLower = basePrice * 0.5;
440
+ const dayUpper = dayBase * 1.5;
441
+ const dayLower = dayBase * 0.5;
442
+ const upperLimit = Math.min(weekUpper, dayUpper);
443
+ const lowerLimit = Math.max(weekLower, dayLower);
444
+ newPrice = Math.max(lowerLimit, Math.min(upperLimit, newPrice));
440
445
  if (newPrice < 1) newPrice = 1;
441
- const dayBase = dailyOpenPrice ?? state.startPrice;
442
- const upperLimit = Math.min(state.startPrice * 1.5, dayBase * 1.5);
443
- const lowerLimit = Math.max(state.startPrice * 0.5, dayBase * 0.5);
444
- if (newPrice > upperLimit) newPrice = upperLimit;
445
- if (newPrice < lowerLimit) newPrice = lowerLimit;
446
446
  newPrice = Number(newPrice.toFixed(2));
447
447
  currentPrice = newPrice;
448
448
  await ctx.database.create("bourse_history", { stockId, price: newPrice, time: /* @__PURE__ */ new Date() });
@@ -749,6 +749,24 @@ function apply(ctx, config) {
749
749
  switchKLinePattern("管理员手动");
750
750
  return "已切换K线模型。";
751
751
  });
752
+ ctx.command("bourse.test.price [ticks:number]", "开发测试:推进价格更新若干次并返回当前价格", { authority: 3 }).action(async ({ session }, ticks) => {
753
+ const n = typeof ticks === "number" && ticks > 0 ? Math.min(ticks, 500) : 1;
754
+ for (let i = 0; i < n; i++) {
755
+ await updatePrice();
756
+ }
757
+ return `测试完成:推进${n}次;当前价格:${Number(currentPrice.toFixed(2))}`;
758
+ });
759
+ ctx.command("bourse.test.clamp <percent:number>", "开发测试:尝试目标涨跌幅并查看限幅结果", { authority: 3 }).action(async ({ session }, percent) => {
760
+ if (typeof percent !== "number") return "请输入百分比(例如 60 或 -40)";
761
+ const baseStart = (await ctx.database.get("bourse_state", { key: "macro_state" }))[0]?.startPrice ?? currentPrice;
762
+ const dayBase = dailyOpenPrice ?? baseStart;
763
+ const upper = Math.min(baseStart * 1.5, dayBase * 1.5);
764
+ const lower = Math.max(baseStart * 0.5, dayBase * 0.5);
765
+ const expected = currentPrice * (1 + percent / 100);
766
+ const clamped = Math.max(lower, Math.min(upper, expected));
767
+ const hint = clamped !== expected ? `(已按±50%限幅从${Number(expected.toFixed(2))}调整为${Number(clamped.toFixed(2))})` : "";
768
+ return `测试限幅:当前价=${Number(currentPrice.toFixed(2))};期望=${Number(expected.toFixed(2))};结果=${Number(clamped.toFixed(2))}${hint}`;
769
+ });
752
770
  async function renderHoldingImage(ctx2, username, holding, pending, currency) {
753
771
  const hasCostData = holding && holding.totalCost !== null;
754
772
  const isProfit = hasCostData ? holding.profit >= 0 : true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-monetary-bourse",
3
- "version": "1.1.1",
3
+ "version": "1.1.2-Alpha.5",
4
4
  "main": "lib/index.js",
5
5
  "typings": "lib/index.d.ts",
6
6
  "files": [
package/readme.md CHANGED
@@ -14,11 +14,18 @@
14
14
  - 结合 **周级波浪** 与 **宏观趋势**,模拟真实市场的多周期叠加效应。
15
15
  - **📊 精美 K 线与持仓渲染**:使用 Puppeteer 渲染实时/日/周线图和个人持仓信息,Alpha 2 版本优化了坐标轴标签算法,防止文字重叠,观感更佳。
16
16
  - **❄️ 资金冻结机制**:交易采用 T+0 但资金/股票非即时到账模式,根据金额计算冻结时间,增加博弈深度。
17
- - **🏦 银行联动**:支持与 `koishi-plugin-monetary-bank` 联动,现金不足时自动扣除银行活期存款。
17
+ - **🏦 银行联动**:支持与[ `koishi-plugin-monetary-bank`](https://github.com/BYWled/koishi-plugin-monetary-bank)[![npm](https://img.shields.io/npm/v/koishi-plugin-monetary-bank?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-monetary-bank) 联动,现金不足时自动扣除银行活期存款。
18
18
  - **🕹️ 宏观调控**:管理员可手动干预股价目标,或强制切换当天的 K 线剧本。
19
19
 
20
20
  ## 更新记录
21
21
 
22
+ - **Alpha.5**:
23
+ - **重写股票走势引擎**:将增量叠加模式改为绝对价格计算模式,彻底解决长期运行后的高频抖动问题。
24
+ - 修复:走势计算采用绝对值合成(基准价+趋势进度+日内波动+周波浪+噪音),避免累积误差导致的失控。
25
+ - 修复:增强±50%限幅机制,确保所有场景下(实时/日/周)均严格受限。
26
+ - 优化:简化宏观调控状态刷新逻辑,移除复杂的delta计算,提升稳定性。
27
+ - 性能:新引擎更清晰高效,可长期稳定运行,不会出现抖动或限幅失效。
28
+
22
29
  - **1.1.1(Alpha.4)**:
23
30
  - 新增:硬性涨跌幅上限规则(±50%),同时约束日内与周内视角。随机与手动调控的结果均会在应用前进行限幅。
24
31
  - 新增:更随机的自动宏观调控——随机刷新宏观目标(约每 6–24 小时),并随机调整周波浪频率与幅度(波浪段数约 5–10、周幅度约 6%–12%)。
@@ -120,24 +127,44 @@
120
127
  - 参数 `status`: `open` (开启), `close` (关闭), `auto` (自动)。
121
128
  - 说明:手动开市时会自动重置并切换一个新的日内 K 线形态。
122
129
 
123
- - **`bourse.test.price [ticks]`**
130
+ - 【默认不开启】**`bourse.test.price [ticks]`**
124
131
  - 开发测试:推进价格更新若干次并返回当前价格。
125
132
  - 参数 `ticks`: 推进次数,默认 1,最大 500。
126
133
 
127
- - **`bourse.test.clamp <percent>`**
134
+ - 【默认不开启】**`bourse.test.clamp <percent>`**
128
135
  - 开发测试:尝试目标涨跌幅并查看限幅结果。
129
136
  - 参数 `percent`: 期望涨跌百分比,例如 `60` 或 `-40`。
130
137
 
131
138
  ## 💡 常见问题
132
139
 
140
+ **Q:Alpha版本有什么区别?**
141
+ A:本插件在发布一个稳定版之前会进行测试,但是股票的走向和时间有关,即使通过开发调试,也难以测试出稳定的结果。因此,在新版本开发出来后(尤其是算法上的更新),我不确定是否存在非致命但影响体验的漏洞。
142
+
143
+ Alpha版本就是在新版本稳定之前的**过渡版**,它们具备了一些没有验证的新功能、更新,但与之一同的是未知的bug。如需使用Alpha版本,请备份数据库和配置文件,防止以外发生。
144
+
145
+ ---
146
+
133
147
  **Q: 为什么买了股票没有立刻到账?**
134
148
  A: 本插件设计了基于交易金额的动态冻结机制。交易额越大,冻结时间越长(可配置)。请使用 `stock.my` 查看剩余解冻时间。
135
149
 
150
+ ---
151
+
136
152
  **Q: 股价是如何波动的?**
137
- A: 股价由四部分叠加而成:
153
+ A: 股价采用**绝对价格计算模式**,由以下部分合成:
154
+
155
+ 1. **宏观趋势**:周期内从起始价线性向目标价移动的进度价格。
156
+ 2. **日内K线形态**:12种剧本叠加在趋势上的波动偏移(约±4%)。
157
+ 3. **周内波浪**:正弦曲线叠加的平滑周期波动。
158
+ 4. **微小噪音**:随机扰动避免价格过于规律。
159
+
160
+ 新引擎完全避免增量累积误差,长期运行稳定可靠。
161
+
162
+ 在Alpha.5版本以前,股价由四部分叠加而成:
138
163
  1. **宏观趋势**:根据自动或手动的目标价格计算的线性趋势。
139
164
  2. **周级波浪**:7天为一个大周期的正弦复合波浪。
140
165
  3. **日内形态**:每天随机从 12 种剧本中选择多种(如 V 型、倒 V 型、阶梯上涨等)。
141
166
  4. **随机噪音**:微小的随机波动。
142
167
 
168
+ 新旧引擎的效果相同,仅仅是在算法上进行优化与修复。
169
+
143
170
  此外,为保证市场稳定性,应用了**涨跌幅硬性限制**:相对于周周期起始价与当日开盘价的双重基准,股价的涨跌幅不得超过 ±50%。当随机或手动设置的目标超出限幅时,将自动截断至限幅边界。