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.
@@ -0,0 +1,8 @@
1
+ # Fonts
2
+
3
+ Place Roboto Mono font files here:
4
+
5
+ - RobotoMono-Regular.ttf
6
+ - RobotoMono-Bold.ttf
7
+
8
+ Source: https://fonts.google.com/specimen/Roboto+Mono (SIL OPEN FONT LICENSE Version 1.1)
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
- if (holdings.length > 0) {
1078
- const h2 = holdings[0];
1079
- const marketValue = fmtAmount(h2.amount * currentPrice);
1080
- const hasCostData = h2.totalCost !== void 0 && h2.totalCost !== null && h2.totalCost > 0;
1081
- const totalCost = hasCostData ? fmtAmount(h2.totalCost) : 0;
1082
- const avgCost = hasCostData && h2.amount > 0 ? fmtPrice(totalCost / h2.amount) : 0;
1083
- const profit = hasCostData ? fmtAmount(marketValue - totalCost) : null;
1084
- const profitPercent = hasCostData && totalCost > 0 ? Number((profit / totalCost * 100).toFixed(2)) : null;
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: h2.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
- if (config.broadcastChannels && config.broadcastChannels.length > 0) {
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;
@@ -2,6 +2,8 @@
2
2
 
3
3
  <head>
4
4
  <style>
5
+ {{FONT_FACE}}
6
+
5
7
  :root {
6
8
  /* 统一配色 (From trade-result) */
7
9
  --bg-color: #0c0f15;
@@ -544,4 +546,4 @@
544
546
  </script>
545
547
  </body>
546
548
 
547
- </html>
549
+ </html>
@@ -1,6 +1,8 @@
1
1
  <html>
2
2
  <head>
3
3
  <style>
4
+ {{FONT_FACE}}
5
+
4
6
  :root {
5
7
  --main-color: {{MAIN_COLOR}};
6
8
  --glow-color: {{GLOW_COLOR}};
@@ -4,6 +4,8 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <style>
7
+ {{FONT_FACE}}
8
+
7
9
  :root {
8
10
  --bg-color: #0c0f15;
9
11
  --card-bg: #161b22;
@@ -604,4 +606,4 @@
604
606
  </script>
605
607
  </body>
606
608
 
607
- </html>
609
+ </html>
@@ -0,0 +1,6 @@
1
+ export type ChannelTarget = {
2
+ platform?: string;
3
+ channelId: string;
4
+ };
5
+ export declare function parseChannelTarget(input: string): ChannelTarget | null;
6
+ export declare function chunkLines(header: string, lines: string[], maxLines: number): string[];
@@ -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-alpha.19",
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
- # koishi-plugin-monetary-bourse
1
+ <h1 align="center">koishi-plugin-monetary-bourse</h1>
2
2
 
3
- [![npm](https://img.shields.io/npm/v/koishi-plugin-monetary-bourse?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-monetary-bourse) [![GitHub stars](https://img.shields.io/github/stars/BYWled/koishi-plugin-monetary-bourse?style=flat-square&logo=github)](https://github.com/BYWled/koishi-plugin-monetary-bourse) [![Gitee](https://img.shields.io/badge/Gitee-Project-c71d23?style=flat-square&logo=gitee&logoColor=white)](https://gitee.com/BYWled/koishi-plugin-monetary-bourse)
3
+ <p align="center">
4
+ 为 Koishi 提供基于 <code>monetary</code> 通用货币系统的股票交易所功能。
5
+ </p>
4
6
 
5
- Koishi 提供基于 `monetary` 通用货币系统的股票交易所功能。
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-alpha.19**
25
+ > 版本:**3.0.0**
10
26
 
11
27
  ## ✨ 特性
12
28
 
@@ -17,11 +33,15 @@
17
33
  - **🖼️ 全可视化交互**:
18
34
  - **专业走势图**:复刻 TradingView 风格的深色玻璃拟态 K 线图,包含动态呼吸灯、渐变填充与详细指标。
19
35
  - **持仓资产卡片**:精美渲染个人持仓、成本分析、浮动盈亏比及排队中的挂单详情。
20
- - **交易交割单**:**(New)** 买卖成交瞬间生成**交易回单图片**,在 K 线图上精确标记买卖点位,直观展示单笔盈亏与买入成本线。
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)[![npm](https://img.shields.io/npm/v/koishi-plugin-monetary-bank?style=flat-square)](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