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.
- package/lib/commands-admin.d.ts +1 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +230 -23
- 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 +47 -2
package/lib/commands-admin.d.ts
CHANGED
|
@@ -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
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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:
|
|
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(
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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,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
|
|
|
@@ -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
|
-
- **
|
|
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)。
|