koishi-plugin-monetary-bourse 2.0.2 → 2.0.3-Alpha.12
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 +192 -121
- package/package.json +1 -1
- package/readme.md +3 -61
package/lib/index.js
CHANGED
|
@@ -242,18 +242,24 @@ function apply(ctx, config) {
|
|
|
242
242
|
const bankDemand = await getBankDemandBalance(uid, currency);
|
|
243
243
|
logger.info(`pay: 现金=${cash}, 活期=${bankDemand}, 需要=${cost}`);
|
|
244
244
|
if (cash + bankDemand < cost) {
|
|
245
|
-
|
|
245
|
+
const msg = `资金不足!需要 ${cost.toFixed(2)},当前现金 ${cash.toFixed(2)} + 活期 ${bankDemand.toFixed(2)}`;
|
|
246
|
+
logger.warn(`pay 失败: ${msg}, uid=${uid}`);
|
|
247
|
+
return { success: false, msg };
|
|
246
248
|
}
|
|
247
249
|
let remainingCost = Number(cost.toFixed(2));
|
|
248
250
|
const cashDeduct = Number(Math.min(cash, remainingCost).toFixed(2));
|
|
249
251
|
if (cashDeduct > 0) {
|
|
250
252
|
const success = await changeCashBalance(uid, currency, -cashDeduct);
|
|
251
|
-
if (!success)
|
|
253
|
+
if (!success) {
|
|
254
|
+
logger.error(`pay 失败: 扣除现金失败 uid=${uid}, cost=${cashDeduct}`);
|
|
255
|
+
return { success: false, msg: "扣除现金失败,请重试" };
|
|
256
|
+
}
|
|
252
257
|
remainingCost = Number((remainingCost - cashDeduct).toFixed(2));
|
|
253
258
|
}
|
|
254
259
|
if (remainingCost > 0) {
|
|
255
260
|
const success = await deductBankDemand(uid, currency, remainingCost);
|
|
256
261
|
if (!success) {
|
|
262
|
+
logger.error(`pay 失败: 银行活期扣款失败 uid=${uid}, cost=${remainingCost}`);
|
|
257
263
|
if (cashDeduct > 0) await changeCashBalance(uid, currency, cashDeduct);
|
|
258
264
|
return { success: false, msg: "银行活期扣款失败" };
|
|
259
265
|
}
|
|
@@ -670,7 +676,7 @@ function apply(ctx, config) {
|
|
|
670
676
|
const morningVol = Math.exp(-8 * dayProgress);
|
|
671
677
|
const afternoonVol = Math.exp(-8 * (1 - dayProgress));
|
|
672
678
|
const volatility = 0.3 + morningVol * 0.5 + afternoonVol * 0.4;
|
|
673
|
-
const randomReturn = normalRandom *
|
|
679
|
+
const randomReturn = normalRandom * 65e-4 * volatility;
|
|
674
680
|
const totalReturn = patternReturn + reversionReturn + randomReturn;
|
|
675
681
|
let newPrice = currentPrice * (1 + totalReturn);
|
|
676
682
|
const dayBase = dailyOpenPrice ?? basePrice;
|
|
@@ -723,9 +729,12 @@ function apply(ctx, config) {
|
|
|
723
729
|
} else if (txn.type === "sell") {
|
|
724
730
|
if (txn.uid && typeof txn.uid === "number") {
|
|
725
731
|
const amount = Number(txn.cost.toFixed(2));
|
|
726
|
-
await changeCashBalance(txn.uid, config.currency, amount);
|
|
732
|
+
const success = await changeCashBalance(txn.uid, config.currency, amount);
|
|
733
|
+
if (!success) {
|
|
734
|
+
logger.error(`processPendingTransactions 失败: 卖出结算充值失败 txn.id=${txn.id}, uid=${txn.uid}, amount=${amount}`);
|
|
735
|
+
}
|
|
727
736
|
} else {
|
|
728
|
-
logger.warn(`processPendingTransactions
|
|
737
|
+
logger.warn(`processPendingTransactions 警告: 卖出订单缺少有效uid, txn.id=${txn.id}`);
|
|
729
738
|
}
|
|
730
739
|
}
|
|
731
740
|
await ctx.database.remove("bourse_pending", { id: txn.id });
|
|
@@ -746,7 +755,7 @@ function apply(ctx, config) {
|
|
|
746
755
|
}));
|
|
747
756
|
}
|
|
748
757
|
__name(getPriceHistory, "getPriceHistory");
|
|
749
|
-
ctx.command("stock [interval:string]", "查看股市行情").action(async ({ session }, interval) => {
|
|
758
|
+
ctx.command("stock [interval:string]", "查看股市行情").userFields(["id"]).action(async ({ session }, interval) => {
|
|
750
759
|
if (["buy", "sell", "my"].includes(interval)) {
|
|
751
760
|
const parts = session.content.trim().split(/\s+/).slice(2);
|
|
752
761
|
const rest = parts.join(" ");
|
|
@@ -774,7 +783,10 @@ function apply(ctx, config) {
|
|
|
774
783
|
});
|
|
775
784
|
history = history.reverse();
|
|
776
785
|
}
|
|
777
|
-
if (history.length === 0)
|
|
786
|
+
if (history.length === 0) {
|
|
787
|
+
logger.warn(`stock: 数据库中未找到 ${stockId} 的行情数据`);
|
|
788
|
+
return "暂无行情数据。";
|
|
789
|
+
}
|
|
778
790
|
if (history.length > 300) {
|
|
779
791
|
const step = Math.ceil(history.length / 300);
|
|
780
792
|
history = history.filter((_, index) => index % step === 0);
|
|
@@ -798,16 +810,29 @@ function apply(ctx, config) {
|
|
|
798
810
|
return img;
|
|
799
811
|
});
|
|
800
812
|
ctx.command("stock.buy <amount:number>", "买入股票").userFields(["id"]).action(async ({ session }, amount) => {
|
|
801
|
-
if (!amount || amount <= 0 || !Number.isInteger(amount))
|
|
813
|
+
if (!amount || amount <= 0 || !Number.isInteger(amount)) {
|
|
814
|
+
logger.warn(`stock.buy: 非法买入数量 user=${session.userId}, amount=${amount}`);
|
|
815
|
+
return "请输入有效的购买股数(整数)。";
|
|
816
|
+
}
|
|
802
817
|
if (!await isMarketOpen()) return "休市中,无法交易。";
|
|
803
|
-
const uid = session.user?.id;
|
|
804
818
|
const visibleUserId = session.userId;
|
|
805
|
-
if (!
|
|
819
|
+
if (!session.user || session.user.id == null) {
|
|
820
|
+
logger.error(`stock.buy: session.user 不存在或 id 为空 user=${session.userId}`);
|
|
806
821
|
return "无法获取用户ID,请稍后重试。";
|
|
807
822
|
}
|
|
823
|
+
let uid = session.user.id;
|
|
824
|
+
if (typeof uid !== "number") {
|
|
825
|
+
const parsedUid = Number(uid);
|
|
826
|
+
if (!parsedUid || isNaN(parsedUid)) {
|
|
827
|
+
logger.error(`stock.buy: 无法获取数字UID user=${session.userId}, rawId=${uid}`);
|
|
828
|
+
return "无法获取用户ID,请稍后重试。";
|
|
829
|
+
}
|
|
830
|
+
uid = parsedUid;
|
|
831
|
+
}
|
|
808
832
|
const cost = Number((currentPrice * amount).toFixed(2));
|
|
809
833
|
const payResult = await pay(uid, cost, config.currency);
|
|
810
834
|
if (!payResult.success) {
|
|
835
|
+
logger.warn(`stock.buy: 支付失败 user=${session.userId}, amount=${amount}, cost=${cost}, reason=${payResult.msg}`);
|
|
811
836
|
return payResult.msg;
|
|
812
837
|
}
|
|
813
838
|
let freezeMinutes = 0;
|
|
@@ -875,16 +900,30 @@ function apply(ctx, config) {
|
|
|
875
900
|
);
|
|
876
901
|
});
|
|
877
902
|
ctx.command("stock.sell <amount:number>", "卖出股票").userFields(["id"]).action(async ({ session }, amount) => {
|
|
878
|
-
if (!amount || amount <= 0 || !Number.isInteger(amount))
|
|
903
|
+
if (!amount || amount <= 0 || !Number.isInteger(amount)) {
|
|
904
|
+
logger.warn(`stock.sell: 非法卖出数量 user=${session.userId}, amount=${amount}`);
|
|
905
|
+
return "请输入有效的卖出股数。";
|
|
906
|
+
}
|
|
879
907
|
if (!await isMarketOpen()) return "休市中,无法交易。";
|
|
880
|
-
const uid = session.user?.id;
|
|
881
908
|
const visibleUserId = session.userId;
|
|
882
|
-
if (!
|
|
909
|
+
if (!session.user || session.user.id == null) {
|
|
910
|
+
logger.error(`stock.sell: session.user 不存在或 id 为空 user=${session.userId}`);
|
|
883
911
|
return "无法获取用户ID,请稍后重试。";
|
|
884
912
|
}
|
|
913
|
+
let uid = session.user.id;
|
|
914
|
+
if (typeof uid !== "number") {
|
|
915
|
+
const parsedUid = Number(uid);
|
|
916
|
+
if (!parsedUid || isNaN(parsedUid)) {
|
|
917
|
+
logger.error(`stock.sell: 无法获取数字UID user=${session.userId}, rawId=${uid}`);
|
|
918
|
+
return "无法获取用户ID,请稍后重试。";
|
|
919
|
+
}
|
|
920
|
+
uid = parsedUid;
|
|
921
|
+
}
|
|
885
922
|
const holding = await ctx.database.get("bourse_holding", { userId: visibleUserId, stockId });
|
|
886
923
|
if (holding.length === 0 || holding[0].amount < amount) {
|
|
887
|
-
|
|
924
|
+
const currentAmount = holding.length ? holding[0].amount : 0;
|
|
925
|
+
logger.warn(`stock.sell: 持仓不足 user=${session.userId}, amount=${amount}, current=${currentAmount}`);
|
|
926
|
+
return `持仓不足!当前持有: ${currentAmount} 股。`;
|
|
888
927
|
}
|
|
889
928
|
const currentHolding = holding[0];
|
|
890
929
|
let existingTotalCost = currentHolding.totalCost;
|
|
@@ -1021,7 +1060,10 @@ function apply(ctx, config) {
|
|
|
1021
1060
|
return img;
|
|
1022
1061
|
});
|
|
1023
1062
|
ctx.command("stock.control <price:number> [hours:number]", "管理员:设置宏观调控目标", { authority: 3 }).action(async ({ session }, price, hours) => {
|
|
1024
|
-
if (!price || price <= 0)
|
|
1063
|
+
if (!price || price <= 0) {
|
|
1064
|
+
logger.warn(`stock.control: 非法目标价格 user=${session.userId}, price=${price}`);
|
|
1065
|
+
return "请输入有效的目标价格。";
|
|
1066
|
+
}
|
|
1025
1067
|
const duration = hours || 24;
|
|
1026
1068
|
const now = /* @__PURE__ */ new Date();
|
|
1027
1069
|
const endTime = new Date(now.getTime() + duration * 3600 * 1e3);
|
|
@@ -1058,7 +1100,10 @@ function apply(ctx, config) {
|
|
|
1058
1100
|
到期后将自动切回随机调控。`;
|
|
1059
1101
|
});
|
|
1060
1102
|
ctx.command("bourse.admin.market <status>", "设置股市开关状态 (open/close/auto)", { authority: 3 }).action(async ({ session }, status) => {
|
|
1061
|
-
if (!["open", "close", "auto"].includes(status))
|
|
1103
|
+
if (!["open", "close", "auto"].includes(status)) {
|
|
1104
|
+
logger.warn(`bourse.admin.market: 非法状态 user=${session.userId}, status=${status}`);
|
|
1105
|
+
return "无效状态,请使用 open, close, 或 auto";
|
|
1106
|
+
}
|
|
1062
1107
|
const wasOpen = await isMarketOpen();
|
|
1063
1108
|
const key = "macro_state";
|
|
1064
1109
|
const existing = await ctx.database.get("bourse_state", { key });
|
|
@@ -1162,118 +1207,144 @@ function apply(ctx, config) {
|
|
|
1162
1207
|
是否继续波动:${moved ? "是" : "否(需检查)"}`;
|
|
1163
1208
|
});
|
|
1164
1209
|
async function renderHoldingImage(ctx2, username, holding, pending, currency) {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1210
|
+
try {
|
|
1211
|
+
const templatePath = (0, import_path.resolve)(__dirname, "templates", "holding-card.html");
|
|
1212
|
+
let template = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
1213
|
+
const data = {
|
|
1214
|
+
username,
|
|
1215
|
+
holding,
|
|
1216
|
+
pending,
|
|
1217
|
+
currency,
|
|
1218
|
+
updateTime: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN")
|
|
1219
|
+
};
|
|
1220
|
+
template = template.replace("{{DATA}}", JSON.stringify(data));
|
|
1221
|
+
const page = await ctx2.puppeteer.page();
|
|
1222
|
+
try {
|
|
1223
|
+
await page.setContent(template);
|
|
1224
|
+
const element = await page.$(".card");
|
|
1225
|
+
if (!element) throw new Error("找不到 .card 元素");
|
|
1226
|
+
const imgBuf = await element.screenshot({ encoding: "binary" });
|
|
1227
|
+
return import_koishi.h.image(imgBuf, "image/png");
|
|
1228
|
+
} finally {
|
|
1229
|
+
await page.close();
|
|
1230
|
+
}
|
|
1231
|
+
} catch (err) {
|
|
1232
|
+
logger.error("renderHoldingImage 失败:", err);
|
|
1233
|
+
return `[错误] 生成图片失败: ${err.message}`;
|
|
1234
|
+
}
|
|
1183
1235
|
}
|
|
1184
1236
|
__name(renderHoldingImage, "renderHoldingImage");
|
|
1185
1237
|
async function renderTradeResultImage(ctx2, tradeType, stockName, amount, tradePrice, totalCost, currency, priceHistory, sellInfo, newHolding, tradeMeta) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1238
|
+
try {
|
|
1239
|
+
const templatePath = (0, import_path.resolve)(__dirname, "templates", "trade-result.html");
|
|
1240
|
+
let template = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
1241
|
+
const tradeIndex = priceHistory.length - 1;
|
|
1242
|
+
const status = tradeMeta?.status ?? "settled";
|
|
1243
|
+
const pendingMinutes = tradeMeta?.pendingMinutes ?? 0;
|
|
1244
|
+
const pendingEndTime = tradeMeta?.pendingEndTime ?? null;
|
|
1245
|
+
const data = {
|
|
1246
|
+
tradeType,
|
|
1247
|
+
stockName,
|
|
1248
|
+
amount,
|
|
1249
|
+
tradePrice,
|
|
1250
|
+
totalCost,
|
|
1251
|
+
currency,
|
|
1252
|
+
tradeTime: (/* @__PURE__ */ new Date()).toLocaleString("zh-CN"),
|
|
1253
|
+
prices: priceHistory.map((d) => d.price),
|
|
1254
|
+
timestamps: priceHistory.map((d) => d.timestamp),
|
|
1255
|
+
tradeIndex,
|
|
1256
|
+
// 卖出额外信息
|
|
1257
|
+
avgBuyPrice: sellInfo?.avgBuyPrice ?? null,
|
|
1258
|
+
buyCost: sellInfo?.buyCost ?? null,
|
|
1259
|
+
profit: sellInfo?.profit ?? null,
|
|
1260
|
+
profitPercent: sellInfo?.profitPercent ?? null,
|
|
1261
|
+
// 买入后持仓
|
|
1262
|
+
newHolding: newHolding ?? amount,
|
|
1263
|
+
status,
|
|
1264
|
+
pendingMinutes,
|
|
1265
|
+
pendingEndTime
|
|
1266
|
+
};
|
|
1267
|
+
template = template.replace("{{DATA}}", JSON.stringify(data));
|
|
1268
|
+
const page = await ctx2.puppeteer.page();
|
|
1269
|
+
try {
|
|
1270
|
+
await page.setContent(template);
|
|
1271
|
+
const element = await page.$(".card");
|
|
1272
|
+
if (!element) throw new Error("找不到 .card 元素");
|
|
1273
|
+
const imgBuf = await element.screenshot({ encoding: "binary" });
|
|
1274
|
+
return import_koishi.h.image(imgBuf, "image/png");
|
|
1275
|
+
} finally {
|
|
1276
|
+
await page.close();
|
|
1277
|
+
}
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
logger.error("renderTradeResultImage 失败:", err);
|
|
1280
|
+
return `[错误] 生成交易确认单失败: ${err.message}`;
|
|
1281
|
+
}
|
|
1223
1282
|
}
|
|
1224
1283
|
__name(renderTradeResultImage, "renderTradeResultImage");
|
|
1225
1284
|
async function renderStockImage(ctx2, data, name2, viewLabel, current, high, low) {
|
|
1226
|
-
if (data.length < 2)
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1285
|
+
if (data.length < 2) {
|
|
1286
|
+
logger.warn(`renderStockImage: 数据点不足(${data.length}),无法绘制图表`);
|
|
1287
|
+
return "数据不足,无法绘制走势图。";
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const startPrice = data[0].price;
|
|
1291
|
+
const change = current - startPrice;
|
|
1292
|
+
const changePercent = change / startPrice * 100;
|
|
1293
|
+
const isUp = change >= 0;
|
|
1294
|
+
const templatePath = (0, import_path.resolve)(__dirname, "templates", "stock-chart.html");
|
|
1295
|
+
let html = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
1296
|
+
const colorScheme = {
|
|
1297
|
+
mainColor: isUp ? "#f23645" : "#089981",
|
|
1298
|
+
gradientStart: isUp ? "rgba(242, 54, 69, 0.25)" : "rgba(8, 153, 129, 0.25)",
|
|
1299
|
+
gradientEnd: "rgba(255, 255, 255, 0)",
|
|
1300
|
+
glowColor: isUp ? "rgba(242, 54, 69, 0.4)" : "rgba(8, 153, 129, 0.4)",
|
|
1301
|
+
iconGradientStart: isUp ? "#f23645" : "#089981",
|
|
1302
|
+
iconGradientEnd: isUp ? "#ff7e87" : "#40c2aa",
|
|
1303
|
+
iconShadow: isUp ? "rgba(242, 54, 69, 0.3)" : "rgba(8, 153, 129, 0.3)",
|
|
1304
|
+
changeBadgeBg: isUp ? "rgba(242, 54, 69, 0.12)" : "rgba(8, 153, 129, 0.12)"
|
|
1305
|
+
};
|
|
1306
|
+
const replacements = {
|
|
1307
|
+
"{{MAIN_COLOR}}": colorScheme.mainColor,
|
|
1308
|
+
"{{GRADIENT_START}}": colorScheme.gradientStart,
|
|
1309
|
+
"{{GRADIENT_END}}": colorScheme.gradientEnd,
|
|
1310
|
+
"{{GLOW_COLOR}}": colorScheme.glowColor,
|
|
1311
|
+
"{{ICON_GRADIENT_START}}": colorScheme.iconGradientStart,
|
|
1312
|
+
"{{ICON_GRADIENT_END}}": colorScheme.iconGradientEnd,
|
|
1313
|
+
"{{ICON_SHADOW}}": colorScheme.iconShadow,
|
|
1314
|
+
"{{CHANGE_BADGE_BG}}": colorScheme.changeBadgeBg,
|
|
1315
|
+
"{{STOCK_NAME}}": name2,
|
|
1316
|
+
"{{VIEW_LABEL}}": viewLabel,
|
|
1317
|
+
"{{CURRENT_TIME}}": (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }),
|
|
1318
|
+
"{{CURRENT_PRICE}}": current.toFixed(2),
|
|
1319
|
+
"{{CHANGE_VALUE}}": `${change >= 0 ? "+" : ""}${change.toFixed(2)}`,
|
|
1320
|
+
"{{CHANGE_ICON}}": change >= 0 ? "↑" : "↓",
|
|
1321
|
+
"{{CHANGE_PERCENT}}": Math.abs(changePercent).toFixed(2),
|
|
1322
|
+
"{{HIGH_PRICE}}": high.toFixed(2),
|
|
1323
|
+
"{{LOW_PRICE}}": low.toFixed(2),
|
|
1324
|
+
"{{AMPLITUDE}}": ((high - low) / startPrice * 100).toFixed(2),
|
|
1325
|
+
"{{START_PRICE}}": startPrice.toFixed(2),
|
|
1326
|
+
"{{UPDATE_TIME}}": (/* @__PURE__ */ new Date()).toLocaleString("zh-CN"),
|
|
1327
|
+
"{{PRICES}}": JSON.stringify(data.map((d) => d.price)),
|
|
1328
|
+
"{{TIMES}}": JSON.stringify(data.map((d) => d.time)),
|
|
1329
|
+
"{{TIMESTAMPS}}": JSON.stringify(data.map((d) => d.timestamp))
|
|
1330
|
+
};
|
|
1331
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
1332
|
+
html = html.replace(new RegExp(key, "g"), value);
|
|
1333
|
+
}
|
|
1334
|
+
const page = await ctx2.puppeteer.page();
|
|
1335
|
+
try {
|
|
1336
|
+
await page.setContent(html);
|
|
1337
|
+
const element = await page.$(".card");
|
|
1338
|
+
if (!element) throw new Error("找不到 .card 元素");
|
|
1339
|
+
const imgBuf = await element.screenshot({ encoding: "binary" });
|
|
1340
|
+
return import_koishi.h.image(imgBuf, "image/png");
|
|
1341
|
+
} finally {
|
|
1342
|
+
await page.close();
|
|
1343
|
+
}
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
logger.error("renderStockImage 失败:", err);
|
|
1346
|
+
return `[错误] 生成行情图失败: ${err.message}`;
|
|
1270
1347
|
}
|
|
1271
|
-
const page = await ctx2.puppeteer.page();
|
|
1272
|
-
await page.setContent(html);
|
|
1273
|
-
const element = await page.$(".card");
|
|
1274
|
-
const imgBuf = await element?.screenshot({ encoding: "binary" });
|
|
1275
|
-
await page.close();
|
|
1276
|
-
return import_koishi.h.image(imgBuf, "image/png");
|
|
1277
1348
|
}
|
|
1278
1349
|
__name(renderStockImage, "renderStockImage");
|
|
1279
1350
|
}
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -89,9 +89,9 @@
|
|
|
89
89
|
|
|
90
90
|
**Q:Alpha版本有什么区别?**
|
|
91
91
|
|
|
92
|
-
A
|
|
92
|
+
A:本插件在发布一个稳定版之前会进行测试,但是股票的走向和时间有关,即使通过开发调试,也难以测试出稳定的结果。因此,在新版本开发出来后(尤其是*算法*上的更新),我不确定是否存在非致命但影响体验的漏洞。
|
|
93
93
|
|
|
94
|
-
Alpha
|
|
94
|
+
Alpha版本就是在新版本稳定之前的**过渡版**,它们具备了一些没有验证的新功能等更新,但与之一同的是未知的bug。如需使用Alpha版本,请备份数据库和配置文件,防止以外发生。
|
|
95
95
|
|
|
96
96
|
---
|
|
97
97
|
|
|
@@ -137,62 +137,4 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
|
|
|
137
137
|
- **enableDebug**: 是否启用调试模式(默认 `false`)。开启后可使用 `bourse.test.*` 系列调试指令。
|
|
138
138
|
|
|
139
139
|
### 宏观调控
|
|
140
|
-
- 已移除配置项中的手动宏观调控字段;请使用管理员指令进行宏观调控。
|
|
141
|
-
|
|
142
|
-
## 更新记录
|
|
143
|
-
|
|
144
|
-
### 2.0.2
|
|
145
|
-
|
|
146
|
-
- **doc**: 更新同步更新Gitee和Github链接至`README`
|
|
147
|
-
|
|
148
|
-
### 2.0.1
|
|
149
|
-
- **fix(build)**: 将静态 HTML 打包进 `lib`,修复构建未更新静态页面的问题
|
|
150
|
-
|
|
151
|
-
### 2.0.0
|
|
152
|
-
- **perf/alg**: 提升随机噪声强度,实时曲线更具呼吸感,减少单调走势。
|
|
153
|
-
- **ui/layout**: 重构交易详情页布局,分离股票信息与价格展示区域,底部改为 Grid 布局,并将页面外间距增至 64px 以提升视觉呼吸感。
|
|
154
|
-
- **feat(ui/display)**: 优化状态徽标逻辑,支持「交易方向」与「挂单中」状态同时并列显示,并为 Pending 状态增加呼吸动画。
|
|
155
|
-
- **ui/typography**: 显著增大详情页关键数值与标签字号,回滚并强制使用 Roboto Mono 等宽字体,确保金融数据对齐与专业感。
|
|
156
|
-
- **fix(ui/trade)**: 交易结果回单在存在冻结时间时也会即时弹出;修正时间戳位置,将其强制固定于页面右下角。
|
|
157
|
-
|
|
158
|
-
### Alpha.10
|
|
159
|
-
- **feat(core)**: 引入 **25 种** 全新 K 线形态库;新增 `selectPatternByExpectation` 智能调度算法,按价格偏离度动态调整形态概率以实现“估值修复”。
|
|
160
|
-
- **feat(ui)**: 新增交易交割单(成交结算图),并将 HTML 模板模板化重构,支持高分辨率渲染与更友好的样式复用。
|
|
161
|
-
- **perf**: 优化持仓成本计算、挂单排队体验与兼容旧数据的处理逻辑。
|
|
162
|
-
|
|
163
|
-
### Alpha.9
|
|
164
|
-
- **fix(build)**: 将静态 HTML 打包进 `lib`,修复构建遗漏导致的插件无法加载问题。
|
|
165
|
-
|
|
166
|
-
### Alpha.8
|
|
167
|
-
- **feat(ui)**: 提取并模板化走势图(`src/templates/stock-chart.html`),全面视觉优化(深色玻璃拟态、坐标轴与字体改进、增强可读性)。
|
|
168
|
-
- **feat(queue)**: 将挂单改为用户串行排队,防止拆单规避冻结时长(T+0 防绕过)。
|
|
169
|
-
- **perf/alg**: 提升基础波动率与形态影响力,改进周波浪与均值回归;新增 `enableDebug` 调试开关以便开发验证。
|
|
170
|
-
|
|
171
|
-
### Alpha.7
|
|
172
|
-
- **feat(model)**: 引入几何布朗运动 + 均值回归,添加正态分布噪声与软着陆限幅,改善日内波动特性以提高拟真度。
|
|
173
|
-
|
|
174
|
-
### Alpha.6
|
|
175
|
-
- **refactor**: 采用百分比波动合成模型,解决长期振幅失控并提升稳定性。
|
|
176
|
-
- **fix**: 修正手动宏观调控在部分场景下重置基准价的问题。
|
|
177
|
-
|
|
178
|
-
### Alpha.5
|
|
179
|
-
- **refactor**: 重写走势引擎为绝对价格合成(基准价 + 趋势 + 日内波动 + 周波浪 + 噪声),修复长期运行抖动并强化 ±50% 涨跌幅限幅。
|
|
180
|
-
|
|
181
|
-
### 1.1.1 (Alpha.4)
|
|
182
|
-
- **feat**: 引入硬性 ±50% 涨跌幅上限并随机刷新宏观目标以增强稳定性和随机性。
|
|
183
|
-
- **fix**: 避免在更新负载中修改主键;移除过时的手动控制配置;新增调试命令以便验证逻辑。
|
|
184
|
-
|
|
185
|
-
### 1.1.0 (Alpha.3)
|
|
186
|
-
- **feat**: 新增持仓盈亏与成本追踪(`totalCost`)及持仓卡片渲染(Puppeteer),提升展示与交互体验。
|
|
187
|
-
- **fix**: 统一金额与价格为两位小数以保证精度与兼容性;当 `maxFreezeTime = 0` 时即时完成交易处理。
|
|
188
|
-
|
|
189
|
-
### 1.0.0
|
|
190
|
-
- **fix**: 修复因更新负载包含主键而导致的 `TypeError: cannot modify primary key`,提高数据库兼容性。
|
|
191
|
-
|
|
192
|
-
### Alpha.2
|
|
193
|
-
- **feat**: 扩展日内走势剧本并叠加周级波浪,改进 Puppeteer 渲染以减少横轴标签重叠与视觉冲突。
|
|
194
|
-
- **note**: 历史版本曾存在主键相关问题,后续版本已逐步修复。
|
|
195
|
-
|
|
196
|
-
### Alpha.1
|
|
197
|
-
- **feat**: 初始发布:实现买卖、挂单、资金冻结、历史行情存储及 `stock` 系列基础指令,支持与 `monetary` / `monetary-bank` 的基础联动。
|
|
198
|
-
|
|
140
|
+
- 已移除配置项中的手动宏观调控字段;请使用管理员指令进行宏观调控。
|