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

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 +53 -46
  2. package/package.json +1 -1
  3. package/readme.md +35 -17
package/lib/index.js CHANGED
@@ -377,21 +377,18 @@ function apply(ctx, config) {
377
377
  }
378
378
  const createAutoState = /* @__PURE__ */ __name(async () => {
379
379
  const durationHours = 7 * 24;
380
- const fluctuation = 0.3;
380
+ const fluctuation = 0.25;
381
381
  const targetRatio = 1 + (Math.random() * 2 - 1) * fluctuation;
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));
382
+ let targetPrice2 = currentPrice * targetRatio;
383
+ targetPrice2 = Math.max(currentPrice * 0.5, Math.min(currentPrice * 1.5, targetPrice2));
386
384
  const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
387
- const minutes = durationHours * 60;
388
- const trendFactor = (targetPrice - currentPrice) / minutes;
389
385
  const newState = {
390
386
  key: "macro_state",
391
387
  lastCycleStart: now,
392
388
  startPrice: currentPrice,
393
- targetPrice,
394
- trendFactor,
389
+ targetPrice: targetPrice2,
390
+ trendFactor: 0,
391
+ // 不再使用线性趋势因子
395
392
  mode: "auto",
396
393
  endTime
397
394
  };
@@ -415,10 +412,10 @@ function apply(ctx, config) {
415
412
  switchKLinePattern("随机时间");
416
413
  }
417
414
  const basePrice = state.startPrice;
415
+ const targetPrice = state.targetPrice;
418
416
  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;
417
+ const elapsed = now.getTime() - state.lastCycleStart.getTime();
418
+ const cycleProgress = Math.max(0, Math.min(1, elapsed / totalDuration));
422
419
  const dayStart = new Date(now);
423
420
  dayStart.setHours(config.openHour, 0, 0, 0);
424
421
  const dayEnd = new Date(now);
