@kamuira/stock-analyzer 1.4.0 → 1.4.1

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/fundamentals.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * 基本面模块 — 数据来自东方财富 F10 主要财务指标(RPT_F10_FINANCE_MAINFINADATA)
3
3
  *
4
- * 取最近一期报告:ROE、营收同比、扣非净利润同比、毛利率、资产负债率。
5
- * 打分思路:成长性(营收/利润增速) + 盈利质量(ROE/毛利) + 财务健康(负债率)。
4
+ * 取最近一期报告:ROE、营收同比、扣非净利润同比、毛利率、资产负债率、净现比(利润现金含量)。
5
+ * 打分思路:成长性(营收/利润增速) + 盈利质量(ROE/毛利/净现比) + 财务健康(负债率)。
6
+ * 净现比 = 每股经营现金流/每股收益,识别"高增长但赚的是应收、不赚现金"的风险。
6
7
  *
7
8
  * 注意:季度更新;为"当前值",不参与回测。
8
9
  */
@@ -13,7 +14,7 @@ async function fetchFundamentals(code) {
13
14
  if (!secu) return null; // 非 A 股
14
15
  const url = 'https://datacenter.eastmoney.com/securities/api/data/v1/get'
15
16
  + '?reportName=RPT_F10_FINANCE_MAINFINADATA'
16
- + '&columns=SECUCODE,REPORT_DATE,ROEJQ,YYZSRGDHBZC,KCFJCXSYJLRTZ,XSMLL,ZCFZL'
17
+ + '&columns=SECUCODE,REPORT_DATE,ROEJQ,YYZSRGDHBZC,KCFJCXSYJLRTZ,XSMLL,ZCFZL,EPSJB,MGJYXJJE'
17
18
  + `&filter=(SECUCODE%3D%22${secu}%22)`
18
19
  + '&pageSize=1&sortColumns=REPORT_DATE&sortTypes=-1&source=HSF10&client=PC';
19
20
  const j = await getJSON(url, { Referer: 'https://emweb.securities.eastmoney.com/' });
@@ -27,6 +28,8 @@ async function fetchFundamentals(code) {
27
28
  profitYoY: r.KCFJCXSYJLRTZ,// 扣非净利润同比增长
28
29
  grossMargin: r.XSMLL, // 销售毛利率
29
30
  debtRatio: r.ZCFZL, // 资产负债率
31
+ eps: r.EPSJB, // 基本每股收益
32
+ ocfps: r.MGJYXJJE, // 每股经营现金流(用于算净现比/利润现金含量)
30
33
  };
31
34
  }
32
35
 
@@ -66,6 +69,18 @@ function scoreFundamentals(f) {
66
69
  }
67
70
 
68
71
  if (f.grossMargin != null) signals.push(`毛利率${f.grossMargin.toFixed(1)}%`);
72
+
73
+ // 利润现金含量(净现比 = 每股经营现金流 / 每股收益):识别"高增长但不赚现金/利润是应收"的风险
74
+ // 注:为当期值,单季(尤其Q1)有季节性,故权重适中、明确标注"当期"
75
+ let cashRatio = null;
76
+ if (f.eps != null && f.eps > 0 && f.ocfps != null) {
77
+ cashRatio = f.ocfps / f.eps;
78
+ if (f.ocfps < 0) { score -= 12; signals.push(`⚠️当期经营现金流为负(每股${f.ocfps.toFixed(2)}元),利润质量存疑`); }
79
+ else if (cashRatio >= 0.8) { score += 5; signals.push(`净现比${cashRatio.toFixed(2)}(利润现金含量高)`); }
80
+ else if (cashRatio >= 0.3) { signals.push(`净现比${cashRatio.toFixed(2)}(利润现金含量一般)`); }
81
+ else { score -= 8; signals.push(`⚠️净现比${cashRatio.toFixed(2)}(利润现金含量低,多为应收/利润质量存疑)`); }
82
+ }
83
+
69
84
  if (f.reportDate) signals.push(`报告期:${f.reportDate}`);
70
85
 
71
86
  score = Math.max(0, Math.min(100, Math.round(score)));
@@ -73,6 +88,7 @@ function scoreFundamentals(f) {
73
88
  score, signals,
74
89
  label: score >= 65 ? '优质' : score >= 45 ? '中等' : '偏弱',
75
90
  roe: f.roe, revenueYoY: f.revenueYoY, profitYoY: f.profitYoY, reportDate: f.reportDate,
91
+ cashRatio: cashRatio != null ? +cashRatio.toFixed(2) : null,
76
92
  };
77
93
  }
78
94
 
package/news.js CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * 消息面模块 — 数据来自东方财富公告接口(np-anotice-stock)
3
3
  *
4
- * 用关键词对最近的公告标题做利好/利空初筛(减持/问询=利空,预增/回购/中标=利好),
5
- * 按时间衰减加权,聚合成 0~100 分(50=中性)。利空权重高于利好(风险更重要)。
4
+ * 用「带上下文护栏的分类器」对最近公告标题做利好/利空判定,按时间衰减加权,
5
+ * 聚合成 0~100 分(50=中性)。利空权重高于利好(风险更重要)。
6
+ * 护栏修正了简单关键词法的典型误判:解除质押/募资监管协议/再融资审核问询 不再被误判为利空。
6
7
  *
