koishi-plugin-monetary-bourse 3.0.0-alpha.18 → 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.
@@ -9,6 +9,7 @@ type AdminCommandDeps = {
9
9
  setWasMarketOpen: (value: boolean) => void;
10
10
  isMarketOpen: () => Promise<boolean>;
11
11
  switchKLinePattern: (reason: string, expectedPrice?: number, cycleProgress?: number) => void;
12
+ broadcastMacroNews: (targetPrice: number, basePrice: number) => Promise<void>;
12
13
  };
13
14
  export declare function registerAdminCommands(deps: AdminCommandDeps): void;
14
15
  export {};
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
  }
@@ -68,6 +69,10 @@ export type SellFeeTier = {
68
69
  minAmount: number;
69
70
  feePercent: number;
70
71
  };
72
+ export type NewsItem = {
73
+ text: string;
74
+ weight: number;
75
+ };
71
76
  export interface Config {
72
77
  currency: string;
73
78
  stockName: string;
@@ -87,6 +92,11 @@ export interface Config {
87
92
  biasMax: number;
88
93
  dividendIntervalDays: number;
89
94
  maxDividendRate: number;
95
+ dividendBroadcastChannels: string[];
96
+ newsDeviationThreshold: number;
97
+ broadcastChannels: string[];
98
+ positiveNews: NewsItem[];
99
+ negativeNews: NewsItem[];
90
100
  }
91
101
  export declare const Config: Schema<Config>;
92
102
  export declare function apply(ctx: Context, config: Config): void;
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种) ====================
@@ -522,7 +549,8 @@ function registerAdminCommands(deps) {
522
549
  getDailyOpenPrice,
523
550
  setWasMarketOpen,
524
551
  isMarketOpen,
525
- switchKLinePattern
552
+ switchKLinePattern,
553
+ broadcastMacroNews
526
554
  } = deps;
527
555
  ctx.command(
528
556
  "stock.control <price:number> [hours:number]",
@@ -568,6 +596,7 @@ function registerAdminCommands(deps) {
568
596
  updateFields
569
597
  );
570
598
  }
599
+ await broadcastMacroNews(targetPriceClamped, currentPrice);
571
600
  const hint = targetPriceClamped !== price ? `(已按±50%限幅从${price}调整为${Number(targetPriceClamped.toFixed(2))})` : "";