@@ -426,14 +423,32 @@ function apply(ctx, config) {
426
423
  const dayDuration = dayEnd.getTime() - dayStart.getTime();
427
424
  const dayElapsed = now.getTime() - dayStart.getTime();
428
425
  const dayProgress = Math.max(0, Math.min(1, dayElapsed / dayDuration));
429
- const dailyAmplitude = basePrice * 0.07;
426
+ const expectedPrice = basePrice + (targetPrice - basePrice) * cycleProgress;
427
+ const deviation = (expectedPrice - currentPrice) / currentPrice;
428
+ const meanReversionStrength = 0.02;
429
+ const driftReturn = deviation * meanReversionStrength;
430
+ const getVolatility = /* @__PURE__ */ __name((progress) => {
431
+ const morningVol = Math.exp(-8 * progress);
432
+ const afternoonVol = Math.exp(-8 * (1 - progress));
433
+ const baseVol = 0.3;
434
+ return baseVol + morningVol * 0.5 + afternoonVol * 0.4;
435
+ }, "getVolatility");
436
+ const volatility = getVolatility(dayProgress);
437
+ const u1 = Math.random();
438
+ const u2 = Math.random();
439
+ const normalRandom = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
440
+ const baseVolatilityPerTick = 15e-4;
441
+ const randomReturn = normalRandom * baseVolatilityPerTick * volatility;
430
442
  const patternFn = kLinePatterns[currentDayPattern];
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;
443
+ const patternValue = patternFn(dayProgress);
444
+ const prevPatternValue = patternFn(Math.max(0, dayProgress - 0.01));
445
+ const patternTrend = (patternValue - prevPatternValue) * 0.5;
446
+ const patternBias = patternTrend * 3e-3;
447
+ const wavePhase = 2 * Math.PI * macroWaveCount * cycleProgress;
448
+ const prevWavePhase = 2 * Math.PI * macroWaveCount * Math.max(0, cycleProgress - 1e-3);
449
+ const waveTrend = (Math.sin(wavePhase) - Math.sin(prevWavePhase)) * macroWeeklyAmplitudeRatio;
450
+ const totalReturn = driftReturn + randomReturn + patternBias + waveTrend;
451
+ let newPrice = currentPrice * (1 + totalReturn);
437
452
  const dayBase = dailyOpenPrice ?? basePrice;
438
453
  const weekUpper = basePrice * 1.5;
439
454
  const weekLower = basePrice * 0.5;
@@ -441,6 +456,14 @@ function apply(ctx, config) {
441
456
  const dayLower = dayBase * 0.5;
442
457
  const upperLimit = Math.min(weekUpper, dayUpper);
443
458
  const lowerLimit = Math.max(weekLower, dayLower);
459
+ if (newPrice > upperLimit * 0.95) {
460
+ const overshoot = (newPrice - upperLimit * 0.95) / (upperLimit * 0.05);
461
+ newPrice = upperLimit * 0.95 + upperLimit * 0.05 * Math.tanh(overshoot);
462
+ }
463
+ if (newPrice < lowerLimit * 1.05) {
464
+ const undershoot = (lowerLimit * 1.05 - newPrice) / (lowerLimit * 0.05);
465
+ newPrice = lowerLimit * 1.05 - lowerLimit * 0.05 * Math.tanh(undershoot);
466
+ }
444
467
  newPrice = Math.max(lowerLimit, Math.min(upperLimit, newPrice));
445
468
  if (newPrice < 1) newPrice = 1;
446
469
  newPrice = Number(newPrice.toFixed(2));
@@ -687,24 +710,26 @@ function apply(ctx, config) {
687
710
  const duration = hours || 24;
688
711
  const now = /* @__PURE__ */ new Date();
689
712
  const endTime = new Date(now.getTime() + duration * 3600 * 1e3);
690
- const minutes = duration * 60;
691
- const baseStart = (await ctx.database.get("bourse_state", { key: "macro_state" }))[0]?.startPrice ?? currentPrice;
692
- const dayBase = dailyOpenPrice ?? baseStart;
693
- const upper = Math.min(baseStart * 1.5, dayBase * 1.5);
694
- const lower = Math.max(baseStart * 0.5, dayBase * 0.5);
713
+ const existing = (await ctx.database.get("bourse_state", { key: "macro_state" }))[0];
714
+ const keepBasePrice = existing?.startPrice ?? currentPrice;
715
+ const dayBase = dailyOpenPrice ?? keepBasePrice;
716
+ const upper = Math.min(keepBasePrice * 1.5, dayBase * 1.5);
717
+ const lower = Math.max(keepBasePrice * 0.5, dayBase * 0.5);
695
718
  const targetPriceClamped = Math.max(lower, Math.min(upper, price));
719
+ const minutes = duration * 60;
696
720
  const trendFactor = (targetPriceClamped - currentPrice) / minutes;
697
721
  const newState = {
698
722
  key: "macro_state",
699
- lastCycleStart: now,
700
- startPrice: currentPrice,
723
+ lastCycleStart: existing?.lastCycleStart ?? now,
724
+ // 保持原周期起点
725
+ startPrice: keepBasePrice,
726
+ // 保持原基准价,不重置
701
727
  targetPrice: targetPriceClamped,
702
728
  trendFactor,
703
729
  mode: "manual",
704
730
  endTime
705
731
  };
706
- const existing = await ctx.database.get("bourse_state", { key: "macro_state" });
707
- if (existing.length === 0) {
732
+ if (!existing) {
708
733
  await ctx.database.create("bourse_state", newState);
709
734
  } else {
710
735
  const { key, ...updateFields } = newState;
@@ -749,24 +774,6 @@ function apply(ctx, config) {
749
774
  switchKLinePattern("管理员手动");
750
775
  return "已切换K线模型。";
751
776
  });
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
- });
770
777
  async function renderHoldingImage(ctx2, username, holding, pending, currency) {
771
778
  const hasCostData = holding && holding.totalCost !== null;
772
779
  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.2-Alpha.5",
3
+ "version": "1.1.2-Alpha.7",
4
4
  "main": "lib/index.js",
5
5
  "typings": "lib/index.d.ts",
6
6
  "files": [
package/readme.md CHANGED
@@ -19,6 +19,21 @@
19
19
 
20
20
  ## 更新记录
21
21
 
22
+ - **Alpha.7**:
23
+ - **采用真实股票模型**:使用几何布朗运动 + 均值回归模型,更贴近真实股票走势。
24
+ - 新增:均值回归机制——价格会自然向"预期价格"回归,而非硬性跳变。
25
+ - 新增:动态波动率——开盘和收盘波动大,午盘相对平静(U型波动率曲线)。
26
+ - 新增:正态分布随机项——使用Box-Muller变换生成更真实的随机波动。
27
+ - 新增:软着陆限幅——接近涨跌幅限制时逐渐减缓,避免硬切导致的不自然走势。
28
+ - 优化:K线形态改为提供方向性偏置,而非直接决定价格位置。
29
+ - 优化:周期波浪改为增量式叠加,更平滑自然。
30
+
31
+ - **Alpha.6**:
32
+ - **再次重构**:改用百分比波动模式,所有波动(K线、周波浪、噪音)以百分比形式叠加在趋势价格上,彻底解决振幅失控问题。
33
+ - 修复:手动宏观调控不再重置周期基准价和起点,保持现有波动机制持续生效。
34
+ - 优化:K线波动幅度调整为±3%,周波浪和噪音也改为百分比计算,波动更符合真实股票特性。
35
+ - 性能:新的百分比合成模式更稳定,长期运行不会出现价格失控或波动失效。
36
+
22
37
  - **Alpha.5**:
23
38
  - **重写股票走势引擎**:将增量叠加模式改为绝对价格计算模式,彻底解决长期运行后的高频抖动问题。
24
39
  - 修复:走势计算采用绝对值合成(基准价+趋势进度+日内波动+周波浪+噪音),避免累积误差导致的失控。
@@ -131,10 +146,6 @@
131
146
  - 开发测试:推进价格更新若干次并返回当前价格。
132
147
  - 参数 `ticks`: 推进次数,默认 1,最大 500。
133
148
 
134
- - 【默认不开启】**`bourse.test.clamp <percent>`**
135
- - 开发测试:尝试目标涨跌幅并查看限幅结果。
136
- - 参数 `percent`: 期望涨跌百分比,例如 `60` 或 `-40`。
137
-
138
149
  ## 💡 常见问题
139
150
 
140
151
  **Q:Alpha版本有什么区别?**
@@ -150,21 +161,28 @@ A: 本插件设计了基于交易金额的动态冻结机制。交易额越大
150
161
  ---
151
162
 
152
163
  **Q: 股价是如何波动的?**
153
- A: 股价采用**绝对价格计算模式**,由以下部分合成:
164
+ A: 股价采用**几何布朗运动 + 均值回归**模型(Alpha.7),更贴近真实股票:
165
+
166
+ **核心公式:** `新价格 = 当前价格 × (1 + 总收益率)`
167
+
168
+ **总收益率由以下部分组成:**
169
+
170
+ 1. **均值回归项(Drift)**:价格会自然向"预期价格"回归
171
+ - 预期价格 = 周期起始价 → 目标价的线性插值
172
+ - 偏离越大,回归力越强(每次回归2%的偏差)
154
173
 
155
- 1. **宏观趋势**:周期内从起始价线性向目标价移动的进度价格。
156
- 2. **日内K线形态**:12种剧本叠加在趋势上的波动偏移(约±4%)。
157
- 3. **周内波浪**:正弦曲线叠加的平滑周期波动。
158
- 4. **微小噪音**:随机扰动避免价格过于规律。
174
+ 2. **随机波动项(Random Walk)**:模拟市场的随机性
175
+ - 使用正态分布随机数(Box-Muller变换)
176
+ - 基础波动率约0.15%/tick
159
177
 
160
- 新引擎完全避免增量累积误差,长期运行稳定可靠。
178
+ 3. **动态波动率**:模拟日内波动率变化
179
+ - 开盘:波动率高(活跃交易)
180
+ - 午盘:波动率低(相对平静)
181
+ - 尾盘:波动率再次升高
161
182
 
162
- 在Alpha.5版本以前,股价由四部分叠加而成:
163
- 1. **宏观趋势**:根据自动或手动的目标价格计算的线性趋势。
164
- 2. **周级波浪**:7天为一个大周期的正弦复合波浪。
165
- 3. **日内形态**:每天随机从 12 种剧本中选择多种(如 V 型、倒 V 型、阶梯上涨等)。
166
- 4. **随机噪音**:微小的随机波动。
183
+ 4. **K线形态偏置**:12种日内形态提供微小的方向性偏好
184
+ - 不再直接决定价格位置,而是影响涨跌概率
167
185
 
168
- 新旧引擎的效果相同,仅仅是在算法上进行优化与修复。
186
+ 5. **周期波浪**:中期波动,模拟市场情绪周期
169
187
 
170
- 此外,为保证市场稳定性,应用了**涨跌幅硬性限制**:相对于周周期起始价与当日开盘价的双重基准,股价的涨跌幅不得超过 ±50%。当随机或手动设置的目标超出限幅时,将自动截断至限幅边界。
188
+ **限幅机制:** 相对于周期起始价和日开盘价的±50%,采用软着陆方式避免硬切。