7
8
  * 特点:
8
9
  * - 关键词法,免费、快(每只 1 次 HTTP,可并发);带 20 分钟内存缓存
@@ -13,21 +14,51 @@
13
14
  */
14
15
  const { getJSON, toSecuCode } = require('./http-util');
15
16
 
16
- // 利好 / 利空关键词(命中即计分;中性/行政事项不匹配)
17
- const POSITIVE = ['预增', '预盈', '扭亏', '增持', '回购', '中标', '订单', '合同', '战略合作',
18
- '收购', '获批', '分红', '派息', '业绩快报', '量产'];
19
- const NEGATIVE = ['减持', '问询函', '关注函', '立案', '处罚', '违规', '诉讼', '仲裁', '商誉减值',
20
- '计提', '预减', '预亏', '亏损', '退市', '风险警示', '*ST', 'ST', '质押', '冻结', '终止',
21
- '下修', '监管', '警示函', '更正', '延期'];
22
-
23
17
  /**
24
- * 上下文护栏:命中的关键词在某些语境下不算利好/利空,返回 true 表示应忽略。
25
- * 例:"回购"出现在股权激励/限制性股票/期权语境里,是行政事项而非真实回购利好。
18
+ * 公告标题分类器 —— 带上下文护栏,返回 { tag:'pos'|'neg', weight, reason } 或 null(中性/忽略)。
19
+ * weight 为相对强度(再乘以基础分与时间衰减)。
20
+ *
21
+ * 重点解决简单关键词法的三类误判:
22
+ * 1) "解除质押" 含"质押" 却被判利空 → 解押其实是风险解除,弱利好
23
+ * 2) "募集资金三方监管协议" 含"监管" 却被判利空 → 常规事项,忽略
24
+ * 3) "向特定对象发行…审核问询函回复" 含"问询函" 却被判利空 → 定增/再融资审核流程,中性偏正;
25
+ * 只有"交易异常波动问询/关注"才是真利空
26
+ * 同时区分 增持↔减持、扩产型定增↔普通融资。
26
27
  */
