Skip to content

Commit b145115

Browse files
authored
feat: add dividend method (#17)
新增 A 股分红派送详情接口 `getDividendDetail`,支持获取历史分红记录,涵盖现金分红、送转股份、财务指标(EPS、BPS、净利润同比等)、关键日期(登记日、除权日、派息日)及方案进度等 20+ 维度数据
1 parent 056d5ce commit b145115

File tree

14 files changed

+705
-11
lines changed

14 files changed

+705
-11
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "stock-sdk",
3-
"version": "1.5.0",
3+
"version": "1.6.0",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.js",

src/core/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export const EM_CONCEPT_CONS_URL = 'https://29.push2.eastmoney.com/api/qt/clist/
3737
export const EM_CONCEPT_KLINE_URL = 'https://91.push2his.eastmoney.com/api/qt/stock/kline/get';
3838
export const EM_CONCEPT_TRENDS_URL = 'https://push2his.eastmoney.com/api/qt/stock/trends2/get';
3939

40+
// 东方财富数据中心 API
41+
export const EM_DATACENTER_URL = 'https://datacenter-web.eastmoney.com/api/data/v1/get';
42+
4043
// 默认配置
4144
export const DEFAULT_TIMEOUT = 30000;
4245
export const DEFAULT_BATCH_SIZE = 500;

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export {
5454
EM_CONCEPT_CONS_URL,
5555
EM_CONCEPT_KLINE_URL,
5656
EM_CONCEPT_TRENDS_URL,
57+
EM_DATACENTER_URL,
5758
DEFAULT_TIMEOUT,
5859
DEFAULT_BATCH_SIZE,
5960
MAX_BATCH_SIZE,
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/**
2+
* 东方财富 - 分红派送详情
3+
* 数据来源:https://data.eastmoney.com/yjfp/detail/{symbol}.html
4+
*/
5+
import { RequestClient, EM_DATACENTER_URL } from '../../core';
6+
import type { DividendDetail } from '../../types';
7+
8+
/**
9+
* 分红派送详情 API 响应结构
10+
*/
11+
interface DividendApiResponse {
12+
result?: {
13+
pages?: number;
14+
data?: DividendApiItem[];
15+
};
16+
}
17+
18+
/**
19+
* 分红派送 API 原始数据项(根据实际 API 返回确认)
20+
*/
21+
interface DividendApiItem {
22+
/** 股票代码 */
23+
SECURITY_CODE?: string;
24+
/** 股票名称 */
25+
SECURITY_NAME_ABBR?: string;
26+
/** 报告期 */
27+
REPORT_DATE?: string;
28+
/** 业绩披露日期/预案公告日 */
29+
PLAN_NOTICE_DATE?: string;
30+
/** 业绩披露日期(同上,用于兼容) */
31+
PUBLISH_DATE?: string;
32+
/** 送转总比例(每10股送转X股) */
33+
BONUS_IT_RATIO?: number;
34+
/** 送股比例(每10股送X股) */
35+
BONUS_RATIO?: number;
36+
/** 转股比例(每10股转X股) */
37+
IT_RATIO?: number;
38+
/** 每10股派息(税前),单位:元 */
39+
PRETAX_BONUS_RMB?: number;
40+
/** 分红描述(如:10派2.36元(含税,扣税后2.124元))- 注意:实际是 IMPL_PLAN_PROFILE */
41+
IMPL_PLAN_PROFILE?: string;
42+
/** 股息率 - 实际字段名 DIVIDENT_RATIO */
43+
DIVIDENT_RATIO?: number;
44+
/** 每股收益(元) */
45+
BASIC_EPS?: number;
46+
/** 每股净资产(元) - 实际字段名 BVPS */
47+
BVPS?: number;
48+
/** 每股公积金(元) */
49+
PER_CAPITAL_RESERVE?: number;
50+
/** 每股未分配利润(元) */
51+
PER_UNASSIGN_PROFIT?: number;
52+
/** 净利润同比增长(%) - 实际字段名 PNP_YOY_RATIO */
53+
PNP_YOY_RATIO?: number;
54+
/** 总股本(股) */
55+
TOTAL_SHARES?: number;
56+
/** 股权登记日 */
57+
EQUITY_RECORD_DATE?: string;
58+
/** 除权除息日 */
59+
EX_DIVIDEND_DATE?: string;
60+
/** 派息日 */
61+
PAY_DATE?: string;
62+
/** 方案进度(如:实施分配)- 实际字段名 ASSIGN_PROGRESS */
63+
ASSIGN_PROGRESS?: string;
64+
/** 最新公告日期 */
65+
NOTICE_DATE?: string;
66+
}
67+
68+
/**
69+
* 解析日期字符串,返回 YYYY-MM-DD 格式
70+
*/
71+
function parseDate(dateStr?: string): string | null {
72+
if (!dateStr) {
73+
return null;
74+
}
75+
// API 返回格式:2024-06-28 00:00:00 或 2024-06-28T00:00:00.000
76+
const match = dateStr.match(/^(\d{4}-\d{2}-\d{2})/);
77+
return match ? match[1] : null;
78+
}
79+
80+
/**
81+
* 将 API 原始数据转换为标准格式
82+
*/
83+
function mapToDividendDetail(item: DividendApiItem): DividendDetail {
84+
return {
85+
// 基本信息
86+
code: item.SECURITY_CODE ?? '',
87+
name: item.SECURITY_NAME_ABBR ?? '',
88+
reportDate: parseDate(item.REPORT_DATE),
89+
planNoticeDate: parseDate(item.PLAN_NOTICE_DATE),
90+
disclosureDate: parseDate(item.PUBLISH_DATE ?? item.PLAN_NOTICE_DATE),
91+
92+
// 送转股份信息
93+
assignTransferRatio: item.BONUS_IT_RATIO ?? null,
94+
bonusRatio: item.BONUS_RATIO ?? null,
95+
transferRatio: item.IT_RATIO ?? null,
96+
97+
// 现金分红信息 - 修正映射
98+
dividendPretax: item.PRETAX_BONUS_RMB ?? null,
99+
dividendDesc: item.IMPL_PLAN_PROFILE ?? null, // ✅ 修正:IMPL_PLAN_PROFILE 是描述
100+
dividendYield: item.DIVIDENT_RATIO ?? null, // ✅ 修正:DIVIDENT_RATIO 是股息率
101+
102+
// 财务指标 - 修正映射
103+
eps: item.BASIC_EPS ?? null,
104+
bps: item.BVPS ?? null, // ✅ 修正:BVPS 是每股净资产
105+
capitalReserve: item.PER_CAPITAL_RESERVE ?? null,
106+
unassignedProfit: item.PER_UNASSIGN_PROFIT ?? null,
107+
netProfitYoy: item.PNP_YOY_RATIO ?? null, // ✅ 修正:PNP_YOY_RATIO 是净利润同比
108+
totalShares: item.TOTAL_SHARES ?? null,
109+
110+
// 关键日期
111+
equityRecordDate: parseDate(item.EQUITY_RECORD_DATE),
112+
exDividendDate: parseDate(item.EX_DIVIDEND_DATE),
113+
payDate: parseDate(item.PAY_DATE),
114+
115+
// 进度信息 - 修正映射
116+
assignProgress: item.ASSIGN_PROGRESS ?? null, // ✅ 修正:ASSIGN_PROGRESS 是方案进度
117+
noticeDate: parseDate(item.NOTICE_DATE),
118+
};
119+
}
120+
121+
/**
122+
* 获取股票分红派送详情
123+
* @param client - 请求客户端
124+
* @param symbol - 股票代码(纯数字或带交易所前缀,如 '600519' 或 'sh600519')
125+
* @returns 分红派送详情列表,按报告日期降序排列
126+
*/
127+
export async function getDividendDetail(
128+
client: RequestClient,
129+
symbol: string
130+
): Promise<DividendDetail[]> {
131+
// 移除可能的交易所前缀
132+
const pureSymbol = symbol.replace(/^(sh|sz|bj)/, '');
133+
134+
const allData: DividendDetail[] = [];
135+
let page = 1;
136+
let totalPages = 1;
137+
138+
do {
139+
const params = new URLSearchParams({
140+
sortColumns: 'REPORT_DATE',
141+
sortTypes: '-1',
142+
pageSize: '500',
143+
pageNumber: String(page),
144+
reportName: 'RPT_SHAREBONUS_DET',
145+
columns: 'ALL',
146+
quoteColumns: '',
147+
source: 'WEB',
148+
client: 'WEB',
149+
filter: `(SECURITY_CODE="${pureSymbol}")`,
150+
});
151+
152+
const url = `${EM_DATACENTER_URL}?${params.toString()}`;
153+
const json = await client.get<DividendApiResponse>(url, {
154+
responseType: 'json',
155+
});
156+
157+
const result = json?.result;
158+
if (!result || !Array.isArray(result.data)) {
159+
break;
160+
}
161+
162+
// 首次请求获取总页数
163+
if (page === 1) {
164+
totalPages = result.pages ?? 1;
165+
}
166+
167+
const items = result.data.map(mapToDividendDetail);
168+
allData.push(...items);
169+
page++;
170+
} while (page <= totalPages);
171+
172+
return allData;
173+
}

src/providers/eastmoney/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ export {
3838
type ConceptBoardMinuteKlineOptions,
3939
} from './conceptBoard';
4040

41+
// 分红派送
42+
export { getDividendDetail } from './dividend';
43+

src/sdk.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import type {
3131
ConceptBoardMinuteTimeline,
3232
ConceptBoardMinuteKline,
3333
SearchResult,
34+
DividendDetail,
3435
} from './types';
3536

3637

@@ -445,6 +446,20 @@ export class StockSDK {
445446
return tencent.getTradingCalendar(this.client);
446447
}
447448

449+
/**
450+
* 获取股票分红派送详情
451+
* @param symbol - 股票代码(纯数字或带交易所前缀),如 '600519' 或 'sh600519'
452+
* @returns 分红派送详情列表,按报告日期降序排列
453+
*
454+
* @example
455+
* // 获取贵州茅台的分红历史
456+
* const dividends = await sdk.getDividendDetail('600519');
457+
* console.log(dividends[0].dividendPretax); // 每10股派息(税前)
458+
*/
459+
getDividendDetail(symbol: string): Promise<DividendDetail[]> {
460+
return eastmoney.getDividendDetail(this.client, symbol);
461+
}
462+
448463
// ==================== 技术指标 ====================
449464

450465
/**

src/types.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,3 +564,53 @@ export interface SearchResult {
564564
/** 资产类别 (GP-A/GP/KJ 等) */
565565
type: string;
566566
}
567+
568+
/**
569+
* 分红派送详情
570+
*/
571+
export interface DividendDetail {
572+
/** 股票代码 */
573+
code: string;
574+
/** 股票名称 */
575+
name: string;
576+
/** 报告期 YYYY-MM-DD */
577+
reportDate: string | null;
578+
/** 预案公告日 YYYY-MM-DD */
579+
planNoticeDate: string | null;
580+
/** 业绩披露日期 YYYY-MM-DD */
581+
disclosureDate: string | null;
582+
/** 送转总比例(每10股送转X股) */
583+
assignTransferRatio: number | null;
584+
/** 送股比例(每10股送X股) */
585+
bonusRatio: number | null;
586+
/** 转股比例(每10股转X股) */
587+
transferRatio: number | null;
588+
/** 每10股派息(税前),单位:元 */
589+
dividendPretax: number | null;
590+
/** 分红描述(如:10派2.36元(含税,扣税后2.124元)) */
591+
dividendDesc: string | null;
592+
/** 股息率 */
593+
dividendYield: number | null;
594+
/** 每股收益(元) */
595+
eps: number | null;
596+
/** 每股净资产(元) */
597+
bps: number | null;
598+
/** 每股公积金(元) */
599+
capitalReserve: number | null;
600+
/** 每股未分配利润(元) */
601+
unassignedProfit: number | null;
602+
/** 净利润同比增长(%) */
603+
netProfitYoy: number | null;
604+
/** 总股本(股) */
605+
totalShares: number | null;
606+
/** 股权登记日 YYYY-MM-DD */
607+
equityRecordDate: string | null;
608+
/** 除权除息日 YYYY-MM-DD */
609+
exDividendDate: string | null;
610+
/** 现金分红发放日 YYYY-MM-DD */
611+
payDate: string | null;
612+
/** 方案进度(如:实施分配、股东大会预案等) */
613+
assignProgress: string | null;
614+
/** 最新公告日期 YYYY-MM-DD */
615+
noticeDate: string | null;
616+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* 分红派送详情集成测试
3+
*/
4+
import { describe, it, expect } from 'vitest';
5+
import { StockSDK } from '../../src';
6+
7+
describe('getDividendDetail 集成测试', () => {
8+
const sdk = new StockSDK();
9+
10+
it('应获取贵州茅台的分红历史(完整字段)', async () => {
11+
const dividends = await sdk.getDividendDetail('600519');
12+
13+
expect(dividends).toBeInstanceOf(Array);
14+
expect(dividends.length).toBeGreaterThan(0);
15+
16+
// 验证完整数据结构
17+
const first = dividends[0];
18+
19+
// 打印数据供验证
20+
console.log('Verified Dividend Data:', JSON.stringify(first, null, 2));
21+
22+
// 基本信息
23+
expect(first).toHaveProperty('code');
24+
expect(first).toHaveProperty('name');
25+
expect(first).toHaveProperty('reportDate');
26+
expect(first).toHaveProperty('planNoticeDate');
27+
expect(first).toHaveProperty('disclosureDate');
28+
29+
// 送转股份信息
30+
expect(first).toHaveProperty('assignTransferRatio');
31+
expect(first).toHaveProperty('bonusRatio');
32+
expect(first).toHaveProperty('transferRatio');
33+
34+
// 现金分红信息
35+
expect(first).toHaveProperty('dividendPretax');
36+
expect(first).toHaveProperty('dividendDesc');
37+
expect(first).toHaveProperty('dividendYield');
38+
39+
// 财务指标
40+
expect(first).toHaveProperty('eps');
41+
expect(first).toHaveProperty('bps');
42+
expect(first).toHaveProperty('capitalReserve');
43+
expect(first).toHaveProperty('unassignedProfit');
44+
expect(first).toHaveProperty('netProfitYoy');
45+
expect(first).toHaveProperty('totalShares');
46+
47+
// 关键日期
48+
expect(first).toHaveProperty('equityRecordDate');
49+
expect(first).toHaveProperty('exDividendDate');
50+
expect(first).toHaveProperty('payDate');
51+
52+
// 进度信息
53+
expect(first).toHaveProperty('assignProgress');
54+
expect(first).toHaveProperty('noticeDate');
55+
56+
// 验证修正后的字段有实际值
57+
expect(first.code).toBe('600519');
58+
expect(first.name).toContain('茅台');
59+
expect(first.assignProgress).toBe('实施分配'); // ✅ 验证方案进度修复
60+
expect(first.dividendDesc).toContain('派'); // ✅ 验证分红描述修复
61+
expect(first.dividendYield).toBeGreaterThan(0); // ✅ 验证股息率补齐
62+
expect(first.bps).toBeGreaterThan(0); // ✅ 验证每股净资产补齐
63+
expect(first.netProfitYoy).toBeDefined(); // ✅ 验证净利润同比补齐
64+
});
65+
66+
it('应支持带交易所前缀的股票代码', async () => {
67+
const dividends = await sdk.getDividendDetail('sh600519');
68+
69+
expect(dividends).toBeInstanceOf(Array);
70+
expect(dividends.length).toBeGreaterThan(0);
71+
expect(dividends[0].code).toBe('600519');
72+
});
73+
74+
it('应获取深市股票的分红历史', async () => {
75+
const dividends = await sdk.getDividendDetail('000858');
76+
77+
expect(dividends).toBeInstanceOf(Array);
78+
expect(dividends.length).toBeGreaterThan(0);
79+
expect(dividends[0].code).toBe('000858');
80+
81+
// 验证财务指标字段有值
82+
const first = dividends[0];
83+
expect(first.eps).toBeDefined();
84+
expect(first.bps).toBeDefined();
85+
});
86+
87+
it('应获取创业板股票的分红历史', async () => {
88+
const dividends = await sdk.getDividendDetail('300073');
89+
90+
expect(dividends).toBeInstanceOf(Array);
91+
// 创业板可能没有分红历史,只验证结构正确
92+
if (dividends.length > 0) {
93+
expect(dividends[0]).toHaveProperty('code');
94+
expect(dividends[0]).toHaveProperty('dividendPretax');
95+
expect(dividends[0]).toHaveProperty('dividendYield');
96+
}
97+
});
98+
99+
it('不存在的股票代码应返回空数组', async () => {
100+
const dividends = await sdk.getDividendDetail('999999');
101+
102+
expect(dividends).toBeInstanceOf(Array);
103+
expect(dividends.length).toBe(0);
104+
});
105+
});

0 commit comments

Comments
 (0)