koishi-plugin-monetary-bourse 3.0.0-alpha.12 → 3.0.0-alpha.14
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 +6 -0
- package/lib/index.js +119 -51
- package/lib/templates/holding-card.html +15 -24
- package/lib/templates/stock-chart.html +10 -6
- package/lib/templates/trade-result.html +17 -35
- package/package.json +1 -1
- package/readme.md +11 -4
package/lib/index.d.ts
CHANGED
|
@@ -61,6 +61,7 @@ export interface BourseState {
|
|
|
61
61
|
endTime: Date;
|
|
62
62
|
marketOpenStatus?: "open" | "close" | "auto";
|
|
63
63
|
}
|
|
64
|
+
export declare const usage = "\n<div style=\"max-width: 800px; font-family: sans-serif; line-height: 1.6;\">\n <div style=\"margin-bottom: 24px;\">\n <h1 style=\"border-bottom: none; margin-bottom: 8px; font-size: 28px;\">\uD83D\uDCC8 monetary-bourse</h1>\n <p style=\"opacity: 0.8; font-size: 14px;\">\u57FA\u4E8E\u8D27\u5E01\u7CFB\u7EDF\u7684\u53EF\u89C6\u5316\u80A1\u7968\u4EA4\u6613\u6240\u63D2\u4EF6\uFF0C\u652F\u6301\u81EA\u52A8\u5B8F\u89C2\u8C03\u63A7\u4E0E\u62DF\u771F K \u7EBF\u5F62\u6001\u3002</p>\n </div>\n\n <h3>\u2699\uFE0F \u914D\u7F6E\u9879</h3>\n <ul style=\"margin-top: 8px; margin-bottom: 20px;\">\n <li><b>\u57FA\u7840\u8BBE\u7F6E</b>\uFF1A\u81EA\u5B9A\u4E49\u8D27\u5E01\u5355\u4F4D\uFF08\u9700\u4E0E monetary \u7CFB\u7EDF\u4E00\u81F4\uFF09\u3001\u80A1\u7968\u540D\u79F0\u3001\u521D\u59CB\u4EF7\u683C\uFF0C\u4EE5\u53CA\u5355\u4EBA\u6700\u5927\u6301\u4ED3\u9650\u989D\u3002</li>\n <li><b>\u80A1\u5E02\u5F00\u5173\u4E0E\u65F6\u95F4</b>\uFF1A\u63A7\u5236\u80A1\u5E02\u542F\u52A8\u72B6\u6001\uFF0C\u652F\u6301\u8BBE\u5B9A\u6BCF\u65E5\u5F00\u5E02 <code>openHour</code> \u4E0E\u4F11\u5E02 <code>closeHour</code> \u5B9E\u73B0\u81EA\u52A8\u542F\u505C\u3002</li>\n <li><b>\u9632\u5237\u51BB\u7ED3\u673A\u5236</b>\uFF1A\u9632\u6B62\u7528\u6237\u4F4E\u4E70\u9AD8\u5356\u9AD8\u9891\u5237\u5355\u3002\u901A\u8FC7 <code>freezeCostPerMinute</code> \u8C03\u6574\u8D44\u91D1\u4E0E\u6392\u961F\u65F6\u95F4\u6BD4\u4F8B\uFF1B\u8BBE\u5B9A <code>minFreezeTime</code> \u4E0E <code>maxFreezeTime</code> \u9632\u6B62\u8FC7\u957F\u6216\u8FC7\u77ED\u6392\u961F\u3002\u53EF\u5C06\u6700\u5C0F\u65F6\u95F4\u8BBE\u4E3A 0 \u4F7F\u5C0F\u989D\u4EA4\u6613\u79D2\u6210\u3002</li>\n <li><b>\u624B\u7EED\u8D39\u4E0E\u7CBE\u5EA6</b>\uFF1A\u53EF\u914D\u7F6E\u5356\u51FA\u624B\u7EED\u8D39 <code>sellFeePercent</code> \u63D0\u5347\u535A\u5F08\u6210\u672C\uFF1B\u82E5\u4F60\u4F7F\u7528\u7684\u901A\u8D27\u4E0D\u652F\u6301\u5C0F\u6570\uFF0C\u8BF7\u5F00\u542F <code>precisionInteger</code>\u3002</li>\n <li><b>\u5B8F\u89C2\u8C03\u63A7\u5F15\u64CE</b>\uFF1A\u8C03\u6574 <code>biasMax</code> \u9650\u5236\u671F\u671B\u504F\u501A\u7684\u6781\u7AEF\u503C\u3002\u6B64\u5916\uFF0C\u53EF\u4EE5\u56FA\u5B9A\u6BCF\u65E5\u5B9A\u671F\u5237\u65B0\u5B8F\u89C2\u76EE\u6807\u7684\u65F6\u523B\uFF0C\u4EE5\u4FBF\u5728\u4EBA\u6D41\u9AD8\u5CF0\u671F\u5236\u9020\u884C\u60C5\u7684\u660E\u786E\u8F6C\u6298\u3002</li>\n </ul>\n\n <h3>\uD83D\uDCD6 \u5F00\u53D1\u8005\u5EFA\u8BAE</h3>\n <p style=\"font-size: 14px; opacity: 0.85;\">\n \u90E8\u7F72\u521D\u671F\uFF0C\u5EFA\u8BAE\u5728\u4E0B\u65B9\u5F00\u542F <code>enableDebug</code>\uFF0C\u901A\u8FC7 <code>bourse.test.price</code> \u6216 <code>bourse.test.run</code> \u6307\u4EE4\u751F\u6210\u672A\u6765\u4E00\u6BB5\u65F6\u95F4\u7684\u6A21\u62DF\u91CF\u4EF7\u5207\u7247\uFF0C\u4EE5\u6B64\u9A8C\u8BC1\u5F53\u524D\u7684\u53C2\u6570\u914D\u7F6E\u662F\u5426\u7B26\u5408\u8D35\u7FA4\u7684\u5E02\u573A\u8282\u594F\u548C\u8D2D\u4E70\u529B\u6C34\u5E73\u3002\u8C03\u6574\u6700\u4F73\u540E\u518D\u5173\u95ED\u8C03\u8BD5\u9009\u9879\u3002\n </p>\n</div>\n";
|
|
64
65
|
export interface Config {
|
|
65
66
|
currency: string;
|
|
66
67
|
stockName: string;
|
|
@@ -73,6 +74,11 @@ export interface Config {
|
|
|
73
74
|
maxFreezeTime: number;
|
|
74
75
|
marketStatus: "open" | "close" | "auto";
|
|
75
76
|
enableDebug: boolean;
|
|
77
|
+
sellFeePercent: number;
|
|
78
|
+
precisionInteger: boolean;
|
|
79
|
+
fixedUpdateTime: boolean;
|
|
80
|
+
fixedUpdateHour: number;
|
|
81
|
+
biasMax: number;
|
|
76
82
|
}
|
|
77
83
|
export declare const Config: Schema<Config>;
|
|
78
84
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -23,7 +23,8 @@ __export(src_exports, {
|
|
|
23
23
|
Config: () => Config,
|
|
24
24
|
apply: () => apply,
|
|
25
25
|
inject: () => inject,
|
|
26
|
-
name: () => name
|
|
26
|
+
name: () => name,
|
|
27
|
+
usage: () => usage
|
|
27
28
|
});
|
|
28
29
|
module.exports = __toCommonJS(src_exports);
|
|
29
30
|
var import_koishi = require("koishi");
|
|
@@ -35,6 +36,28 @@ var inject = {
|
|
|
35
36
|
optional: ["monetary"]
|
|
36
37
|
};
|
|
37
38
|
var logger = new import_koishi.Logger("bourse");
|
|
39
|
+
var usage = `
|
|
40
|
+
<div style="max-width: 800px; font-family: sans-serif; line-height: 1.6;">
|
|
41
|
+
<div style="margin-bottom: 24px;">
|
|
42
|
+
<h1 style="border-bottom: none; margin-bottom: 8px; font-size: 28px;">📈 monetary-bourse</h1>
|
|
43
|
+
<p style="opacity: 0.8; font-size: 14px;">基于货币系统的可视化股票交易所插件,支持自动宏观调控与拟真 K 线形态。</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<h3>⚙️ 配置项</h3>
|
|
47
|
+
<ul style="margin-top: 8px; margin-bottom: 20px;">
|
|
48
|
+
<li><b>基础设置</b>:自定义货币单位(需与 monetary 系统一致)、股票名称、初始价格,以及单人最大持仓限额。</li>
|
|
49
|
+
<li><b>股市开关与时间</b>:控制股市启动状态,支持设定每日开市 <code>openHour</code> 与休市 <code>closeHour</code> 实现自动启停。</li>
|
|
50
|
+
<li><b>防刷冻结机制</b>:防止用户低买高卖高频刷单。通过 <code>freezeCostPerMinute</code> 调整资金与排队时间比例;设定 <code>minFreezeTime</code> 与 <code>maxFreezeTime</code> 防止过长或过短排队。可将最小时间设为 0 使小额交易秒成。</li>
|
|
51
|
+
<li><b>手续费与精度</b>:可配置卖出手续费 <code>sellFeePercent</code> 提升博弈成本;若你使用的通货不支持小数,请开启 <code>precisionInteger</code>。</li>
|
|
52
|
+
<li><b>宏观调控引擎</b>:调整 <code>biasMax</code> 限制期望偏倚的极端值。此外,可以固定每日定期刷新宏观目标的时刻,以便在人流高峰期制造行情的明确转折。</li>
|
|
53
|
+
</ul>
|
|
54
|
+
|
|
55
|
+
<h3>📖 开发者建议</h3>
|
|
56
|
+
<p style="font-size: 14px; opacity: 0.85;">
|
|
57
|
+
部署初期,建议在下方开启 <code>enableDebug</code>,通过 <code>bourse.test.price</code> 或 <code>bourse.test.run</code> 指令生成未来一段时间的模拟量价切片,以此验证当前的参数配置是否符合贵群的市场节奏和购买力水平。调整最佳后再关闭调试选项。
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
`;
|
|
38
61
|
var Config = import_koishi.Schema.intersect([
|
|
39
62
|
import_koishi.Schema.object({
|
|
40
63
|
currency: import_koishi.Schema.string().default("信用点").description("货币单位名称"),
|
|
@@ -56,6 +79,15 @@ var Config = import_koishi.Schema.intersect([
|
|
|
56
79
|
minFreezeTime: import_koishi.Schema.number().min(0).default(10).description("最小冻结时间(分钟)"),
|
|
57
80
|
maxFreezeTime: import_koishi.Schema.number().min(0).default(1440).description("最大交易冻结时间(分钟)")
|
|
58
81
|
}).description("冻结机制"),
|
|
82
|
+
import_koishi.Schema.object({
|
|
83
|
+
sellFeePercent: import_koishi.Schema.number().min(0).max(100).step(0.01).default(0).description("卖出手续费(%)"),
|
|
84
|
+
precisionInteger: import_koishi.Schema.boolean().default(false).description("是否将所有计数精度设置为整数")
|
|
85
|
+
}).description("手续费与精度"),
|
|
86
|
+
import_koishi.Schema.object({
|
|
87
|
+
fixedUpdateTime: import_koishi.Schema.boolean().default(false).description("是否固定宏观目标的更新时间"),
|
|
88
|
+
fixedUpdateHour: import_koishi.Schema.number().min(0).max(23).step(1).default(9).description("固定更新时间(小时,仅 fixedUpdateTime 为 true 时生效)"),
|
|
89
|
+
biasMax: import_koishi.Schema.number().min(0.1).max(0.9).step(0.01).default(0.45).description("宏观期望上下偏倚的最大值")
|
|
90
|
+
}).description("宏观调控高级设置"),
|
|
59
91
|
import_koishi.Schema.object({
|
|
60
92
|
enableDebug: import_koishi.Schema.boolean().default(false).description("启用调试模式(开启后可使用调试指令)")
|
|
61
93
|
}).description("开发者选项")
|
|
@@ -115,6 +147,14 @@ function apply(ctx, config) {
|
|
|
115
147
|
);
|
|
116
148
|
const stockId = "MAIN";
|
|
117
149
|
let currentPrice = Number(config.initialPrice.toFixed(2));
|
|
150
|
+
function fmtPrice(value) {
|
|
151
|
+
return config.precisionInteger ? Math.round(value) : Number(value.toFixed(2));
|
|
152
|
+
}
|
|
153
|
+
__name(fmtPrice, "fmtPrice");
|
|
154
|
+
function fmtAmount(value) {
|
|
155
|
+
return config.precisionInteger ? Math.round(value) : Number(value.toFixed(2));
|
|
156
|
+
}
|
|
157
|
+
__name(fmtAmount, "fmtAmount");
|
|
118
158
|
ctx.on("ready", async () => {
|
|
119
159
|
const history = await ctx.database.get(
|
|
120
160
|
"bourse_history",
|
|
@@ -122,7 +162,7 @@ function apply(ctx, config) {
|
|
|
122
162
|
{ limit: 1, sort: { time: "desc" } }
|
|
123
163
|
);
|
|
124
164
|
if (history.length > 0) {
|
|
125
|
-
currentPrice =
|
|
165
|
+
currentPrice = fmtPrice(history[0].price);
|
|
126
166
|
} else {
|
|
127
167
|
await ctx.database.create("bourse_history", {
|
|
128
168
|
stockId,
|
|
@@ -219,7 +259,7 @@ function apply(ctx, config) {
|
|
|
219
259
|
}
|
|
220
260
|
}
|
|
221
261
|
const current = Number(records[0].value || 0);
|
|
222
|
-
const newValue =
|
|
262
|
+
const newValue = fmtAmount(current + delta);
|
|
223
263
|
if (newValue < 0) {
|
|
224
264
|
logger.warn(
|
|
225
265
|
`changeCashBalance: 余额不足 current=${current}, delta=${delta}`
|
|
@@ -279,14 +319,14 @@ function apply(ctx, config) {
|
|
|
279
319
|
const tables = ctx.database.tables;
|
|
280
320
|
if (!tables || !("monetary_bank_int" in tables)) return false;
|
|
281
321
|
const demandRecords = await ctx.database.select("monetary_bank_int").where({ uid, currency, type: "demand" }).orderBy("settlementDate", "asc").execute();
|
|
282
|
-
let remaining =
|
|
322
|
+
let remaining = fmtAmount(amount);
|
|
283
323
|
for (const record of demandRecords) {
|
|
284
324
|
if (remaining <= 0) break;
|
|
285
325
|
if (record.amount <= remaining) {
|
|
286
|
-
remaining =
|
|
326
|
+
remaining = fmtAmount(remaining - record.amount);
|
|
287
327
|
await ctx.database.remove("monetary_bank_int", { id: record.id });
|
|
288
328
|
} else {
|
|
289
|
-
const newAmount =
|
|
329
|
+
const newAmount = fmtAmount(record.amount - remaining);
|
|
290
330
|
await ctx.database.set(
|
|
291
331
|
"monetary_bank_int",
|
|
292
332
|
{ id: record.id },
|
|
@@ -315,15 +355,15 @@ function apply(ctx, config) {
|
|
|
315
355
|
logger.warn(`pay 失败: ${msg}, uid=${uid}`);
|
|
316
356
|
return { success: false, msg };
|
|
317
357
|
}
|
|
318
|
-
let remainingCost =
|
|
319
|
-
const cashDeduct =
|
|
358
|
+
let remainingCost = fmtAmount(cost);
|
|
359
|
+
const cashDeduct = fmtAmount(Math.min(cash, remainingCost));
|
|
320
360
|
if (cashDeduct > 0) {
|
|
321
361
|
const success = await changeCashBalance(uid, currency, -cashDeduct);
|
|
322
362
|
if (!success) {
|
|
323
363
|
logger.error(`pay 失败: 扣除现金失败 uid=${uid}, cost=${cashDeduct}`);
|
|
324
364
|
return { success: false, msg: "扣除现金失败,请重试" };
|
|
325
365
|
}
|
|
326
|
-
remainingCost =
|
|
366
|
+
remainingCost = fmtAmount(remainingCost - cashDeduct);
|
|
327
367
|
}
|
|
328
368
|
if (remainingCost > 0) {
|
|
329
369
|
const success = await deductBankDemand(uid, currency, remainingCost);
|
|
@@ -617,7 +657,7 @@ function apply(ctx, config) {
|
|
|
617
657
|
const deviationThreshold = 0.05;
|
|
618
658
|
if (Math.abs(deviation) > deviationThreshold) {
|
|
619
659
|
const adjustmentStrength = Math.min(Math.abs(deviation) / 0.3, 1);
|
|
620
|
-
const maxBias =
|
|
660
|
+
const maxBias = config.biasMax;
|
|
621
661
|
if (deviation > 0) {
|
|
622
662
|
bullishProb = 0.33 + adjustmentStrength * maxBias;
|
|
623
663
|
bearishProb = 0.33 - adjustmentStrength * maxBias * 0.7;
|
|
@@ -703,7 +743,17 @@ function apply(ctx, config) {
|
|
|
703
743
|
if (now > endTime) needNewState = true;
|
|
704
744
|
}
|
|
705
745
|
const createAutoState = /* @__PURE__ */ __name(async () => {
|
|
706
|
-
|
|
746
|
+
let endTime;
|
|
747
|
+
if (config.fixedUpdateTime) {
|
|
748
|
+
endTime = new Date(now);
|
|
749
|
+
endTime.setHours(config.fixedUpdateHour, 0, 0, 0);
|
|
750
|
+
if (endTime <= now) {
|
|
751
|
+
endTime.setDate(endTime.getDate() + 1);
|
|
752
|
+
}
|
|
753
|
+
} else {
|
|
754
|
+
const durationHours = 7 * 24;
|
|
755
|
+
endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
|
|
756
|
+
}
|
|
707
757
|
const fluctuation = 0.25;
|
|
708
758
|
const targetRatio = 1 + (Math.random() * 2 - 1) * fluctuation;
|
|
709
759
|
let targetPrice2 = currentPrice * targetRatio;
|
|
@@ -711,7 +761,6 @@ function apply(ctx, config) {
|
|
|
711
761
|
currentPrice * 0.5,
|
|
712
762
|
Math.min(currentPrice * 1.5, targetPrice2)
|
|
713
763
|
);
|
|
714
|
-
const endTime = new Date(now.getTime() + durationHours * 3600 * 1e3);
|
|
715
764
|
const newState = {
|
|
716
765
|
key: "macro_state",
|
|
717
766
|
lastCycleStart: now,
|
|
@@ -801,7 +850,7 @@ function apply(ctx, config) {
|
|
|
801
850
|
}
|
|
802
851
|
newPrice = Math.max(lowerLimit, Math.min(upperLimit, newPrice));
|
|
803
852
|
if (newPrice < 1) newPrice = 1;
|
|
804
|
-
newPrice =
|
|
853
|
+
newPrice = fmtPrice(newPrice);
|
|
805
854
|
currentPrice = newPrice;
|
|
806
855
|
await ctx.database.create("bourse_history", {
|
|
807
856
|
stockId,
|
|
@@ -826,17 +875,17 @@ function apply(ctx, config) {
|
|
|
826
875
|
userId: txn.userId,
|
|
827
876
|
stockId,
|
|
828
877
|
amount: txn.amount,
|
|
829
|
-
totalCost:
|
|
878
|
+
totalCost: fmtAmount(txn.cost)
|
|
830
879
|
});
|
|
831
880
|
} else {
|
|
832
881
|
let existingCost = holding[0].totalCost;
|
|
833
882
|
if (!existingCost || existingCost <= 0) {
|
|
834
|
-
existingCost =
|
|
883
|
+
existingCost = fmtAmount(holding[0].amount * txn.price);
|
|
835
884
|
logger.info(
|
|
836
885
|
`processPendingTransactions: 旧持仓无成本记录,使用交易价格估算: ${holding[0].amount}股 * ${txn.price} = ${existingCost}`
|
|
837
886
|
);
|
|
838
887
|
}
|
|
839
|
-
const newTotalCost =
|
|
888
|
+
const newTotalCost = fmtAmount(existingCost + txn.cost);
|
|
840
889
|
await ctx.database.set(
|
|
841
890
|
"bourse_holding",
|
|
842
891
|
{ userId: txn.userId, stockId },
|
|
@@ -848,7 +897,7 @@ function apply(ctx, config) {
|
|
|
848
897
|
}
|
|
849
898
|
} else if (txn.type === "sell") {
|
|
850
899
|
if (txn.uid !== void 0 && txn.uid !== null && typeof txn.uid === "number" && !Number.isNaN(txn.uid)) {
|
|
851
|
-
const amount =
|
|
900
|
+
const amount = fmtAmount(txn.cost);
|
|
852
901
|
const success = await changeCashBalance(
|
|
853
902
|
txn.uid,
|
|
854
903
|
config.currency,
|
|
@@ -901,23 +950,22 @@ function apply(ctx, config) {
|
|
|
901
950
|
let history;
|
|
902
951
|
const now = /* @__PURE__ */ new Date();
|
|
903
952
|
if (interval === "day") {
|
|
904
|
-
const
|
|
953
|
+
const todayOpen = new Date(now);
|
|
954
|
+
todayOpen.setHours(config.openHour, 0, 0, 0);
|
|
955
|
+
const dayAgo = new Date(now.getTime() - 24 * 3600 * 1e3);
|
|
956
|
+
let startTime = todayOpen > dayAgo ? todayOpen : dayAgo;
|
|
957
|
+
if (startTime > now) startTime = dayAgo;
|
|
905
958
|
history = await ctx.database.get(
|
|
906
959
|
"bourse_history",
|
|
907
|
-
{
|
|
908
|
-
stockId,
|
|
909
|
-
time: { $gte: startTime }
|
|
910
|
-
},
|
|
960
|
+
{ stockId, time: { $gte: startTime } },
|
|
911
961
|
{ sort: { time: "asc" } }
|
|
912
962
|
);
|
|
913
963
|
} else if (interval === "week") {
|
|
914
|
-
const
|
|
964
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 3600 * 1e3);
|
|
965
|
+
weekAgo.setHours(config.openHour, 0, 0, 0);
|
|
915
966
|
history = await ctx.database.get(
|
|
916
967
|
"bourse_history",
|
|
917
|
-
{
|
|
918
|
-
stockId,
|
|
919
|
-
time: { $gte: startTime }
|
|
920
|
-
},
|
|
968
|
+
{ stockId, time: { $gte: weekAgo } },
|
|
921
969
|
{ sort: { time: "asc" } }
|
|
922
970
|
);
|
|
923
971
|
} else {
|
|
@@ -1004,7 +1052,7 @@ function apply(ctx, config) {
|
|
|
1004
1052
|
);
|
|
1005
1053
|
return "无法获取用户ID,请稍后重试。";
|
|
1006
1054
|
}
|
|
1007
|
-
const cost =
|
|
1055
|
+
const cost = fmtAmount(currentPrice * amount);
|
|
1008
1056
|
const payResult = await pay(uid, cost, config.currency);
|
|
1009
1057
|
if (!payResult.success) {
|
|
1010
1058
|
logger.warn(
|
|
@@ -1132,17 +1180,15 @@ function apply(ctx, config) {
|
|
|
1132
1180
|
const currentHolding = holding[0];
|
|
1133
1181
|
let existingTotalCost = currentHolding.totalCost;
|
|
1134
1182
|
if (!existingTotalCost || existingTotalCost <= 0) {
|
|
1135
|
-
existingTotalCost =
|
|
1136
|
-
(currentHolding.amount * currentPrice).toFixed(2)
|
|
1137
|
-
);
|
|
1183
|
+
existingTotalCost = fmtAmount(currentHolding.amount * currentPrice);
|
|
1138
1184
|
logger.info(
|
|
1139
1185
|
`stock.sell: 旧持仓无成本记录,使用当前市价估算: ${currentHolding.amount}股 * ${currentPrice} = ${existingTotalCost}`
|
|
1140
1186
|
);
|
|
1141
1187
|
}
|
|
1142
|
-
const avgCostPerShare =
|
|
1143
|
-
|
|
1188
|
+
const avgCostPerShare = fmtPrice(
|
|
1189
|
+
existingTotalCost / currentHolding.amount
|
|
1144
1190
|
);
|
|
1145
|
-
const soldCost =
|
|
1191
|
+
const soldCost = fmtAmount(avgCostPerShare * amount);
|
|
1146
1192
|
const newAmount = currentHolding.amount - amount;
|
|
1147
1193
|
if (newAmount === 0) {
|
|
1148
1194
|
await ctx.database.remove("bourse_holding", {
|
|
@@ -1150,7 +1196,7 @@ function apply(ctx, config) {
|
|
|
1150
1196
|
stockId
|
|
1151
1197
|
});
|
|
1152
1198
|
} else {
|
|
1153
|
-
const newTotalCost =
|
|
1199
|
+
const newTotalCost = fmtAmount(existingTotalCost - soldCost);
|
|
1154
1200
|
await ctx.database.set(
|
|
1155
1201
|
"bourse_holding",
|
|
1156
1202
|
{ userId: visibleUserId, stockId },
|
|
@@ -1161,10 +1207,13 @@ function apply(ctx, config) {
|
|
|
1161
1207
|
}
|
|
1162
1208
|
);
|
|
1163
1209
|
}
|
|
1164
|
-
const gain =
|
|
1210
|
+
const gain = fmtAmount(currentPrice * amount);
|
|
1211
|
+
const feePercent = config.sellFeePercent;
|
|
1212
|
+
const fee = feePercent > 0 ? fmtAmount(gain * feePercent / 100) : 0;
|
|
1213
|
+
const netGain = fmtAmount(gain - fee);
|
|
1165
1214
|
let freezeMinutes = 0;
|
|
1166
1215
|
if (config.maxFreezeTime > 0) {
|
|
1167
|
-
freezeMinutes =
|
|
1216
|
+
freezeMinutes = netGain / config.freezeCostPerMinute;
|
|
1168
1217
|
if (freezeMinutes > config.maxFreezeTime)
|
|
1169
1218
|
freezeMinutes = config.maxFreezeTime;
|
|
1170
1219
|
if (freezeMinutes < config.minFreezeTime)
|
|
@@ -1191,12 +1240,12 @@ function apply(ctx, config) {
|
|
|
1191
1240
|
type: "sell",
|
|
1192
1241
|
amount,
|
|
1193
1242
|
price: currentPrice,
|
|
1194
|
-
cost:
|
|
1243
|
+
cost: netGain,
|
|
1195
1244
|
startTime,
|
|
1196
1245
|
endTime
|
|
1197
1246
|
});
|
|
1198
1247
|
const hasCostRecord = existingTotalCost > 0;
|
|
1199
|
-
const profit = hasCostRecord ?
|
|
1248
|
+
const profit = hasCostRecord ? fmtAmount(netGain - soldCost) : null;
|
|
1200
1249
|
const profitPercent = hasCostRecord && soldCost > 0 ? Number((profit / soldCost * 100).toFixed(2)) : null;
|
|
1201
1250
|
const tradeMeta = freezeMinutes === 0 ? {
|
|
1202
1251
|
status: "settled",
|
|
@@ -1223,7 +1272,9 @@ function apply(ctx, config) {
|
|
|
1223
1272
|
avgBuyPrice: hasCostRecord ? avgCostPerShare : null,
|
|
1224
1273
|
buyCost: hasCostRecord ? soldCost : null,
|
|
1225
1274
|
profit,
|
|
1226
|
-
profitPercent
|
|
1275
|
+
profitPercent,
|
|
1276
|
+
fee: fee > 0 ? fee : null,
|
|
1277
|
+
feePercent: feePercent > 0 ? feePercent : null
|
|
1227
1278
|
},
|
|
1228
1279
|
void 0,
|
|
1229
1280
|
tradeMeta
|
|
@@ -1243,7 +1294,9 @@ function apply(ctx, config) {
|
|
|
1243
1294
|
avgBuyPrice: hasCostRecord ? avgCostPerShare : null,
|
|
1244
1295
|
buyCost: hasCostRecord ? soldCost : null,
|
|
1245
1296
|
profit,
|
|
1246
|
-
profitPercent
|
|
1297
|
+
profitPercent,
|
|
1298
|
+
fee: fee > 0 ? fee : null,
|
|
1299
|
+
feePercent: feePercent > 0 ? feePercent : null
|
|
1247
1300
|
},
|
|
1248
1301
|
void 0,
|
|
1249
1302
|
tradeMeta
|
|
@@ -1262,16 +1315,16 @@ function apply(ctx, config) {
|
|
|
1262
1315
|
let holdingData = null;
|
|
1263
1316
|
if (holdings.length > 0) {
|
|
1264
1317
|
const h2 = holdings[0];
|
|
1265
|
-
const marketValue =
|
|
1318
|
+
const marketValue = fmtAmount(h2.amount * currentPrice);
|
|
1266
1319
|
const hasCostData = h2.totalCost !== void 0 && h2.totalCost !== null && h2.totalCost > 0;
|
|
1267
|
-
const totalCost = hasCostData ?
|
|
1268
|
-
const avgCost = hasCostData && h2.amount > 0 ?
|
|
1269
|
-
const profit = hasCostData ?
|
|
1320
|
+
const totalCost = hasCostData ? fmtAmount(h2.totalCost) : 0;
|
|
1321
|
+
const avgCost = hasCostData && h2.amount > 0 ? fmtPrice(totalCost / h2.amount) : 0;
|
|
1322
|
+
const profit = hasCostData ? fmtAmount(marketValue - totalCost) : null;
|
|
1270
1323
|
const profitPercent = hasCostData && totalCost > 0 ? Number((profit / totalCost * 100).toFixed(2)) : null;
|
|
1271
1324
|
holdingData = {
|
|
1272
1325
|
stockName: config.stockName,
|
|
1273
1326
|
amount: h2.amount,
|
|
1274
|
-
currentPrice:
|
|
1327
|
+
currentPrice: fmtPrice(currentPrice),
|
|
1275
1328
|
avgCost: hasCostData ? avgCost : null,
|
|
1276
1329
|
// null 表示无成本记录
|
|
1277
1330
|
totalCost: hasCostData ? totalCost : null,
|
|
@@ -1291,8 +1344,8 @@ function apply(ctx, config) {
|
|
|
1291
1344
|
type: p.type === "buy" ? "买入" : "卖出",
|
|
1292
1345
|
typeClass: p.type,
|
|
1293
1346
|
amount: p.amount,
|
|
1294
|
-
price:
|
|
1295
|
-
cost:
|
|
1347
|
+
price: fmtPrice(p.price),
|
|
1348
|
+
cost: fmtAmount(p.cost),
|
|
1296
1349
|
timeLeft: `${minutes}分${seconds}秒`
|
|
1297
1350
|
};
|
|
1298
1351
|
});
|
|
@@ -1541,6 +1594,8 @@ function apply(ctx, config) {
|
|
|
1541
1594
|
buyCost: sellInfo?.buyCost ?? null,
|
|
1542
1595
|
profit: sellInfo?.profit ?? null,
|
|
1543
1596
|
profitPercent: sellInfo?.profitPercent ?? null,
|
|
1597
|
+
fee: sellInfo?.fee ?? null,
|
|
1598
|
+
feePercent: sellInfo?.feePercent ?? null,
|
|
1544
1599
|
// 买入后持仓
|
|
1545
1600
|
newHolding: newHolding ?? amount,
|
|
1546
1601
|
status,
|
|
@@ -1577,6 +1632,17 @@ function apply(ctx, config) {
|
|
|
1577
1632
|
const klineData = chartType === "kline" ? buildKLineData(data, klineBucketMs ?? 2 * 3600 * 1e3) : [];
|
|
1578
1633
|
const templatePath = (0, import_path.resolve)(__dirname, "templates", "stock-chart.html");
|
|
1579
1634
|
let html = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
1635
|
+
const g2Path = (0, import_path.resolve)(__dirname, "templates", "g2.min.js");
|
|
1636
|
+
let g2Script = "";
|
|
1637
|
+
try {
|
|
1638
|
+
g2Script = await import_fs.promises.readFile(g2Path, "utf-8");
|
|
1639
|
+
g2Script = g2Script.replace(/<\/script>/g, "<\\/script>");
|
|
1640
|
+
} catch (err) {
|
|
1641
|
+
logger.warn(
|
|
1642
|
+
`renderStockImage: failed to read local G2 script: ${g2Path}`,
|
|
1643
|
+
err
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1580
1646
|
const colorScheme = {
|
|
1581
1647
|
mainColor: isUp ? "#f23645" : "#089981",
|
|
1582
1648
|
gradientStart: isUp ? "rgba(242, 54, 69, 0.25)" : "rgba(8, 153, 129, 0.25)",
|
|
@@ -1615,10 +1681,11 @@ function apply(ctx, config) {
|
|
|
1615
1681
|
"{{PRICES}}": JSON.stringify(data.map((d) => d.price)),
|
|
1616
1682
|
"{{TIMES}}": JSON.stringify(data.map((d) => d.time)),
|
|
1617
1683
|
"{{TIMESTAMPS}}": JSON.stringify(data.map((d) => d.timestamp)),
|
|
1618
|
-
"{{KLINE_DATA}}": JSON.stringify(klineData)
|
|
1684
|
+
"{{KLINE_DATA}}": JSON.stringify(klineData),
|
|
1685
|
+
"{{G2_SCRIPT}}": g2Script
|
|
1619
1686
|
};
|
|
1620
1687
|
for (const [key, value] of Object.entries(replacements)) {
|
|
1621
|
-
html = html.replace(new RegExp(key, "g"), value);
|
|
1688
|
+
html = html.replace(new RegExp(key, "g"), () => value);
|
|
1622
1689
|
}
|
|
1623
1690
|
const page = await ctx2.puppeteer.page();
|
|
1624
1691
|
try {
|
|
@@ -1698,5 +1765,6 @@ __name(apply, "apply");
|
|
|
1698
1765
|
Config,
|
|
1699
1766
|
apply,
|
|
1700
1767
|
inject,
|
|
1701
|
-
name
|
|
1768
|
+
name,
|
|
1769
|
+
usage
|
|
1702
1770
|
});
|
|
@@ -202,6 +202,10 @@
|
|
|
202
202
|
border-top: 1px solid var(--border-color);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
.profit-section.up { background: var(--profit-up-bg); }
|
|
206
|
+
.profit-section.down { background: var(--profit-down-bg); }
|
|
207
|
+
.profit-section.no-data { background: rgba(255, 255, 255, 0.03); }
|
|
208
|
+
|
|
205
209
|
.profit-label {
|
|
206
210
|
font-size: 13px;
|
|
207
211
|
font-weight: 600;
|
|
@@ -214,6 +218,10 @@
|
|
|
214
218
|
font-family: 'Roboto Mono';
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
.profit-value.up { color: var(--profit-up); }
|
|
222
|
+
.profit-value.down { color: var(--profit-down); }
|
|
223
|
+
.profit-value.no-data { color: #8b919e; }
|
|
224
|
+
|
|
217
225
|
.profit-percent {
|
|
218
226
|
font-size: 13px;
|
|
219
227
|
font-weight: 600;
|
|
@@ -413,13 +421,6 @@
|
|
|
413
421
|
// 从 JSON 标签中读取数据
|
|
414
422
|
const DATA = JSON.parse(document.getElementById('data-source').textContent);
|
|
415
423
|
|
|
416
|
-
// 统一字体:数字和金额统一使用与其他页面一致的等宽字体
|
|
417
|
-
const monoTargets = document.querySelectorAll('.stat-value, .profit-value, .pending-amount, .pending-price, .pending-cost, .pending-type, .stock-amount')
|
|
418
|
-
monoTargets.forEach(el => {
|
|
419
|
-
el.style.fontFamily = "'Roboto Mono','Trebuchet MS','Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif"
|
|
420
|
-
el.style.letterSpacing = '-0.2px'
|
|
421
|
-
})
|
|
422
|
-
|
|
423
424
|
// 渲染用户信息
|
|
424
425
|
document.getElementById('avatar').textContent = DATA.username.charAt(0).toUpperCase();
|
|
425
426
|
document.getElementById('username').textContent = DATA.username;
|
|
@@ -431,26 +432,16 @@
|
|
|
431
432
|
const h = DATA.holding;
|
|
432
433
|
const hasCostData = h.totalCost !== null;
|
|
433
434
|
const isProfit = hasCostData ? h.profit >= 0 : true;
|
|
434
|
-
|
|
435
|
-
// 适配深色模式的颜色
|
|
436
|
-
const profitColor = isProfit ? '#f87171' : '#4ade80';
|
|
437
435
|
const profitSign = isProfit ? '+' : '';
|
|
438
|
-
const
|
|
436
|
+
const profitClass = hasCostData ? (isProfit ? 'up' : 'down') : 'no-data';
|
|
439
437
|
|
|
440
|
-
const profitSection =
|
|
441
|
-
<div class="profit-section
|
|
442
|
-
<div class="profit-label">盈亏</div>
|
|
443
|
-
<div class="profit-value" style="color: ${profitColor}">
|
|
444
|
-
${profitSign}${h.profit.toFixed(2)} ${DATA.currency}
|
|
445
|
-
<span class="profit-percent">(${profitSign}${h.profitPercent.toFixed(2)}%)</span>
|
|
446
|
-
</div>
|
|
447
|
-
</div>
|
|
448
|
-
` : `
|
|
449
|
-
<div class="profit-section no-data" style="background: rgba(255, 255, 255, 0.03)">
|
|
438
|
+
const profitSection = `
|
|
439
|
+
<div class="profit-section ${profitClass}">
|
|
450
440
|
<div class="profit-label">盈亏</div>
|
|
451
|
-
<div class="profit-value
|
|
452
|
-
|
|
453
|
-
<span class="profit-
|
|
441
|
+
<div class="profit-value ${profitClass}">
|
|
442
|
+
${hasCostData ? `${profitSign}${h.profit.toFixed(2)} ${DATA.currency}
|
|
443
|
+
<span class="profit-percent">(${profitSign}${h.profitPercent.toFixed(2)}%)</span>` : `暂无成本记录
|
|
444
|
+
<span class="profit-hint">(新交易后将自动记录)</span>`}
|
|
454
445
|
</div>
|
|
455
446
|
</div>
|
|
456
447
|
`;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
--icon-gradient-end: {{ICON_GRADIENT_END}};
|
|
9
9
|
--icon-shadow: {{ICON_SHADOW}};
|
|
10
10
|
--change-badge-bg: {{CHANGE_BADGE_BG}};
|
|
11
|
+
--rise-color: #f23645;
|
|
12
|
+
--fall-color: #089981;
|
|
11
13
|
|
|
12
14
|
--bg-color: #0c0f15;
|
|
13
15
|
--card-bg: #161b22;
|
|
@@ -256,14 +258,16 @@
|
|
|
256
258
|
<div class="update-time">Updated {{UPDATE_TIME}}</div>
|
|
257
259
|
</div>
|
|
258
260
|
|
|
261
|
+
<script>{{G2_SCRIPT}}</script>
|
|
259
262
|
<script>
|
|
260
263
|
const chartType = "{{CHART_TYPE}}";
|
|
261
|
-
const klineData =
|
|
262
|
-
const prices =
|
|
263
|
-
const times =
|
|
264
|
-
const timestamps =
|
|
265
|
-
const
|
|
266
|
-
const
|
|
264
|
+
const klineData = {{KLINE_DATA}};
|
|
265
|
+
const prices = {{PRICES}};
|
|
266
|
+
const times = {{TIMES}};
|
|
267
|
+
const timestamps = {{TIMESTAMPS}};
|
|
268
|
+
const rootStyle = getComputedStyle(document.documentElement);
|
|
269
|
+
const riseColor = rootStyle.getPropertyValue("--rise-color").trim();
|
|
270
|
+
const fallColor = rootStyle.getPropertyValue("--fall-color").trim();
|
|
267
271
|
|
|
268
272
|
function markReady(className) {
|
|
269
273
|
document.body.classList.add(className || "chart-ready");
|
|
@@ -240,11 +240,15 @@
|
|
|
240
240
|
/* 增加高度以容纳更大的字体 */
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
.stats-grid.sell-grid {
|
|
244
|
+
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
245
|
+
}
|
|
246
|
+
|
|
243
247
|
.stat-item {
|
|
244
248
|
background: var(--item-bg);
|
|
245
249
|
border: 1px solid var(--border-color);
|
|
246
250
|
border-radius: 12px;
|
|
247
|
-
padding: 20px
|
|
251
|
+
padding: 16px 20px;
|
|
248
252
|
display: flex;
|
|
249
253
|
flex-direction: column;
|
|
250
254
|
justify-content: center;
|
|
@@ -369,26 +373,6 @@
|
|
|
369
373
|
</script>
|
|
370
374
|
|
|
371
375
|
<script>
|
|
372
|
-
// 模拟数据用于测试
|
|
373
|
-
/*
|
|
374
|
-
const TEST_DATA = {
|
|
375
|
-
tradeType: 'buy',
|
|
376
|
-
stockName: 'NVDA · NVIDIA Corp',
|
|
377
|
-
status: 'pending',
|
|
378
|
-
pendingMinutes: 5.5,
|
|
379
|
-
pendingEndTime: '14:35',
|
|
380
|
-
tradePrice: 485.20,
|
|
381
|
-
amount: 10,
|
|
382
|
-
totalCost: 4852.00,
|
|
383
|
-
tradeTime: '2023-10-27 10:30:05',
|
|
384
|
-
newHolding: 150,
|
|
385
|
-
currency: 'USD',
|
|
386
|
-
prices: [480, 482, 481, 483, 485, 484, 486, 485.2, 487, 488, 486],
|
|
387
|
-
timestamps: [1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 11000],
|
|
388
|
-
tradeIndex: 7
|
|
389
|
-
};
|
|
390
|
-
*/
|
|
391
|
-
|
|
392
376
|
let DATA;
|
|
393
377
|
try {
|
|
394
378
|
DATA = JSON.parse(document.getElementById('data-source').textContent);
|
|
@@ -400,16 +384,7 @@
|
|
|
400
384
|
const status = DATA.status || 'settled';
|
|
401
385
|
const isPending = status === 'pending';
|
|
402
386
|
|
|
403
|
-
const
|
|
404
|
-
buy: '#f87171',
|
|
405
|
-
sell: '#4ade80',
|
|
406
|
-
pending: '#eab308',
|
|
407
|
-
grid: '#30363d',
|
|
408
|
-
text: '#f0f3f5',
|
|
409
|
-
textDim: '#8b949e'
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const mainColor = isBuy ? colors.buy : colors.sell;
|
|
387
|
+
const mainColor = isBuy ? '#f87171' : '#4ade80';
|
|
413
388
|
|
|
414
389
|
// --- 1. 渲染顶部基本信息 ---
|
|
415
390
|
document.getElementById('stock-name').textContent = DATA.stockName || 'Unknown Stock';
|
|
@@ -465,7 +440,9 @@
|
|
|
465
440
|
const isProfit = profit >= 0;
|
|
466
441
|
const profitClass = isProfit ? 'profit' : 'loss';
|
|
467
442
|
const profitSign = isProfit ? '+' : '';
|
|
443
|
+
const hasFee = DATA.fee !== null && DATA.fee > 0;
|
|
468
444
|
|
|
445
|
+
statsGrid.classList.add('sell-grid');
|
|
469
446
|
statsGrid.innerHTML = `
|
|
470
447
|
<div class="stat-item highlight">
|
|
471
448
|
<div class="stat-label">Revenue 卖出金额</div>
|
|
@@ -473,9 +450,14 @@
|
|
|
473
450
|
<div class="stat-sub">${DATA.currency}</div>
|
|
474
451
|
</div>
|
|
475
452
|
<div class="stat-item">
|
|
476
|
-
<div class="stat-label">
|
|
477
|
-
<div class="stat-value">${
|
|
478
|
-
<div class="stat-sub"
|
|
453
|
+
<div class="stat-label">Fee 手续费</div>
|
|
454
|
+
<div class="stat-value">${hasFee ? DATA.fee.toFixed(2) : '--'}</div>
|
|
455
|
+
<div class="stat-sub">${DATA.currency}${hasFee ? ` (${DATA.feePercent}%)` : ''}</div>
|
|
456
|
+
</div>
|
|
457
|
+
<div class="stat-item">
|
|
458
|
+
<div class="stat-label">Net 到账</div>
|
|
459
|
+
<div class="stat-value">${(DATA.totalCost - (DATA.fee || 0)).toFixed(2)}</div>
|
|
460
|
+
<div class="stat-sub">${DATA.currency}</div>
|
|
479
461
|
</div>
|
|
480
462
|
<div class="stat-item ${profitClass}">
|
|
481
463
|
<div class="stat-label">P/L 盈亏</div>
|
|
@@ -530,7 +512,7 @@
|
|
|
530
512
|
|
|
531
513
|
// 价格标签 (字体也同步调整为 Roboto Mono 且加大)
|
|
532
514
|
const val = yMin + (i / gridLines) * yRange;
|
|
533
|
-
ctx.fillStyle =
|
|
515
|
+
ctx.fillStyle = '#8b949e';
|
|
534
516
|
// 修改 1 & 2: 增大字号到 14px 并使用 Roboto Mono
|
|
535
517
|
ctx.font = '500 14px "Roboto Mono", monospace';
|
|
536
518
|
ctx.textAlign = 'left';
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
本插件模拟了一个具备自动宏观调控、25种经典K线形态、智能概率博弈和可视化交割单的深度拟真股票市场。用户可以使用机器人通用的货币(如信用点)进行股票买卖、炒股理财。
|
|
8
8
|
|
|
9
|
-
> 版本:**
|
|
10
|
-
|
|
9
|
+
> 版本:**3.0.0-alpha.14**
|
|
11
10
|
## ✨ 特性
|
|
12
11
|
|
|
13
12
|
- **📈 拟真 K 线引擎**:
|
|
@@ -21,6 +20,8 @@
|
|
|
21
20
|
- **❄️ 资金冻结与挂单排队**:
|
|
22
21
|
- 交易采用 T+0 机制,但大额资金/股票会根据金额计算**动态冻结时间**。
|
|
23
22
|
- 挂单采用**串行排队模式**,同一用户的多个挂单需依次读秒,防止通过拆单绕过冻结机制,增加博弈深度。
|
|
23
|
+
- **💸 手续费与精度控制**:支持卖出手续费配置,回单展示净到账金额;可开启整数精度模式,适配不支持小数的货币体系。
|
|
24
|
+
- **🧭 宏观周期固定刷新**:支持固定时刻刷新宏观目标周期,便于在活动时段塑造更清晰的趋势节奏。
|
|
24
25
|
- **🏦 银行联动**:支持与[ `koishi-plugin-monetary-bank`](https://github.com/BYWled/koishi-plugin-monetary-bank)[](https://www.npmjs.com/package/koishi-plugin-monetary-bank) 联动,现金不足时自动扣除银行活期存款。
|
|
25
26
|
|
|
26
27
|
## 📦 依赖
|
|
@@ -133,11 +134,17 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
|
|
|
133
134
|
- **minFreezeTime**: 最小冻结时间(分钟,默认 `10`)。
|
|
134
135
|
- **maxFreezeTime**: 最大冻结时间(分钟,默认 `1440` 即24小时)。
|
|
135
136
|
|
|
137
|
+
### 手续费与精度
|
|
138
|
+
- **sellFeePercent**: 卖出手续费百分比(默认 `0`)。
|
|
139
|
+
- **precisionInteger**: 是否启用整数精度(默认 `false`)。
|
|
140
|
+
|
|
136
141
|
### 开发者选项
|
|
137
142
|
- **enableDebug**: 是否启用调试模式(默认 `false`)。开启后可使用 `bourse.test.*` 系列调试指令。
|
|
138
143
|
|
|
139
|
-
###
|
|
140
|
-
-
|
|
144
|
+
### 宏观调控高级设置
|
|
145
|
+
- **fixedUpdateTime**: 是否固定宏观目标的更新时间(默认 `false`)。
|
|
146
|
+
- **fixedUpdateHour**: 固定更新时间(小时,0-23,默认 `9`)。仅在 `fixedUpdateTime` 为 `true` 时生效。
|
|
147
|
+
- **biasMax**: 宏观期望上下偏倚的最大值(默认 `0.45`)。
|
|
141
148
|
|
|
142
149
|
## 📝 更新日志
|
|
143
150
|
|