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.
- package/lib/index.js +53 -46
- package/package.json +1 -1
- 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.
|
|
380
|
+
const fluctuation = 0.25;
|
|
381
381
|
const targetRatio = 1 + (Math.random() * 2 - 1) * fluctuation;
|
|
382
|
-
let
|
|
383
|
-
|
|
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 =
|
|
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
|
|
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
|
|
432
|
-
const
|
|
433
|
-
const
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
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
|
|
691
|
-
const
|
|
692
|
-
const dayBase = dailyOpenPrice ??
|
|
693
|
-
const upper = Math.min(
|
|
694
|
-
const lower = Math.max(
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
4. **微小噪音**:随机扰动避免价格过于规律。
|
|
174
|
+
2. **随机波动项(Random Walk)**:模拟市场的随机性
|
|
175
|
+
- 使用正态分布随机数(Box-Muller变换)
|
|
176
|
+
- 基础波动率约0.15%/tick
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
3. **动态波动率**:模拟日内波动率变化
|
|
179
|
+
- 开盘:波动率高(活跃交易)
|
|
180
|
+
- 午盘:波动率低(相对平静)
|
|
181
|
+
- 尾盘:波动率再次升高
|
|
161
182
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
2. **周级波浪**:7天为一个大周期的正弦复合波浪。
|
|
165
|
-
3. **日内形态**:每天随机从 12 种剧本中选择多种(如 V 型、倒 V 型、阶梯上涨等)。
|
|
166
|
-
4. **随机噪音**:微小的随机波动。
|
|
183
|
+
4. **K线形态偏置**:12种日内形态提供微小的方向性偏好
|
|
184
|
+
- 不再直接决定价格位置,而是影响涨跌概率
|
|
167
185
|
|
|
168
|
-
|
|
186
|
+
5. **周期波浪**:中期波动,模拟市场情绪周期
|
|
169
187
|
|
|
170
|
-
|
|
188
|
+
**限幅机制:** 相对于周期起始价和日开盘价的±50%,采用软着陆方式避免硬切。
|