koishi-plugin-monetary-bourse 3.0.0-alpha.19 → 3.0.0-alpha.20
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 +2 -0
- package/lib/index.js +153 -21
- package/lib/utils/broadcast.d.ts +6 -0
- package/lib/utils/holding-summary.d.ts +25 -0
- package/package.json +6 -2
- package/readme.md +4 -1
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
|
@@ -252,6 +252,33 @@ function formatKLineTime(date) {
|
|
|
252
252
|
}
|
|
253
253
|
__name(formatKLineTime, "formatKLineTime");
|
|
254
254
|
|
|
255
|
+
// src/utils/broadcast.ts
|
|
256
|
+
function parseChannelTarget(input) {
|
|
257
|
+
if (!input) return null;
|
|
258
|
+
const trimmed = input.trim();
|
|
259
|
+
if (!trimmed) return null;
|
|
260
|
+
const colonIndex = trimmed.indexOf(":");
|
|
261
|
+
if (colonIndex === -1) {
|
|
262
|
+
return { channelId: trimmed };
|
|
263
|
+
}
|
|
264
|
+
const platform = trimmed.slice(0, colonIndex).trim();
|
|
265
|
+
const channelId = trimmed.slice(colonIndex + 1).trim();
|
|
266
|
+
if (!platform || !channelId) return null;
|
|
267
|
+
return { platform, channelId };
|
|
268
|
+
}
|
|
269
|
+
__name(parseChannelTarget, "parseChannelTarget");
|
|
270
|
+
function chunkLines(header, lines, maxLines) {
|
|
271
|
+
if (!lines.length) return [];
|
|
272
|
+
const safeMax = Math.max(1, Math.floor(maxLines));
|
|
273
|
+
const chunks = [];
|
|
274
|
+
for (let i = 0; i < lines.length; i += safeMax) {
|
|
275
|
+
const block = lines.slice(i, i + safeMax);
|
|
276
|
+
chunks.push([header, ...block].join("\n"));
|
|
277
|
+
}
|
|
278
|
+
return chunks;
|
|
279
|
+
}
|
|
280
|
+
__name(chunkLines, "chunkLines");
|
|
281
|
+
|
|
255
282
|
// src/pattern.ts
|
|
256
283
|
var kLinePatterns = {
|
|
257
284
|
// ==================== 看涨模型 (8种) ====================
|
|
@@ -625,6 +652,66 @@ function registerAdminCommands(deps) {
|
|
|
625
652
|
}
|
|
626
653
|
__name(registerAdminCommands, "registerAdminCommands");
|
|
627
654
|
|
|
655
|
+
// src/utils/holding-summary.ts
|
|
656
|
+
function buildHoldingSummary(input) {
|
|
657
|
+
const holdingAmount = input.holding?.amount ?? 0;
|
|
658
|
+
const pendingBuys = input.pending.filter((p) => p.type === "buy");
|
|
659
|
+
const pendingSells = input.pending.filter((p) => p.type === "sell");
|
|
660
|
+
const pendingBuyAmount = pendingBuys.reduce((sum, p) => sum + p.amount, 0);
|
|
661
|
+
const pendingSellAmount = pendingSells.reduce((sum, p) => sum + p.amount, 0);
|
|
662
|
+
const totalAmount = holdingAmount + pendingBuyAmount + pendingSellAmount;
|
|
663
|
+
if (totalAmount <= 0) return null;
|
|
664
|
+
const holdingValue = holdingAmount * input.currentPrice;
|
|
665
|
+
const pendingBuyValue = pendingBuyAmount * input.currentPrice;
|
|
666
|
+
const pendingSellValue = pendingSells.reduce(
|
|
667
|
+
(sum, p) => sum + p.amount * p.price,
|
|
668
|
+
0
|
|
669
|
+
);
|
|
670
|
+
const marketValue = holdingValue + pendingBuyValue + pendingSellValue;
|
|
671
|
+
let hasCostData = true;
|
|
672
|
+
let totalCost = 0;
|
|
673
|
+
if (holdingAmount > 0) {
|
|
674
|
+
const holdingCost = input.holding?.totalCost ?? null;
|
|
675
|
+
if (!holdingCost || holdingCost <= 0) {
|
|
676
|
+
hasCostData = false;
|
|
677
|
+
} else {
|
|
678
|
+
totalCost += holdingCost;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
for (const pending of pendingBuys) {
|
|
682
|
+
if (!pending.cost || pending.cost <= 0) hasCostData = false;
|
|
683
|
+
else totalCost += pending.cost;
|
|
684
|
+
}
|
|
685
|
+
for (const pending of pendingSells) {
|
|
686
|
+
if (!pending.buyCost || pending.buyCost <= 0) hasCostData = false;
|
|
687
|
+
else totalCost += pending.buyCost;
|
|
688
|
+
}
|
|
689
|
+
if (!hasCostData) {
|
|
690
|
+
return {
|
|
691
|
+
amount: totalAmount,
|
|
692
|
+
marketValue,
|
|
693
|
+
totalCost: null,
|
|
694
|
+
avgCost: null,
|
|
695
|
+
profit: null,
|
|
696
|
+
profitPercent: null,
|
|
697
|
+
hasCostData: false
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const avgCost = totalCost / totalAmount;
|
|
701
|
+
const profit = marketValue - totalCost;
|
|
702
|
+
const profitPercent = totalCost > 0 ? profit / totalCost * 100 : 0;
|
|
703
|
+
return {
|
|
704
|
+
amount: totalAmount,
|
|
705
|
+
marketValue,
|
|
706
|
+
totalCost,
|
|
707
|
+
avgCost,
|
|
708
|
+
profit,
|
|
709
|
+
profitPercent,
|
|
710
|
+
hasCostData: true
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
__name(buildHoldingSummary, "buildHoldingSummary");
|
|
714
|
+
|
|
628
715
|
// src/commands-stock.ts
|
|
629
716
|
function resolveSellFeePercent(amount, tiers) {
|
|
630
717
|
if (!Array.isArray(tiers) || tiers.length === 0) return 0;
|
|
@@ -999,6 +1086,7 @@ function registerStockCommands(deps) {
|
|
|
999
1086
|
amount,
|
|
1000
1087
|
price: currentPrice,
|
|
1001
1088
|
cost: netGain,
|
|
1089
|
+
buyCost: soldCost,
|
|
1002
1090
|
startTime,
|
|
1003
1091
|
endTime
|
|
1004
1092
|
});
|
|
@@ -1074,23 +1162,27 @@ function registerStockCommands(deps) {
|
|
|
1074
1162
|
const pending = await ctx.database.get("bourse_pending", { userId });
|
|
1075
1163
|
let holdingData = null;
|
|
1076
1164
|
const currentPrice = getCurrentPrice();
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1165
|
+
const summary = buildHoldingSummary({
|
|
1166
|
+
currentPrice,
|
|
1167
|
+
holding: holdings.length ? { amount: holdings[0].amount, totalCost: holdings[0].totalCost } : null,
|
|
1168
|
+
pending: pending.map((p) => ({
|
|
1169
|
+
type: p.type,
|
|
1170
|
+
amount: p.amount,
|
|
1171
|
+
price: p.price,
|
|
1172
|
+
cost: p.cost,
|
|
1173
|
+
buyCost: p.buyCost ?? null
|
|
1174
|
+
}))
|
|
1175
|
+
});
|
|
1176
|
+
if (summary) {
|
|
1085
1177
|
holdingData = {
|
|
1086
1178
|
stockName: config.stockName,
|
|
1087
|
-
amount:
|
|
1179
|
+
amount: summary.amount,
|
|
1088
1180
|
currentPrice: fmtPrice(currentPrice),
|
|
1089
|
-
avgCost: hasCostData ? avgCost : null,
|
|
1090
|
-
totalCost: hasCostData ? totalCost : null,
|
|
1091
|
-
marketValue,
|
|
1092
|
-
profit,
|
|
1093
|
-
profitPercent
|
|
1181
|
+
avgCost: summary.hasCostData ? fmtPrice(summary.avgCost) : null,
|
|
1182
|
+
totalCost: summary.hasCostData ? fmtAmount(summary.totalCost) : null,
|
|
1183
|
+
marketValue: fmtAmount(summary.marketValue),
|
|
1184
|
+
profit: summary.hasCostData ? fmtAmount(summary.profit) : null,
|
|
1185
|
+
profitPercent: summary.hasCostData ? Number(summary.profitPercent.toFixed(2)) : null
|
|
1094
1186
|
};
|
|
1095
1187
|
}
|
|
1096
1188
|
const pendingData = pending.map((p) => {
|
|
@@ -1294,6 +1386,9 @@ var Config = import_koishi2.Schema.intersect([
|
|
|
1294
1386
|
dividendIntervalDays: import_koishi2.Schema.number().min(1).step(1).default(7).description("分红结算周期(天)"),
|
|
1295
1387
|
maxDividendRate: import_koishi2.Schema.number().min(0).max(1).step(0.01).default(0.15).description("最大分红期望利润率(0-1,超出部分用于除息而非派发)")
|
|
1296
1388
|
}).description("分红机制"),
|
|
1389
|
+
import_koishi2.Schema.object({
|
|
1390
|
+
dividendBroadcastChannels: import_koishi2.Schema.array(import_koishi2.Schema.string()).default([]).description("分红播报的频道/群聊 ID 列表(支持 onebot:123 或 123)")
|
|
1391
|
+
}).description("分红播报"),
|
|
1297
1392
|
import_koishi2.Schema.object({
|
|
1298
1393
|
newsDeviationThreshold: import_koishi2.Schema.number().min(0.05).max(1).step(0.01).default(0.15).description(
|
|
1299
1394
|
"触发新闻的偏离度阈值(例如 0.15 表示当新目标价与当前价相差 15% 以上时触发)"
|
|
@@ -1361,6 +1456,7 @@ function apply(ctx, config) {
|
|
|
1361
1456
|
amount: "integer",
|
|
1362
1457
|
price: "double",
|
|
1363
1458
|
cost: "double",
|
|
1459
|
+
buyCost: "double",
|
|
1364
1460
|
startTime: "timestamp",
|
|
1365
1461
|
endTime: "timestamp"
|
|
1366
1462
|
},
|
|
@@ -1713,6 +1809,27 @@ function apply(ctx, config) {
|
|
|
1713
1809
|
return { success: true };
|
|
1714
1810
|
}
|
|
1715
1811
|
__name(pay, "pay");
|
|
1812
|
+
async function sendBroadcast(channels, message, label) {
|
|
1813
|
+
if (!channels || channels.length === 0) return;
|
|
1814
|
+
for (const raw of channels) {
|
|
1815
|
+
const target = parseChannelTarget(raw);
|
|
1816
|
+
if (!target) {
|
|
1817
|
+
logger.warn(`${label}: 无效频道配置 ${raw}`);
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
const bot = target.platform ? ctx.bots.find((b) => b.platform === target.platform) : ctx.bots[0];
|
|
1821
|
+
if (!bot) {
|
|
1822
|
+
logger.warn(`${label}: 未找到可用 bot (${raw})`);
|
|
1823
|
+
continue;
|
|
1824
|
+
}
|
|
1825
|
+
try {
|
|
1826
|
+
await bot.sendMessage(target.channelId, message);
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
logger.warn(`${label}: 发送失败 channel=${raw}`, err);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
__name(sendBroadcast, "sendBroadcast");
|
|
1716
1833
|
async function broadcastMacroNews(targetPrice, basePrice) {
|
|
1717
1834
|
const deviation = (targetPrice - basePrice) / basePrice;
|
|
1718
1835
|
if (Math.abs(deviation) < config.newsDeviationThreshold) return;
|
|
@@ -1733,13 +1850,7 @@ function apply(ctx, config) {
|
|
|
1733
1850
|
logger.info(
|
|
1734
1851
|
`触发宏观新闻播报: ${newsText} (偏离度=${(deviation * 100).toFixed(2)}%)`
|
|
1735
1852
|
);
|
|
1736
|
-
|
|
1737
|
-
try {
|
|
1738
|
-
await ctx.broadcast(config.broadcastChannels, newsText);
|
|
1739
|
-
} catch (err) {
|
|
1740
|
-
logger.warn(`新闻播报广播失败:`, err);
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1853
|
+
await sendBroadcast(config.broadcastChannels, newsText, "新闻播报");
|
|
1743
1854
|
}
|
|
1744
1855
|
__name(broadcastMacroNews, "broadcastMacroNews");
|
|
1745
1856
|
const patternsByCategory = {
|
|
@@ -2094,6 +2205,7 @@ function apply(ctx, config) {
|
|
|
2094
2205
|
let totalDividendPaid = 0;
|
|
2095
2206
|
let successCount = 0;
|
|
2096
2207
|
let failCount = 0;
|
|
2208
|
+
const successRecords = [];
|
|
2097
2209
|
for (const holding of allHoldings) {
|
|
2098
2210
|
const dividendAmount = fmtAmount(
|
|
2099
2211
|
holding.amount * effectiveDividendRate * currentPrice
|
|
@@ -2114,6 +2226,7 @@ function apply(ctx, config) {
|
|
|
2114
2226
|
if (success) {
|
|
2115
2227
|
totalDividendPaid += dividendAmount;
|
|
2116
2228
|
successCount++;
|
|
2229
|
+
successRecords.push({ userId: holding.userId, amount: dividendAmount });
|
|
2117
2230
|
} else {
|
|
2118
2231
|
logger.warn(
|
|
2119
2232
|
`分红引擎: 用户 ${holding.userId} (uid=${holding.uid}) 分红派发失败`
|
|
@@ -2121,6 +2234,25 @@ function apply(ctx, config) {
|
|
|
2121
2234
|
failCount++;
|
|
2122
2235
|
}
|
|
2123
2236
|
}
|
|
2237
|
+
if (config.dividendBroadcastChannels && config.dividendBroadcastChannels.length > 0 && successCount > 0) {
|
|
2238
|
+
const summary = `【分红播报】参与人数: ${successCount} | 派息率: ${(effectiveDividendRate * 100).toFixed(2)}% | 总派息: ${totalDividendPaid.toFixed(2)} ${config.currency}`;
|
|
2239
|
+
await sendBroadcast(
|
|
2240
|
+
config.dividendBroadcastChannels,
|
|
2241
|
+
summary,
|
|
2242
|
+
"分红播报"
|
|
2243
|
+
);
|
|
2244
|
+
const detailLines = successRecords.map((record) => {
|
|
2245
|
+
const ratio = totalDividendPaid > 0 ? record.amount / totalDividendPaid * 100 : 0;
|
|
2246
|
+
return `- ${record.userId}:占比 ${ratio.toFixed(2)}% | ${record.amount.toFixed(2)} ${config.currency}`;
|
|
2247
|
+
});
|
|
2248
|
+
for (const chunk of chunkLines("【分红明细】", detailLines, 10)) {
|
|
2249
|
+
await sendBroadcast(
|
|
2250
|
+
config.dividendBroadcastChannels,
|
|
2251
|
+
chunk,
|
|
2252
|
+
"分红明细"
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2124
2256
|
const oldPrice = currentPrice;
|
|
2125
2257
|
currentPrice = fmtPrice(currentPrice * (1 - priceDropRate));
|
|
2126
2258
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-monetary-bourse",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.20",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"typings": "lib/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -8,7 +8,11 @@
|
|
|
8
8
|
"dist"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
|
-
"build": "tsc && node -e \"require('fs').cpSync('src/templates', 'lib/templates', {recursive: true})\""
|
|
11
|
+
"build": "tsc && node -e \"require('fs').cpSync('src/templates', 'lib/templates', {recursive: true})\"",
|
|
12
|
+
"test": "node --test --import tsx"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"tsx": "^4.7.1"
|
|
12
16
|
},
|
|
13
17
|
"koishi": {
|
|
14
18
|
"description": {
|
package/readme.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
本插件模拟了一个具备自动宏观调控、25种经典K线形态、智能概率博弈和可视化交割单的深度拟真股票市场。用户可以使用机器人通用的货币(如信用点)进行股票买卖、炒股理财。
|
|
8
8
|
|
|
9
|
-
> 版本:**3.0.0-alpha.
|
|
9
|
+
> 版本:**3.0.0-alpha.20**
|
|
10
10
|
|
|
11
11
|
## ✨ 特性
|
|
12
12
|
|
|
@@ -156,6 +156,9 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
|
|
|
156
156
|
- **dividendIntervalDays**: 分红结算周期(天,默认 `7`)。
|
|
157
157
|
- **maxDividendRate**: 最大分红期望利润率(0-1,默认 `0.15`,超出部分用于除息而非派发)。
|
|
158
158
|
|
|
159
|
+
### 分红播报
|
|
160
|
+
- **dividendBroadcastChannels**: 分红播报的频道/群聊 ID 列表(支持 `onebot:123` 或 `123`)。
|
|
161
|
+
|
|
159
162
|
### 新闻播报机制
|
|
160
163
|
- **newsDeviationThreshold**: 触发新闻的偏离度阈值(默认 `0.15`,例如 `0.15` 表示偏离 15% 以上触发)。
|
|
161
164
|
- **broadcastChannels**: 播报新闻的频道/群聊 ID 列表(为空则仅后台日志,不发送群消息)。
|