572
601
  return `宏观调控已设置:
573
602
  目标价格:${Number(targetPriceClamped.toFixed(2))}${hint}
@@ -623,6 +652,66 @@ function registerAdminCommands(deps) {
623
652
  }
624
653
  __name(registerAdminCommands, "registerAdminCommands");
625
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
+
626
715
  // src/commands-stock.ts
627
716
  function resolveSellFeePercent(amount, tiers) {
628
717
  if (!Array.isArray(tiers) || tiers.length === 0) return 0;
@@ -997,6 +1086,7 @@ function registerStockCommands(deps) {
997
1086
  amount,
998
1087
  price: currentPrice,
999
1088
  cost: netGain,
1089
+ buyCost: soldCost,
1000
1090
  startTime,
1001
1091
  endTime
1002
1092
  });
@@ -1072,23 +1162,27 @@ function registerStockCommands(deps) {
1072
1162
  const pending = await ctx.database.get("bourse_pending", { userId });
1073
1163
  let holdingData = null;
1074
1164
  const currentPrice = getCurrentPrice();
1075
- if (holdings.length > 0) {
1076
- const h2 = holdings[0];
1077
- const marketValue = fmtAmount(h2.amount * currentPrice);
1078
- const hasCostData = h2.totalCost !== void 0 && h2.totalCost !== null && h2.totalCost > 0;
1079
- const totalCost = hasCostData ? fmtAmount(h2.totalCost) : 0;
1080
- const avgCost = hasCostData && h2.amount > 0 ? fmtPrice(totalCost / h2.amount) : 0;
1081
- const profit = hasCostData ? fmtAmount(marketValue - totalCost) : null;
1082
- const profitPercent = hasCostData && totalCost > 0 ? Number((profit / totalCost * 100).toFixed(2)) : null;
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) {
1083
1177
  holdingData = {
1084
1178
  stockName: config.stockName,
1085
- amount: h2.amount,
1179
+ amount: summary.amount,
1086
1180
  currentPrice: fmtPrice(currentPrice),
1087
- avgCost: hasCostData ? avgCost : null,
1088
- totalCost: hasCostData ? totalCost : null,
1089
- marketValue,
1090
- profit,
1091
- 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
1092
1186
  };
1093
1187
  }
1094
1188
  const pendingData = pending.map((p) => {
@@ -1292,6 +1386,47 @@ var Config = import_koishi2.Schema.intersect([
1292
1386
  dividendIntervalDays: import_koishi2.Schema.number().min(1).step(1).default(7).description("分红结算周期(天)"),
1293
1387
  maxDividendRate: import_koishi2.Schema.number().min(0).max(1).step(0.01).default(0.15).description("最大分红期望利润率(0-1,超出部分用于除息而非派发)")
1294
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("分红播报"),
1392
+ import_koishi2.Schema.object({
1393
+ newsDeviationThreshold: import_koishi2.Schema.number().min(0.05).max(1).step(0.01).default(0.15).description(
1394
+ "触发新闻的偏离度阈值(例如 0.15 表示当新目标价与当前价相差 15% 以上时触发)"
1395
+ ),
1396
+ broadcastChannels: import_koishi2.Schema.array(import_koishi2.Schema.string()).default([]).description(
1397
+ "播报新闻的频道/群聊 ID 列表(为空则只在后台输出日志,不发送群消息)"
1398
+ ),
1399
+ positiveNews: import_koishi2.Schema.array(
1400
+ import_koishi2.Schema.object({
1401
+ text: import_koishi2.Schema.string().description("新闻文本"),
1402
+ weight: import_koishi2.Schema.number().min(1).step(1).default(1).description("权重")
1403
+ })
1404
+ ).role("table").default([
1405
+ {
1406
+ text: "【财经快讯】{stockName} 宣布取得重大技术突破,预期利润大增!",
1407
+ weight: 1
1408
+ },
1409
+ {
1410
+ text: "【市场异动】神秘巨头入局,{stockName} 获大额资本注资!",
1411
+ weight: 1
1412
+ }
1413
+ ]).description("利好新闻列表(可用 {stockName} 作为股票名称的占位符)"),
1414
+ negativeNews: import_koishi2.Schema.array(
1415
+ import_koishi2.Schema.object({
1416
+ text: import_koishi2.Schema.string().description("新闻文本"),
1417
+ weight: import_koishi2.Schema.number().min(1).step(1).default(1).description("权重")
1418
+ })
1419
+ ).role("table").default([
1420
+ {
1421
+ text: "【黑天鹅】{stockName} 遭遇大规模不可抗力打击,市场恐慌情绪蔓延!",
1422
+ weight: 1
1423
+ },
1424
+ {
1425
+ text: "【行业悲报】{stockName} 最新季度财报严重不及预期,高管集体减持!",
1426
+ weight: 1
1427
+ }
1428
+ ]).description("利空新闻列表(可用 {stockName} 作为股票名称的占位符)")
1429
+ }).description("新闻播报机制"),
1295
1430
  import_koishi2.Schema.object({
1296
1431
  enableDebug: import_koishi2.Schema.boolean().default(false).description("启用调试模式(开启后可使用调试指令)")
1297
1432
  }).description("开发者选项")
@@ -1321,6 +1456,7 @@ function apply(ctx, config) {
1321
1456
  amount: "integer",
1322
1457
  price: "double",
1323
1458
  cost: "double",
1459
+ buyCost: "double",
1324
1460
  startTime: "timestamp",
1325
1461
  endTime: "timestamp"
1326
1462
  },
@@ -1673,6 +1809,50 @@ function apply(ctx, config) {
1673
1809
  return { success: true };
1674
1810
  }
1675
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");
1833
+ async function broadcastMacroNews(targetPrice, basePrice) {
1834
+ const deviation = (targetPrice - basePrice) / basePrice;
1835
+ if (Math.abs(deviation) < config.newsDeviationThreshold) return;
1836
+ const newsPool = deviation > 0 ? config.positiveNews : config.negativeNews;
1837
+ if (!newsPool || newsPool.length === 0) return;
1838
+ const totalWeight = newsPool.reduce((sum, item) => sum + item.weight, 0);
1839
+ if (totalWeight <= 0) return;
1840
+ let roll = Math.random() * totalWeight;
1841
+ let selected = newsPool[newsPool.length - 1];
1842
+ for (const item of newsPool) {
1843
+ roll -= item.weight;
1844
+ if (roll < 0) {
1845
+ selected = item;
1846
+ break;
1847
+ }
1848
+ }
1849
+ const newsText = selected.text.replace(/\{stockName\}/g, config.stockName);
1850
+ logger.info(
1851
+ `触发宏观新闻播报: ${newsText} (偏离度=${(deviation * 100).toFixed(2)}%)`
1852
+ );
1853
+ await sendBroadcast(config.broadcastChannels, newsText, "新闻播报");
1854
+ }
1855
+ __name(broadcastMacroNews, "broadcastMacroNews");
1676
1856
  const patternsByCategory = {
1677
1857
  bullish: [],
1678
1858
  bearish: [],
@@ -1821,6 +2001,7 @@ function apply(ctx, config) {
1821
2001
  }, "createAutoState");
1822
2002
  if (needNewState) {
1823
2003
  await createAutoState();
2004
+ await broadcastMacroNews(state.targetPrice, currentPrice);
1824
2005
  }
1825
2006
  const basePrice = state.startPrice;
1826
2007
  const targetPrice = state.targetPrice;
@@ -2024,6 +2205,7 @@ function apply(ctx, config) {
2024
2205
  let totalDividendPaid = 0;
2025
2206
  let successCount = 0;
2026
2207
  let failCount = 0;
2208
+ const successRecords = [];
2027
2209
  for (const holding of allHoldings) {
2028
2210
  const dividendAmount = fmtAmount(
2029
2211
  holding.amount * effectiveDividendRate * currentPrice
@@ -2044,6 +2226,7 @@ function apply(ctx, config) {
2044
2226
  if (success) {
2045
2227
  totalDividendPaid += dividendAmount;
2046
2228
  successCount++;
2229
+ successRecords.push({ userId: holding.userId, amount: dividendAmount });
2047
2230
  } else {
2048
2231
  logger.warn(
2049
2232
  `分红引擎: 用户 ${holding.userId} (uid=${holding.uid}) 分红派发失败`
@@ -2051,6 +2234,25 @@ function apply(ctx, config) {
2051
2234
  failCount++;
2052
2235
  }
2053
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
+ }
2054
2256
  const oldPrice = currentPrice;
2055
2257
  currentPrice = fmtPrice(currentPrice * (1 - priceDropRate));
2056
2258
  if (currentPrice < 1) currentPrice = 1;
@@ -2074,13 +2276,17 @@ function apply(ctx, config) {
2074
2276
  } else {
2075
2277
  newEndTime = new Date(now.getTime() + 7 * 24 * 3600 * 1e3);
2076
2278
  }
2077
- await ctx.database.set("bourse_state", { key: "macro_state" }, {
2078
- startPrice: currentPrice,
2079
- targetPrice: fmtPrice(newTargetPrice),
2080
- lastCycleStart: now,
2081
- endTime: newEndTime,
2082
- lastDividendDate: now
2083
- });
2279
+ await ctx.database.set(
2280
+ "bourse_state",
2281
+ { key: "macro_state" },
2282
+ {
2283
+ startPrice: currentPrice,
2284
+ targetPrice: fmtPrice(newTargetPrice),
2285
+ lastCycleStart: now,
2286
+ endTime: newEndTime,
2287
+ lastDividendDate: now
2288
+ }
2289
+ );
2084
2290
  switchKLinePattern("分红除息");
2085
2291
  logger.info(
2086
2292
  `分红引擎: 执行完毕 | 除息前股价=${oldPrice} -> 除息后=${currentPrice} (${(-priceDropRate * 100).toFixed(2)}%) | 派发成功=${successCount}人 | 失败=${failCount}人 | 合计派发=${totalDividendPaid.toFixed(2)} ${config.currency}`
@@ -2140,7 +2346,8 @@ function apply(ctx, config) {
2140
2346
  getDailyOpenPrice,
2141
2347
  setWasMarketOpen,
2142
2348
  isMarketOpen,
2143
- switchKLinePattern
2349
+ switchKLinePattern,
2350
+ broadcastMacroNews
2144
2351
  });
2145
2352
  registerTestCommands({
2146
2353
  ctx,
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-monetary-bourse",
3
- "version": "3.0.0-alpha.18",
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.18**
9
+ > 版本:**3.0.0-alpha.20**
10
10
 
11
11
  ## ✨ 特性
12
12
 
@@ -129,6 +129,8 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
129
129
  ### 交易时间
130
130
  - **openHour**: 每日开市时间(小时,0-23,默认 `8` 点)。
131
131
  - **closeHour**: 每日休市时间(小时,0-23,默认 `23` 点)。
132
+
133
+ ### 股市开关
132
134
  - **marketStatus**: 股市总开关,可选 `open` (强制开启)、`close` (强制关闭)、`auto` (自动按时间)。
133
135
 
134
136
  ### 冻结机制
@@ -137,7 +139,9 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
137
139
  - **maxFreezeTime**: 最大冻结时间(分钟,默认 `1440` 即24小时)。
138
140
 
139
141
  ### 手续费与精度
140
- - **sellFeePercent**: 卖出手续费百分比(默认 `0`)。
142
+ - **sellFeeTiers**: 卖出手续费分档列表(默认 `[{ minAmount: 1, feePercent: 0 }]`)。
143
+ - **minAmount**: 起始股数(含)。
144
+ - **feePercent**: 手续费比例(%)。
141
145
  - **precisionInteger**: 是否启用整数精度(默认 `false`)。
142
146
 
143
147
  ### 开发者选项
@@ -148,6 +152,47 @@ A: 股价采用 **"智能期望模型"** 驱动,更贴近真实博弈:
148
152
  - **fixedUpdateHour**: 固定更新时间(小时,0-23,默认 `9`)。仅在 `fixedUpdateTime` 为 `true` 时生效。
149
153
  - **biasMax**: 宏观期望上下偏倚的最大值(默认 `0.45`)。
150
154
 
155
+ ### 分红机制
156
+ - **dividendIntervalDays**: 分红结算周期(天,默认 `7`)。
157
+ - **maxDividendRate**: 最大分红期望利润率(0-1,默认 `0.15`,超出部分用于除息而非派发)。
158
+
159
+ ### 分红播报
160
+ - **dividendBroadcastChannels**: 分红播报的频道/群聊 ID 列表(支持 `onebot:123` 或 `123`)。
161
+
162
+ ### 新闻播报机制
163
+ - **newsDeviationThreshold**: 触发新闻的偏离度阈值(默认 `0.15`,例如 `0.15` 表示偏离 15% 以上触发)。
164
+ - **broadcastChannels**: 播报新闻的频道/群聊 ID 列表(为空则仅后台日志,不发送群消息)。
165
+ - **positiveNews**: 利好新闻列表(表格配置)。
166
+ - **negativeNews**: 利空新闻列表(表格配置)。
167
+ - **text**: 新闻文本,可使用 `{stockName}` 作为股票名称占位符。
168
+ - **weight**: 权重(整数,最小 `1`,数值越大越容易被选中)。
169
+
170
+ ### 开发者选项
171
+ - **enableDebug**: 是否启用调试模式(默认 `false`)。开启后可使用 `bourse.test.*` 系列调试指令。
172
+
173
+ ### 配置示例
174
+ ```yaml
175
+ sellFeeTiers:
176
+ - minAmount: 1
177
+ feePercent: 0.6
178
+ - minAmount: 200
179
+ feePercent: 0.3
180
+
181
+ newsDeviationThreshold: 0.15
182
+ broadcastChannels:
183
+ - "123456789"
184
+ positiveNews:
185
+ - text: "【财经快讯】{stockName} 宣布取得重大技术突破,预期利润大增!"
186
+ weight: 3
187
+ - text: "【市场异动】神秘巨头入局,{stockName} 获大额资本注资!"
188
+ weight: 1
189
+ negativeNews:
190
+ - text: "【黑天鹅】{stockName} 遭遇大规模不可抗力打击,市场恐慌情绪蔓延!"
191
+ weight: 2
192
+ - text: "【行业悲报】{stockName} 最新季度财报严重不及预期,高管集体减持!"
193
+ weight: 1
194
+ ```
195
+
151
196
  ## 📝 更新日志
152
197
 
153
198
  详细的更新日志请查看 [CHANGELOG.md](./CHANGELOG.md)。