koishi-plugin-monetary-bourse 1.1.0 → 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.
- package/lib/index.d.ts +0 -3
- package/lib/index.js +95 -83
- package/package.json +1 -1
- package/readme.md +49 -7
package/lib/index.d.ts
CHANGED
|
@@ -71,9 +71,6 @@ export interface Config {
|
|
|
71
71
|
freezeCostPerMinute: number;
|
|
72
72
|
minFreezeTime: number;
|
|
73
73
|
maxFreezeTime: number;
|
|
74
|
-
enableManualControl: boolean;
|
|
75
|
-
manualTargetPrice: number;
|
|
76
|
-
manualDuration: number;
|
|
77
74
|
marketStatus: 'open' | 'close' | 'auto';
|
|
78
75
|
}
|
|
79
76
|
export declare const Config: Schema<Config>;
|
package/lib/index.js
CHANGED
|
@@ -51,12 +51,7 @@ var Config = import_koishi.Schema.intersect([
|
|
|
51
51
|
freezeCostPerMinute: import_koishi.Schema.number().min(1).default(100).description("每多少货币计为1分钟冻结时间"),
|
|
52
52
|
minFreezeTime: import_koishi.Schema.number().min(0).default(10).description("最小冻结时间(分钟)"),
|
|
53
53
|
maxFreezeTime: import_koishi.Schema.number().min(0).default(1440).description("最大交易冻结时间(分钟)")
|
|
54
|
-
}).description("冻结机制")
|
|
55
|
-
import_koishi.Schema.object({
|
|
56
|
-
enableManualControl: import_koishi.Schema.boolean().default(false).description("开启手动宏观调控(覆盖自动)"),
|
|
57
|
-
manualTargetPrice: import_koishi.Schema.number().min(0.01).default(1e3).description("手动目标价格"),
|
|
58
|
-
manualDuration: import_koishi.Schema.number().min(1).default(24).description("手动调控周期(小时)")
|
|
59
|
-
}).description("手动宏观调控")
|
|
54
|
+
}).description("冻结机制")
|
|
60
55
|
]);
|
|
61
56
|
function apply(ctx, config) {
|
|
62
57
|
ctx.model.extend("bourse_holding", {
|
|
@@ -106,10 +101,17 @@ function apply(ctx, config) {
|
|
|
106
101
|
}
|
|
107
102
|
});
|
|
108
103
|
let wasMarketOpen = false;
|
|
104
|
+
let dailyOpenPrice = null;
|
|
105
|
+
let macroWaveCount = 7;
|
|
106
|
+
let macroWeeklyAmplitudeRatio = 0.08;
|
|
107
|
+
let nextMacroSwitchTime = null;
|
|
109
108
|
ctx.setInterval(async () => {
|
|
110
109
|
const isOpen = await isMarketOpen();
|
|
111
110
|
if (isOpen && !wasMarketOpen) {
|
|
112
111
|
switchKLinePattern("自动开市");
|
|
112
|
+
dailyOpenPrice = currentPrice;
|
|
113
|
+
const hours = 6 + Math.floor(Math.random() * 19);
|
|
114
|
+
nextMacroSwitchTime = new Date(Date.now() + hours * 3600 * 1e3);
|
|
113
115
|
}
|
|
114
116
|
wasMarketOpen = isOpen;
|
|
115
117
|
if (!isOpen) return;
|
|
@@ -366,73 +368,57 @@ function apply(ctx, config) {
|
|
|
366
368
|
if (!state.endTime) state.endTime = new Date(state.lastCycleStart.getTime() + 7 * 24 * 3600 * 1e3);
|
|
367
369
|
if (!(state.endTime instanceof Date)) state.endTime = new Date(state.endTime);
|
|
368
370
|
}
|
|
369
|
-
if (config.enableManualControl) {
|
|
370
|
-
if (!state || state.mode !== "manual" || Math.abs(state.targetPrice - config.manualTargetPrice) > 0.01) {
|
|
371
|
-
const durationHours = config.manualDuration;
|
|
372
|
-
const targetPrice = config.manualTargetPrice;
|
|
373
|
-
const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
|
|
374
|
-
const minutes = durationHours * 60;
|
|
375
|
-
const trendFactor = (targetPrice - currentPrice) / minutes;
|
|
376
|
-
const newState = {
|
|
377
|
-
key: "macro_state",
|
|
378
|
-
lastCycleStart: now,
|
|
379
|
-
startPrice: currentPrice,
|
|
380
|
-
targetPrice,
|
|
381
|
-
trendFactor,
|
|
382
|
-
mode: "manual",
|
|
383
|
-
endTime
|
|
384
|
-
};
|
|
385
|
-
if (!state) await ctx.database.create("bourse_state", newState);
|
|
386
|
-
else {
|
|
387
|
-
const { key, ...updateFields } = newState;
|
|
388
|
-
await ctx.database.set("bourse_state", { key: "macro_state" }, updateFields);
|
|
389
|
-
}
|
|
390
|
-
state = newState;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
371
|
let needNewState = false;
|
|
394
|
-
if (!
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
await ctx.database.set("bourse_state", { key: "macro_state" }, updateFields);
|
|
425
|
-
}
|
|
426
|
-
state = newState;
|
|
372
|
+
if (!state) {
|
|
373
|
+
needNewState = true;
|
|
374
|
+
} else {
|
|
375
|
+
const endTime = state.endTime || new Date(state.lastCycleStart.getTime() + 7 * 24 * 3600 * 1e3);
|
|
376
|
+
if (state.mode !== "manual" && now > endTime) needNewState = true;
|
|
377
|
+
}
|
|
378
|
+
const createAutoState = /* @__PURE__ */ __name(async () => {
|
|
379
|
+
const durationHours = 7 * 24;
|
|
380
|
+
const fluctuation = 0.3;
|
|
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));
|
|
386
|
+
const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
|
|
387
|
+
const minutes = durationHours * 60;
|
|
388
|
+
const trendFactor = (targetPrice - currentPrice) / minutes;
|
|
389
|
+
const newState = {
|
|
390
|
+
key: "macro_state",
|
|
391
|
+
lastCycleStart: now,
|
|
392
|
+
startPrice: currentPrice,
|
|
393
|
+
targetPrice,
|
|
394
|
+
trendFactor,
|
|
395
|
+
mode: "auto",
|
|
396
|
+
endTime
|
|
397
|
+
};
|
|
398
|
+
if (!state) await ctx.database.create("bourse_state", newState);
|
|
399
|
+
else {
|
|
400
|
+
const { key, ...updateFields } = newState;
|
|
401
|
+
await ctx.database.set("bourse_state", { key: "macro_state" }, updateFields);
|
|
427
402
|
}
|
|
403
|
+
state = newState;
|
|
404
|
+
}, "createAutoState");
|
|
405
|
+
if (needNewState) {
|
|
406
|
+
await createAutoState();
|
|
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);
|
|
410
|
+
await createAutoState();
|
|
428
411
|
}
|
|
429
412
|
const timeSinceLastSwitch = now.getTime() - lastPatternSwitchTime.getTime();
|
|
430
413
|
const forceSwitchDuration = 30 * 3600 * 1e3;
|
|
431
414
|
if (now >= nextPatternSwitchTime || timeSinceLastSwitch > forceSwitchDuration) {
|
|
432
415
|
switchKLinePattern("随机时间");
|
|
433
416
|
}
|
|
434
|
-
const
|
|
435
|
-
const
|
|
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;
|
|
436
422
|
const dayStart = new Date(now);
|
|
437
423
|
dayStart.setHours(config.openHour, 0, 0, 0);
|
|
438
424
|
const dayEnd = new Date(now);
|
|
@@ -440,21 +426,22 @@ function apply(ctx, config) {
|
|
|
440
426
|
const dayDuration = dayEnd.getTime() - dayStart.getTime();
|
|
441
427
|
const dayElapsed = now.getTime() - dayStart.getTime();
|
|
442
428
|
const dayProgress = Math.max(0, Math.min(1, dayElapsed / dayDuration));
|
|
443
|
-
const dailyAmplitude =
|
|
429
|
+
const dailyAmplitude = basePrice * 0.07;
|
|
444
430
|
const patternFn = kLinePatterns[currentDayPattern];
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
|
|
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));
|
|
458
445
|
if (newPrice < 1) newPrice = 1;
|
|
459
446
|
newPrice = Number(newPrice.toFixed(2));
|
|
460
447
|
currentPrice = newPrice;
|
|
@@ -701,12 +688,17 @@ function apply(ctx, config) {
|
|
|
701
688
|
const now = /* @__PURE__ */ new Date();
|
|
702
689
|
const endTime = new Date(now.getTime() + duration * 3600 * 1e3);
|
|
703
690
|
const minutes = duration * 60;
|
|
704
|
-
const
|
|
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);
|
|
695
|
+
const targetPriceClamped = Math.max(lower, Math.min(upper, price));
|
|
696
|
+
const trendFactor = (targetPriceClamped - currentPrice) / minutes;
|
|
705
697
|
const newState = {
|
|
706
698
|
key: "macro_state",
|
|
707
699
|
lastCycleStart: now,
|
|
708
700
|
startPrice: currentPrice,
|
|
709
|
-
targetPrice:
|
|
701
|
+
targetPrice: targetPriceClamped,
|
|
710
702
|
trendFactor,
|
|
711
703
|
mode: "manual",
|
|
712
704
|
endTime
|
|
@@ -715,10 +707,12 @@ function apply(ctx, config) {
|
|
|
715
707
|
if (existing.length === 0) {
|
|
716
708
|
await ctx.database.create("bourse_state", newState);
|
|
717
709
|
} else {
|
|
718
|
-
|
|
710
|
+
const { key, ...updateFields } = newState;
|
|
711
|
+
await ctx.database.set("bourse_state", { key: "macro_state" }, updateFields);
|
|
719
712
|
}
|
|
713
|
+
const hint = targetPriceClamped !== price ? `(已按±50%限幅从${price}调整为${Number(targetPriceClamped.toFixed(2))})` : "";
|
|
720
714
|
return `宏观调控已设置:
|
|
721
|
-
目标价格:${
|
|
715
|
+
目标价格:${Number(targetPriceClamped.toFixed(2))}${hint}
|
|
722
716
|
期限:${duration}小时
|
|
723
717
|
模式:手动干预
|
|
724
718
|
到期后将自动切回随机调控。`;
|
|
@@ -755,6 +749,24 @@ function apply(ctx, config) {
|
|
|
755
749
|
switchKLinePattern("管理员手动");
|
|
756
750
|
return "已切换K线模型。";
|
|
757
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
|
+
});
|
|
758
770
|
async function renderHoldingImage(ctx2, username, holding, pending, currency) {
|
|
759
771
|
const hasCostData = holding && holding.totalCost !== null;
|
|
760
772
|
const isProfit = hasCostData ? holding.profit >= 0 : true;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -14,12 +14,26 @@
|
|
|
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)[](https://www.npmjs.com/package/koishi-plugin-monetary-bank) 联动,现金不足时自动扣除银行活期存款。
|
|
18
18
|
- **🕹️ 宏观调控**:管理员可手动干预股价目标,或强制切换当天的 K 线剧本。
|
|
19
19
|
|
|
20
20
|
## 更新记录
|
|
21
21
|
|
|
22
|
-
-
|
|
22
|
+
- **Alpha.5**:
|
|
23
|
+
- **重写股票走势引擎**:将增量叠加模式改为绝对价格计算模式,彻底解决长期运行后的高频抖动问题。
|
|
24
|
+
- 修复:走势计算采用绝对值合成(基准价+趋势进度+日内波动+周波浪+噪音),避免累积误差导致的失控。
|
|
25
|
+
- 修复:增强±50%限幅机制,确保所有场景下(实时/日/周)均严格受限。
|
|
26
|
+
- 优化:简化宏观调控状态刷新逻辑,移除复杂的delta计算,提升稳定性。
|
|
27
|
+
- 性能:新引擎更清晰高效,可长期稳定运行,不会出现抖动或限幅失效。
|
|
28
|
+
|
|
29
|
+
- **1.1.1(Alpha.4)**:
|
|
30
|
+
- 新增:硬性涨跌幅上限规则(±50%),同时约束日内与周内视角。随机与手动调控的结果均会在应用前进行限幅。
|
|
31
|
+
- 新增:更随机的自动宏观调控——随机刷新宏观目标(约每 6–24 小时),并随机调整周波浪频率与幅度(波浪段数约 5–10、周幅度约 6%–12%)。
|
|
32
|
+
- 修复:手动宏观调控命令更新状态时,严格排除主键字段,避免 `TypeError: cannot modify primary key`。
|
|
33
|
+
- 变更:移除配置项中的手动宏观调控相关字段(`enableManualControl`、`manualTargetPrice`、`manualDuration`),改为仅通过管理员指令进行宏观调控。
|
|
34
|
+
- 新增:开发测试命令方便快速验证逻辑(`bourse.test.price`、`bourse.test.clamp`),默认注释不启用,若需要请手动修改代码打开调试。
|
|
35
|
+
|
|
36
|
+
- **1.1.0**:
|
|
23
37
|
- Alpha.3版本转为正式版。
|
|
24
38
|
|
|
25
39
|
- **Alpha.3**:
|
|
@@ -72,9 +86,7 @@
|
|
|
72
86
|
- **maxFreezeTime**: 最大冻结时间(分钟,默认 `1440` 即24小时)。
|
|
73
87
|
|
|
74
88
|
### 宏观调控
|
|
75
|
-
-
|
|
76
|
-
- **manualTargetPrice**: 手动模式下的目标价格。
|
|
77
|
-
- **manualDuration**: 手动调控周期(小时)。
|
|
89
|
+
- 已移除配置项中的手动宏观调控字段;请使用管理员指令进行宏观调控。
|
|
78
90
|
|
|
79
91
|
## 🎮 指令说明
|
|
80
92
|
|
|
@@ -103,7 +115,7 @@
|
|
|
103
115
|
|
|
104
116
|
- **`stock.control <price> [hours]`**
|
|
105
117
|
- 设置宏观调控目标。
|
|
106
|
-
-
|
|
118
|
+
- 说明:强行引导股价在指定时间内向目标价格移动。若目标涨跌超出±50%限幅,会自动调整至限幅边界后再应用。
|
|
107
119
|
- 示例:`stock.control 5000 12` (在12小时内让股价涨/跌到5000)。
|
|
108
120
|
|
|
109
121
|
- **`stock.pattern`** *(Alpha 2 新增)*
|
|
@@ -115,14 +127,44 @@
|
|
|
115
127
|
- 参数 `status`: `open` (开启), `close` (关闭), `auto` (自动)。
|
|
116
128
|
- 说明:手动开市时会自动重置并切换一个新的日内 K 线形态。
|
|
117
129
|
|
|
130
|
+
- 【默认不开启】**`bourse.test.price [ticks]`**
|
|
131
|
+
- 开发测试:推进价格更新若干次并返回当前价格。
|
|
132
|
+
- 参数 `ticks`: 推进次数,默认 1,最大 500。
|
|
133
|
+
|
|
134
|
+
- 【默认不开启】**`bourse.test.clamp <percent>`**
|
|
135
|
+
- 开发测试:尝试目标涨跌幅并查看限幅结果。
|
|
136
|
+
- 参数 `percent`: 期望涨跌百分比,例如 `60` 或 `-40`。
|
|
137
|
+
|
|
118
138
|
## 💡 常见问题
|
|
119
139
|
|
|
140
|
+
**Q:Alpha版本有什么区别?**
|
|
141
|
+
A:本插件在发布一个稳定版之前会进行测试,但是股票的走向和时间有关,即使通过开发调试,也难以测试出稳定的结果。因此,在新版本开发出来后(尤其是算法上的更新),我不确定是否存在非致命但影响体验的漏洞。
|
|
142
|
+
|
|
143
|
+
Alpha版本就是在新版本稳定之前的**过渡版**,它们具备了一些没有验证的新功能、更新,但与之一同的是未知的bug。如需使用Alpha版本,请备份数据库和配置文件,防止以外发生。
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
120
147
|
**Q: 为什么买了股票没有立刻到账?**
|
|
121
148
|
A: 本插件设计了基于交易金额的动态冻结机制。交易额越大,冻结时间越长(可配置)。请使用 `stock.my` 查看剩余解冻时间。
|
|
122
149
|
|
|
150
|
+
---
|
|
151
|
+
|
|
123
152
|
**Q: 股价是如何波动的?**
|
|
124
|
-
A:
|
|
153
|
+
A: 股价采用**绝对价格计算模式**,由以下部分合成:
|
|
154
|
+
|
|
155
|
+
1. **宏观趋势**:周期内从起始价线性向目标价移动的进度价格。
|
|
156
|
+
2. **日内K线形态**:12种剧本叠加在趋势上的波动偏移(约±4%)。
|
|
157
|
+
3. **周内波浪**:正弦曲线叠加的平滑周期波动。
|
|
158
|
+
4. **微小噪音**:随机扰动避免价格过于规律。
|
|
159
|
+
|
|
160
|
+
新引擎完全避免增量累积误差,长期运行稳定可靠。
|
|
161
|
+
|
|
162
|
+
在Alpha.5版本以前,股价由四部分叠加而成:
|
|
125
163
|
1. **宏观趋势**:根据自动或手动的目标价格计算的线性趋势。
|
|
126
164
|
2. **周级波浪**:7天为一个大周期的正弦复合波浪。
|
|
127
165
|
3. **日内形态**:每天随机从 12 种剧本中选择多种(如 V 型、倒 V 型、阶梯上涨等)。
|
|
128
166
|
4. **随机噪音**:微小的随机波动。
|
|
167
|
+
|
|
168
|
+
新旧引擎的效果相同,仅仅是在算法上进行优化与修复。
|
|
169
|
+
|
|
170
|
+
此外,为保证市场稳定性,应用了**涨跌幅硬性限制**:相对于周周期起始价与当日开盘价的双重基准,股价的涨跌幅不得超过 ±50%。当随机或手动设置的目标超出限幅时,将自动截断至限幅边界。
|