koishi-plugin-monetary-bourse 3.0.0-alpha.19 → 3.0.0
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/assets/fonts/README.md +8 -0
- package/assets/fonts/RobotoMono-Bold.ttf +0 -0
- package/assets/fonts/RobotoMono-Regular.ttf +0 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +189 -21
- package/lib/templates/holding-card.html +3 -1
- package/lib/templates/stock-chart.html +2 -0
- package/lib/templates/trade-result.html +3 -1
- package/lib/utils/broadcast.d.ts +6 -0
- package/lib/utils/holding-summary.d.ts +25 -0
- package/package.json +7 -2
- package/readme.md +31 -5
|
Binary file
|
|
Binary file
|
package/lib/index.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface BoursePending {
|
|
|
43
43
|
amount: number;
|
|
44
44
|
price: number;
|
|
45
45
|
cost: number;
|
|
46
|
+
buyCost?: number;
|
|
46
47
|
startTime: Date;
|
|
47
48
|
endTime: Date;
|
|
48
49
|
}
|
|
@@ -91,6 +92,7 @@ export interface Config {
|
|
|
91
92
|
biasMax: number;
|
|
92
93
|
dividendIntervalDays: number;
|
|
93
94
|
maxDividendRate: number;
|
|
95
|
+
dividendBroadcastChannels: string[];
|
|
94
96
|
newsDeviationThreshold: number;
|
|
95
97
|
broadcastChannels: string[];
|
|
96
98
|
positiveNews: NewsItem[];
|
package/lib/index.js
CHANGED
|
@@ -32,12 +32,46 @@ var import_koishi2 = require("koishi");
|
|
|
32
32
|
// src/render.ts
|
|
33
33
|
var import_koishi = require("koishi");
|
|
34
34
|
var import_path = require("path");
|
|
35
|
+
var import_url = require("url");
|
|
35
36
|
var import_fs = require("fs");
|
|
36
37
|
var templatesDir = (0, import_path.resolve)(__dirname, "templates");
|
|
38
|
+
var assetsDir = (0, import_path.resolve)(__dirname, "..", "assets");
|
|
39
|
+
var assetsBaseUrl = (0, import_url.pathToFileURL)(assetsDir).toString().replace(/\/$/, "");
|
|
40
|
+
function getAssetUrl(relativePath) {
|
|
41
|
+
return `${assetsBaseUrl}/${relativePath}`;
|
|
42
|
+
}
|
|
43
|
+
__name(getAssetUrl, "getAssetUrl");
|
|
44
|
+
function getFontFaceCss() {
|
|
45
|
+
const fontRegularUrl = getAssetUrl("fonts/RobotoMono-Regular.ttf");
|
|
46
|
+
const fontBoldUrl = getAssetUrl("fonts/RobotoMono-Bold.ttf");
|
|
47
|
+
return `
|
|
48
|
+
@font-face {
|
|
49
|
+
font-family: 'Roboto Mono';
|
|
50
|
+
src: url('${fontRegularUrl}') format('truetype');
|
|
51
|
+
font-weight: 400;
|
|
52
|
+
font-style: normal;
|
|
53
|
+
font-display: swap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@font-face {
|
|
57
|
+
font-family: 'Roboto Mono';
|
|
58
|
+
src: url('${fontBoldUrl}') format('truetype');
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
font-style: normal;
|
|
61
|
+
font-display: swap;
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
__name(getFontFaceCss, "getFontFaceCss");
|
|
66
|
+
function injectFontFace(template) {
|
|
67
|
+
return template.replace("{{FONT_FACE}}", getFontFaceCss());
|
|
68
|
+
}
|
|
69
|
+
__name(injectFontFace, "injectFontFace");
|
|
37
70
|
async function renderHoldingImage(ctx, logger2, username, holding, pending, currency) {
|
|
38
71
|
try {
|
|
39
72
|
const templatePath = (0, import_path.resolve)(templatesDir, "holding-card.html");
|
|
40
73
|
let template = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
74
|
+
template = injectFontFace(template);
|
|
41
75
|
const data = {
|
|
42
76
|
username,
|
|
43
77
|
holding,
|
|
@@ -66,6 +100,7 @@ async function renderTradeResultImage(ctx, logger2, tradeType, stockName, amount
|
|
|
66
100
|
try {
|
|
67
101
|
const templatePath = (0, import_path.resolve)(templatesDir, "trade-result.html");
|
|
68
102
|
let template = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
103
|
+
template = injectFontFace(template);
|
|
69
104
|
const tradeIndex = priceHistory.length - 1;
|
|
70
105
|
const status = tradeMeta?.status ?? "settled";
|
|
71
106
|
const pendingMinutes = tradeMeta?.pendingMinutes ?? 0;
|
|
@@ -127,6 +162,7 @@ async function renderStockImage(ctx, logger2, data, name2, viewLabel, current, h
|
|
|
127
162
|
}) : [];
|
|
128
163
|
const templatePath = (0, import_path.resolve)(templatesDir, "stock-chart.html");
|
|
129
164
|
let html = await import_fs.promises.readFile(templatePath, "utf-8");
|
|
165
|
+
html = injectFontFace(html);
|
|
130
166
|
const colorScheme = {
|
|
131
167
|
mainColor: isUp ? "#f23645" : "#089981",
|
|
132
168
|
gradientStart: isUp ? "rgba(242, 54, 69, 0.25)" : "rgba(8, 153, 129, 0.25)",
|
|
@@ -252,6 +288,33 @@ function formatKLineTime(date) {
|
|
|
252
288
|
}
|
|
253
289
|
__name(formatKLineTime, "formatKLineTime");
|
|
254
290
|
|
|
291
|
+
// src/utils/broadcast.ts
|
|
292
|
+
function parseChannelTarget(input) {
|
|
293
|
+
if (!input) return null;
|
|
294
|
+
const trimmed = input.trim();
|
|
295
|
+
if (!trimmed) return null;
|
|
296
|
+
const colonIndex = trimmed.indexOf(":");
|
|
297
|
+
if (colonIndex === -1) {
|
|
298
|
+
return { channelId: trimmed };
|
|
299
|
+
}
|
|
300
|
+
const platform = trimmed.slice(0, colonIndex).trim();
|
|
301
|
+
const channelId = trimmed.slice(colonIndex + 1).trim();
|
|
302
|
+
if (!platform || !channelId) return null;
|
|
303
|
+
return { platform, channelId };
|
|
304
|
+
}
|
|
305
|
+
__name(parseChannelTarget, "parseChannelTarget");
|
|
306
|
+
function chunkLines(header, lines, maxLines) {
|
|
307
|
+
if (!lines.length) return [];
|
|
308
|
+
const safeMax = Math.max(1, Math.floor(maxLines));
|
|
309
|
+
const chunks = [];
|
|
310
|
+
for (let i = 0; i < lines.length; i += safeMax) {
|
|
311
|
+
const block = lines.slice(i, i + safeMax);
|
|
312
|
+
chunks.push([header, ...block].join("\n"));
|
|
313
|
+
}
|
|
314
|
+
return chunks;
|
|
315
|
+
}
|
|
316
|
+
__name(chunkLines, "chunkLines");
|
|
317
|
+
|
|
255
318
|
// src/pattern.ts
|
|
256
319
|
var kLinePatterns = {
|
|
257
320
|
// ==================== 看涨模型 (8种) ====================
|
|
@@ -625,6 +688,66 @@ function registerAdminCommands(deps) {
|
|
|
625
688
|
}
|
|
626
689
|
__name(registerAdminCommands, "registerAdminCommands");
|
|
627
690
|
|
|
691
|
+
// src/utils/holding-summary.ts
|
|
692
|
+
function buildHoldingSummary(input) {
|
|
693
|
+
const holdingAmount = input.holding?.amount ?? 0;
|
|
694
|
+
const pendingBuys = input.pending.filter((p) => p.type === "buy");
|
|
695
|
+
const pendingSells = input.pending.filter((p) => p.type === "sell");
|
|
696
|
+
const pendingBuyAmount = pendingBuys.reduce((sum, p) => sum + p.amount, 0);
|
|
697
|
+
const pendingSellAmount = pendingSells.reduce((sum, p) => sum + p.amount, 0);
|
|
698
|
+
const totalAmount = holdingAmount + pendingBuyAmount + pendingSellAmount;
|
|
699
|
+
if (totalAmount <= 0) return null;
|
|
700
|
+
const holdingValue = holdingAmount * input.currentPrice;
|
|
701
|
+
const pendingBuyValue = pendingBuyAmount * input.currentPrice;
|
|
702
|
+
const pendingSellValue = pendingSells.reduce(
|
|
703
|
+
(sum, p) => sum + p.amount * p.price,
|
|
704
|
+
0
|
|
705
|
+
);
|
|
706
|
+
const marketValue = holdingValue + pendingBuyValue + pendingSellValue;
|
|
707
|
+
let hasCostData = true;
|
|
708
|
+
let totalCost = 0;
|
|
709
|
+
if (holdingAmount > 0) {
|
|
710
|
+
const holdingCost = input.holding?.totalCost ?? null;
|
|
711
|
+
if (!holdingCost || holdingCost <= 0) {
|
|
712
|
+
hasCostData = false;
|
|
713
|
+
} else {
|
|
714
|
+
totalCost += holdingCost;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
for (const pending of pendingBuys) {
|
|
718
|
+
if (!pending.cost || pending.cost <= 0) hasCostData = false;
|
|
719
|
+
else totalCost += pending.cost;
|
|
720
|
+
}
|
|
721
|
+
for (const pending of pendingSells) {
|
|
722
|
+
if (!pending.buyCost || pending.buyCost <= 0) hasCostData = false;
|
|
723
|
+
else totalCost += pending.buyCost;
|
|
724
|
+
}
|
|
725
|
+
if (!hasCostData) {
|
|
726
|
+
return {
|
|
727
|
+
amount: totalAmount,
|
|
728
|
+
marketValue,
|
|
729
|
+
totalCost: null,
|
|
730
|
+
avgCost: null,
|
|
731
|
+
profit: null,
|
|
732
|
+
profitPercent: null,
|
|
733
|
+
hasCostData: false
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const avgCost = totalCost / totalAmount;
|
|
737
|
+
const profit = marketValue - totalCost;
|
|
738
|
+
const profitPercent = totalCost > 0 ? profit / totalCost * 100 : 0;
|
|
739
|
+
return {
|
|
740
|
+
amount: totalAmount,
|
|
741
|
+
marketValue,
|
|
742
|
+
totalCost,
|
|
743
|
+
avgCost,
|
|
744
|
+
profit,
|
|
745
|
+
profitPercent,
|
|
746
|
+
hasCostData: true
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
__name(buildHoldingSummary, "buildHoldingSummary");
|
|
750
|
+
|
|
628
751
|
// src/commands-stock.ts
|
|
629
752
|
function resolveSellFeePercent(amount, tiers) {
|
|
630
753
|
if (!Array.isArray(tiers) || tiers.length === 0) return 0;
|
|
@@ -999,6 +1122,7 @@ function registerStockCommands(deps) {
|
|
|
999
1122
|
amount,
|
|
1000
1123
|
price: currentPrice,
|
|
1001
1124
|
cost: netGain,
|
|
1125
|
+
buyCost: soldCost,
|
|
1002
1126
|
startTime,
|
|
1003
1127
|
endTime
|
|
1004
1128
|
});
|
|
@@ -1074,23 +1198,27 @@ function registerStockCommands(deps) {
|
|
|
1074
1198
|
const pending = await ctx.database.get("bourse_pending", { userId });
|
|
1075
1199
|
let holdingData = null;
|
|
1076
1200
|
const currentPrice = getCurrentPrice();
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1201
|
+
const summary = buildHoldingSummary({
|
|
1202
|
+
currentPrice,
|
|
1203
|
+
holding: holdings.length ? { amount: holdings[0].amount, totalCost: holdings[0].totalCost } : null,
|
|
1204
|
+
pending: pending.map((p) => ({
|
|
1205
|
+
type: p.type,
|
|
1206
|
+
amount: p.amount,
|
|
1207
|
+
price: p.price,
|
|
1208
|
+
cost: p.cost,
|
|
1209
|
+
buyCost: p.buyCost ?? null
|
|
1210
|
+
}))
|
|
1211
|
+
});
|
|
1212
|
+
if (summary) {
|
|
1085
1213
|
holdingData = {
|
|
1086
1214
|
stockName: config.stockName,
|
|
1087
|
-
amount:
|
|
1215
|
+
amount: summary.amount,
|
|
1088
1216
|
currentPrice: fmtPrice(currentPrice),
|
|
1089
|
-
avgCost: hasCostData ? avgCost : null,
|
|
1090
|
-
totalCost: hasCostData ? totalCost : null,
|
|
1091
|
-
marketValue,
|
|
1092
|
-
profit,
|
|
1093
|
-
profitPercent
|
|
1217
|
+
avgCost: summary.hasCostData ? fmtPrice(summary.avgCost) : null,
|
|
1218
|
+
totalCost: summary.hasCostData ? fmtAmount(summary.totalCost) : null,
|
|
1219
|
+
marketValue: fmtAmount(summary.marketValue),
|
|
1220
|
+
profit: summary.hasCostData ? fmtAmount(summary.profit) : null,
|
|
1221
|
+
profitPercent: summary.hasCostData ? Number(summary.profitPercent.toFixed(2)) : null
|
|
1094
1222
|
};
|
|
1095
1223
|
}
|
|
1096
1224
|
const pendingData = pending.map((p) => {
|
|
@@ -1294,6 +1422,9 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
1294
1422
|
dividendIntervalDays: import_koishi2.Schema.number().min(1).step(1).default(7).description("分红结算周期(天)"),
|
|
1295
1423
|
maxDividendRate: import_koishi2.Schema.number().min(0).max(1).step(0.01).default(0.15).description("最大分红期望利润率(0-1,超出部分用于除息而非派发)")
|
|
1296
1424
|
}).description("分红机制"),
|
|
1425
|
+
import_koishi2.Schema.object({
|
|
1426
|
+
dividendBroadcastChannels: import_koishi2.Schema.array(import_koishi2.Schema.string()).default([]).description("分红播报的频道/群聊 ID 列表(支持 onebot:123 或 123)")
|
|
1427
|
+
}).description("分红播报"),
|
|
1297
1428
|
import_koishi2.Schema.object({
|
|
1298
1429
|
newsDeviationThreshold: import_koishi2.Schema.number().min(0.05).max(1).step(0.01).default(0.15).description(
|
|
1299
1430
|
"触发新闻的偏离度阈值(例如 0.15 表示当新目标价与当前价相差 15% 以上时触发)"
|
|
@@ -1361,6 +1492,7 @@ function apply(ctx, config) {
|
|
|
1361
1492
|
amount: "integer",
|
|
1362
1493
|
price: "double",
|
|
1363
1494
|
cost: "double",
|
|
1495
|
+
buyCost: "double",
|
|
1364
1496
|
startTime: "timestamp",
|
|
1365
1497
|
endTime: "timestamp"
|
|
1366
1498
|
},
|
|
@@ -1713,6 +1845,27 @@ function apply(ctx, config) {
|
|
|
1713
1845
|
return { success: true };
|
|
1714
1846
|
}
|
|
1715
1847
|
__name(pay, "pay");
|
|
1848
|
+
async function sendBroadcast(channels, message, label) {
|
|
1849
|
+
if (!channels || channels.length === 0) return;
|
|
1850
|
+
for (const raw of channels) {
|
|
1851
|
+
const target = parseChannelTarget(raw);
|
|
1852
|
+
if (!target) {
|
|
1853
|
+
logger.warn(`${label}: 无效频道配置 ${raw}`);
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
const bot = target.platform ? ctx.bots.find((b) => b.platform === target.platform) : ctx.bots[0];
|
|
1857
|
+
if (!bot) {
|
|
1858
|
+
logger.warn(`${label}: 未找到可用 bot (${raw})`);
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
await bot.sendMessage(target.channelId, message);
|
|
1863
|
+
} catch (err) {
|
|
1864
|
+
logger.warn(`${label}: 发送失败 channel=${raw}`, err);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
__name(sendBroadcast, "sendBroadcast");
|
|
1716
1869
|
async function broadcastMacroNews(targetPrice, basePrice) {
|
|
1717
1870
|
const deviation = (targetPrice - basePrice) / basePrice;
|
|
1718
1871
|
if (Math.abs(deviation) < config.newsDeviationThreshold) return;
|
|
@@ -1733,13 +1886,7 @@ function apply(ctx, config) {
|
|
|
1733
1886
|
logger.info(
|
|
1734
1887
|
`触发宏观新闻播报: ${newsText} (偏离度=${(deviation * 100).toFixed(2)}%)`
|
|
1735
1888
|
);
|
|
1736
|
-
|
|
1737
|
-
try {
|
|
1738
|
-
await ctx.broadcast(config.broadcastChannels, newsText);
|
|
1739
|
-
} catch (err) {
|
|
1740
|
-
logger.warn(`新闻播报广播失败:`, err);
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1889
|
+
await sendBroadcast(config.broadcastChannels, newsText, "新闻播报");
|
|
1743
1890
|
}
|
|
1744
1891
|
__name(broadcastMacroNews, "broadcastMacroNews");
|
|
1745
1892
|
const patternsByCategory = {
|
|
@@ -2094,6 +2241,7 @@ function apply(ctx, config) {
|
|
|
2094
2241
|
let totalDividendPaid = 0;
|
|
2095
2242
|
let successCount = 0;
|
|
2096
2243
|
let failCount = 0;
|
|
2244
|
+
const successRecords = [];
|
|
2097
2245
|
for (const holding of allHoldings) {
|
|
2098
2246
|
const dividendAmount = fmtAmount(
|
|
2099
2247
|
holding.amount * effectiveDividendRate * currentPrice
|
|
@@ -2114,6 +2262,7 @@ function apply(ctx, config) {
|
|
|
2114
2262
|
if (success) {
|
|
2115
2263
|
totalDividendPaid += dividendAmount;
|
|
2116
2264
|
successCount++;
|
|
2265
|
+
successRecords.push({ userId: holding.userId, amount: dividendAmount });
|
|
2117
2266
|
} else {
|
|
2118
2267
|
logger.warn(
|
|
2119
2268
|
`分红引擎: 用户 ${holding.userId} (uid=${holding.uid}) 分红派发失败`
|
|
@@ -2121,6 +2270,25 @@ function apply(ctx, config) {
|
|
|
2121
2270
|
failCount++;
|
|
2122
2271
|
}
|
|
2123
2272
|
}
|
|
2273
|
+
if (config.dividendBroadcastChannels && config.dividendBroadcastChannels.length > 0 && successCount > 0) {
|
|
2274
|
+
const summary = `【分红播报】参与人数: ${successCount} | 派息率: ${(effectiveDividendRate * 100).toFixed(2)}% | 总派息: ${totalDividendPaid.toFixed(2)} ${config.currency}`;
|
|
2275
|
+
await sendBroadcast(
|
|
2276
|
+
config.dividendBroadcastChannels,
|
|
2277
|
+
summary,
|
|
2278
|
+
"分红播报"
|
|
2279
|
+
);
|
|
2280
|
+
const detailLines = successRecords.map((record) => {
|
|
2281
|
+
const ratio = totalDividendPaid > 0 ? record.amount / totalDividendPaid * 100 : 0;
|
|
2282
|
+
return `- ${record.userId}:占比 ${ratio.toFixed(2)}% | ${record.amount.toFixed(2)} ${config.currency}`;
|
|
2283
|
+
});
|
|
2284
|
+
for (const chunk of chunkLines("【分红明细】", detailLines, 10)) {
|
|
2285
|
+
await sendBroadcast(
|
|
2286
|
+
config.dividendBroadcastChannels,
|
|
2287
|
+
chunk,
|
|
2288
|
+
"分红明细"
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2124
2292
|
const oldPrice = currentPrice;
|
|
2125
2293
|
currentPrice = fmtPrice(currentPrice * (1 - priceDropRate));
|
|
2126
2294
|
if (currentPrice < 1) currentPrice = 1;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type HoldingRecord = {
|
|
2
|
+
amount: number;
|
|
3
|
+
totalCost?: number | null;
|
|
4
|
+
};
|
|
5
|
+
export type PendingRecord = {
|
|
6
|
+
type: "buy" | "sell";
|
|
7
|
+
amount: number;
|
|
8
|
+
price: number;
|
|
9
|
+
cost: number;
|
|
10
|
+
buyCost?: number | null;
|
|
11
|
+
};
|
|
12
|
+
export type HoldingSummary = {
|
|
13
|
+
amount: number;
|
|
14
|
+
marketValue: number;
|
|
15
|
+
totalCost: number | null;
|
|
16
|
+
avgCost: number | null;
|
|
17
|
+
profit: number | null;
|
|
18
|
+
profitPercent: number | null;
|
|
19
|
+
hasCostData: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function buildHoldingSummary(input: {
|
|
22
|
+
currentPrice: number;
|
|
23
|
+
holding: HoldingRecord | null;
|
|
24
|
+
pending: PendingRecord[];
|
|
25
|
+
}): HoldingSummary | null;
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-monetary-bourse",
|
|
3
|
-
"version": "3.0.0
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"typings": "lib/index.d.ts",
|
|
6
6
|
"files": [
|
|
7
|
+
"assets",
|
|
7
8
|
"lib",
|
|
8
9
|
"dist"
|
|
9
10
|
],
|
|
10
11
|
"scripts": {
|
|
11
|
-
"build": "tsc && node -e \"require('fs').cpSync('src/templates', 'lib/templates', {recursive: true})\""
|
|
12
|
+
"build": "tsc && node -e \"require('fs').cpSync('src/templates', 'lib/templates', {recursive: true})\"",
|
|
13
|
+
"test": "node --test --import tsx"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"tsx": "^4.7.1"
|
|
12
17
|
},
|
|
13
18
|
"koishi": {
|
|
14
19
|
"description": {
|
package/readme.md
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">koishi-plugin-monetary-bourse</h1>
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
为 Koishi 提供基于 <code>monetary</code> 通用货币系统的股票交易所功能。
|
|
5
|
+
</p>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/koishi-plugin-monetary-bourse"><img src="https://img.shields.io/npm/v/koishi-plugin-monetary-bourse?style=for-the-badge&logo=npm" alt="npm" /></a>
|
|
9
|
+
<a href="https://github.com/BYWled/koishi-plugin-monetary-bourse"><img src="https://img.shields.io/github/stars/BYWled/koishi-plugin-monetary-bourse?style=for-the-badge&logo=github" alt="GitHub Stars" /></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://gitee.com/BYWled/koishi-plugin-monetary-bourse"><img src="https://img.shields.io/badge/Gitee-代码镜像-C71D23?style=for-the-badge&logo=gitee&logoColor=white" alt="Gitee Mirror" /></a>
|
|
14
|
+
<a href="https://gitcode.com/BYWled/koishi-plugin-monetary-bourse"><img src="https://img.shields.io/badge/GitCode-代码镜像-2962FF?style=for-the-badge" alt="GitCode Mirror" /></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://github.com/BYWled/koishi-plugin-monetary-bourse">GitHub</a> ·
|
|
19
|
+
<a href="https://gitee.com/BYWled/koishi-plugin-monetary-bourse">Gitee</a> ·
|
|
20
|
+
<a href="https://gitcode.com/BYWled/koishi-plugin-monetary-bourse">GitCode</a>
|
|
21
|
+
</p>
|
|
6
22
|
|
|
7
23
|
本插件模拟了一个具备自动宏观调控、25种经典K线形态、智能概率博弈和可视化交割单的深度拟真股票市场。用户可以使用机器人通用的货币(如信用点)进行股票买卖、炒股理财。
|
|
8
24
|
|
|
9
|
-
> 版本:**3.0.0
|
|
25
|
+
> 版本:**3.0.0**
|
|
10
26
|
|
|
11
27
|
## ✨ 特性
|
|
12
28
|
|
|
@@ -17,11 +33,15 @@
|
|
|
17
33
|
- **🖼️ 全可视化交互**:
|
|
18
34
|
- **专业走势图**:复刻 TradingView 风格的深色玻璃拟态 K 线图,包含动态呼吸灯、渐变填充与详细指标。
|
|
19
35
|
- **持仓资产卡片**:精美渲染个人持仓、成本分析、浮动盈亏比及排队中的挂单详情。
|
|
20
|
-
-
|
|
36
|
+
- **交易交割单**:买卖成交瞬间生成**交易回单图片**,在 K 线图上精确标记买卖点位,直观展示单笔盈亏与买入成本线。
|
|
37
|
+
- **内置字体**:使用 Roboto Mono 字体,确保图表中的数字和文本清晰易读。
|
|
21
38
|
- **❄️ 资金冻结与挂单排队**:
|
|
22
39
|
- 交易采用 T+0 机制,但大额资金/股票会根据金额计算**动态冻结时间**。
|
|
23
40
|
- 挂单采用**串行排队模式**,同一用户的多个挂单需依次读秒,防止通过拆单绕过冻结机制,增加博弈深度。
|
|
24
41
|
- **💸 手续费与精度控制**:支持卖出手续费配置,回单展示净到账金额;可开启整数精度模式,适配不支持小数的货币体系。
|
|
42
|
+
- **📊 分红与新闻系统**:
|
|
43
|
+
- 定期分红:根据持仓占比和预设利润率进行分红,支持分红播报。
|
|
44
|
+
- 新闻触发:当价格偏离宏观目标超过设定阈值时,自动从利好/利空新闻库中随机选取并播报,增加市场氛围。
|
|
25
45
|
- **🧭 宏观周期固定刷新**:支持固定时刻刷新宏观目标周期,便于在活动时段塑造更清晰的趋势节奏。
|
|
26
46
|
- **🏦 银行联动**:支持与[ `koishi-plugin-monetary-bank`](https://github.com/BYWled/koishi-plugin-monetary-bank)[](https://www.npmjs.com/package/koishi-plugin-monetary-bank) 联动,现金不足时自动扣除银行活期存款。
|
|
27
47
|
|
|
@@ -156,6 +176,9 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
|
|
|
156
176
|
- **dividendIntervalDays**: 分红结算周期(天,默认 `7`)。
|
|
157
177
|
- **maxDividendRate**: 最大分红期望利润率(0-1,默认 `0.15`,超出部分用于除息而非派发)。
|
|
158
178
|
|
|
179
|
+
### 分红播报
|
|
180
|
+
- **dividendBroadcastChannels**: 分红播报的频道/群聊 ID 列表(支持 `onebot:123` 或 `123`)。
|
|
181
|
+
|
|
159
182
|
### 新闻播报机制
|
|
160
183
|
- **newsDeviationThreshold**: 触发新闻的偏离度阈值(默认 `0.15`,例如 `0.15` 表示偏离 15% 以上触发)。
|
|
161
184
|
- **broadcastChannels**: 播报新闻的频道/群聊 ID 列表(为空则仅后台日志,不发送群消息)。
|
|
@@ -194,6 +217,9 @@ negativeNews:
|
|
|
194
217
|
|
|
195
218
|
详细的更新日志请查看 [CHANGELOG.md](./CHANGELOG.md)。
|
|
196
219
|
|
|
220
|
+
## 🙏 第三方资源/致谢
|
|
221
|
+
- Roboto Mono 字体(SIL OPEN FONT LICENSE Version 1.1):[字体链接](https://fonts.google.com/specimen/Roboto+Mono)
|
|
222
|
+
|
|
197
223
|
---
|
|
198
224
|
|
|
199
225
|
**开发者**: BYWled
|