27
- function isFalsePositive(kw, title) {
28
- if (kw === '回购' && /激励|限制性|期权/.test(title)) return true;
29
- if (kw === '增持' && /激励|限制性|期权/.test(title)) return true;
30
- return false;
28
+ function classifyAnnouncement(title) {
29
+ const t = title;
30
+
31
+ // ===== 护栏:看似利空、实为中性/利好的语境(优先判定) =====
32
+ if (/解除质押|解押/.test(t)) return { tag: 'pos', weight: 0.4, reason: '解除质押(风险解除)' };
33
+ if (/募集资金.*(监管|专户|存放|三方)|三方监管协议|监管协议/.test(t)) return null; // 募资监管=常规,忽略
34
+ // 再融资/重组的审核问询、回复、募集说明书更新等 = 推进流程,中性偏正(非利空)
35
+ if (/(向特定对象发行|非公开发行|定向增发|定增|可转债|配股|发行股票|发行A股|资产重组|重大资产)/.test(t)
36
+ && /(问询|审核|回复|反馈|募集说明书|更新|受理|批复|注册|核准|申请文件|预案|问询函)/.test(t)) {
37
+ return { tag: 'pos', weight: 0.3, reason: '再融资/重组推进' };
38
+ }
39
+
40
+ // ===== 强利空:真实风险事件 =====
41
+ if (/(交易|股票|股价).*(异常波动|异动).*(问询|关注)|(异常波动|异动).*问询/.test(t)) return { tag: 'neg', weight: 1.2, reason: '交易异动问询' };
42
+ if (/立案|行政处罚|被处罚|涉嫌|稽查|警示函|监管措施|责令改正|纪律处分/.test(t)) return { tag: 'neg', weight: 1.4, reason: '监管处罚/立案' };
43
+ if (/退市|风险警示|\*ST|实施其他风险警示/.test(t)) return { tag: 'neg', weight: 1.5, reason: '退市/ST风险' };
44
+ if (/减持/.test(t) && !/增持/.test(t)) return { tag: 'neg', weight: 1.0, reason: '股东减持' };
45
+ if (/预减|预亏|由盈转亏|业绩.*(下滑|下降|亏损)|商誉减值|计提.*减值|资产减值/.test(t)) return { tag: 'neg', weight: 1.2, reason: '业绩下滑/减值' };
46
+ if (/诉讼|仲裁|被起诉/.test(t)) return { tag: 'neg', weight: 0.7, reason: '诉讼仲裁' };
47
+ if (/冻结|平仓|司法拍卖/.test(t)) return { tag: 'neg', weight: 0.8, reason: '股份冻结/平仓' };
48
+ if (/质押/.test(t)) return { tag: 'neg', weight: 0.4, reason: '股权质押' }; // 弱(解押已在护栏排除)
49
+ if (/下修|终止(?!上市辅导)|延期/.test(t)) return { tag: 'neg', weight: 0.4, reason: '下修/终止/延期' };
50
+
51
+ // ===== 利好 =====
52
+ if (/(定增|向特定对象发行|非公开发行|可转债|投资|拟投资|新建|开工|募投).*(项目|产能|扩产|生产线|基地|建设)/.test(t)) return { tag: 'pos', weight: 1.0, reason: '扩产/投资项目' };
53
+ if (/扩产|投产|达产|量产|产能.*释放|满产/.test(t)) return { tag: 'pos', weight: 1.0, reason: '产能释放' };
54
+ if (/预增|预盈|扭亏|业绩快报|净利.*增长|业绩.*大增/.test(t)) return { tag: 'pos', weight: 1.2, reason: '业绩预增' };
55
+ if (/增持/.test(t) && !/激励|限制性|期权/.test(t)) return { tag: 'pos', weight: 1.1, reason: '股东增持' };
56
+ if (/回购/.test(t) && !/激励|限制性|期权|注销.*回购/.test(t)) return { tag: 'pos', weight: 0.9, reason: '股份回购' };
57
+ if (/中标|中选|大额订单|重大合同|重大订单|签约|战略合作|框架协议/.test(t)) return { tag: 'pos', weight: 0.9, reason: '订单/合作' };
58
+ if (/收购|并购|获批|核准.*(项目|生产)|认证|入选|获得.*(资质|认定)|通过.*认证/.test(t)) return { tag: 'pos', weight: 0.7, reason: '收购/获批/认证' };
59
+ if (/分红|派息|利润分配/.test(t)) return { tag: 'pos', weight: 0.5, reason: '分红派息' };
60
+
61
+ return null; // 常规/中性事项
31
62
  }
32
63
 
33
64
  /** 'sz001309' → '001309';非 A 股返回 null */
@@ -87,10 +118,12 @@ function scoreNews(items) {
87
118
  for (const it of items) {
88
119
  const w = recencyWeight(daysAgo(it.date));
89
120
  if (w === 0) continue;
90
- const neg = NEGATIVE.find(k => it.title.includes(k));
91
- const pos = POSITIVE.find(k => it.title.includes(k) && !isFalsePositive(k, it.title));
92
- if (neg) { score -= 12 * w; hits.push({ date: it.date, title: it.title, tag: '利空', kw: neg }); }
93
- else if (pos) { score += 7 * w; hits.push({ date: it.date, title: it.title, tag: '利好', kw: pos }); }
121
+ const c = classifyAnnouncement(it.title);
122
+ if (!c) continue;
123
+ // 利空基础分(12)高于利好(7):风险比利好更重要;再乘以事件强度与时间衰减
124
+ const delta = (c.tag === 'neg' ? -12 : 7) * c.weight * w;
125
+ score += delta;
126
+ hits.push({ date: it.date, title: it.title, tag: c.tag === 'neg' ? '利空' : '利好', reason: c.reason });
94
127
  }
95
128
  score = Math.max(0, Math.min(100, Math.round(score)));
96
129
 
@@ -100,7 +133,7 @@ function scoreNews(items) {
100
133
  } else {
101
134
  // 利空优先展示
102
135
  hits.sort((a, b) => (a.tag === '利空' ? -1 : 1) - (b.tag === '利空' ? -1 : 1));
103
- for (const h of hits.slice(0, 6)) signals.push(`[${h.tag}] ${h.date} ${h.title}`);
136
+ for (const h of hits.slice(0, 6)) signals.push(`[${h.tag}·${h.reason}] ${h.date} ${h.title}`);
104
137
  }
105
138
  const label = score >= 60 ? '偏多' : score >= 40 ? '中性' : '偏空';
106
139
  return { score, signals, label, hitCount: hits.length };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamuira/stock-analyzer",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "preferGlobal": true,
5
5
  "description": "A股/台股综合分析工具 - 技术面+估值+基本面+消息面四维评分、批量排名、含成本回测、买卖建议、行业板块划分、热门板块与潜伏启动筛选",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -184,7 +184,7 @@ async function rankStocks(inputs) {
184
184
  dims,
185
185
  launch: { score: launch.score, isLaunching: launch.isLaunching, stage: launch.stage, volIgnite: launch.volIgnite, rangePct: launch.rangePct, signals: launch.signals },
186
186
  valuation: { score: val.score, label: val.label || null, peTTM: val.peTTM != null ? +val.peTTM.toFixed(1) : null, pePercentile: val.pePercentile, board: val.board || null, signals: val.signals },
187
- fundamental: { score: fund.score, label: fund.label || null, roe: fund.roe, revenueYoY: fund.revenueYoY, profitYoY: fund.profitYoY, reportDate: fund.reportDate, signals: fund.signals },
187
+ fundamental: { score: fund.score, label: fund.label || null, roe: fund.roe, revenueYoY: fund.revenueYoY, profitYoY: fund.profitYoY, cashRatio: fund.cashRatio != null ? fund.cashRatio : null, reportDate: fund.reportDate, signals: fund.signals },
188
188
  news: { score: news.score, label: news.label || null, hitCount: news.hitCount || 0, signals: news.signals },
189
189
  };
190
190
  } catch (e) {