@kamuira/stock-analyzer 1.2.2 → 1.2.4

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.
Files changed (3) hide show
  1. package/analyze.js +0 -3
  2. package/index.html +84 -49
  3. package/package.json +1 -1
package/analyze.js CHANGED
@@ -35,9 +35,6 @@ const WATCH_LIST = {
35
35
  'sh600111': '北方稀土',
36
36
  'sh601318': '中国平安',
37
37
  'sh601066': '中信建投',
38
- 'tw2330': '台积电',
39
- 'tw2317': '鸿海',
40
- 'tw2454': '联发科',
41
38
  };
42
39
 
43
40
  // ==================== 数据获取 ====================
package/index.html CHANGED
@@ -72,31 +72,42 @@
72
72
  .result { display: none; }
73
73
  .result.active { display: block; }
74
74
 
75
- /* 信号卡片 */
75
+ /* 信号卡片 — 中性底色 + 顶部强调条 + 微渐变 */
76
76
  .signal-card {
77
- text-align: center; padding: 30px; border-radius: 12px;
78
- margin-bottom: 20px; position: relative; overflow: hidden;
79
- }
80
- .signal-card::before {
81
- content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
82
- opacity: 0.1; z-index: 0;
83
- }
84
- .signal-card.strong-buy { background: #1a2e1a; border: 1px solid #2d5a2d; }
85
- .signal-card.strong-buy::before { background: #00ff00; }
86
- .signal-card.buy { background: #1a2e1a; border: 1px solid #2d5a2d; }
87
- .signal-card.weak-buy { background: #1a2a1a; border: 1px solid #2a4a2a; }
88
- .signal-card.neutral { background: #2a2a1a; border: 1px solid #4a4a2a; }
89
- .signal-card.sell { background: #2e1a1a; border: 1px solid #5a2d2d; }
90
- .signal-card.strong-sell { background: #2e1a1a; border: 1px solid #5a2d2d; }
91
-
92
- .signal-card .stock-info { margin-bottom: 15px; position: relative; z-index: 1; }
77
+ text-align: center; padding: 28px 30px 26px; border-radius: 12px;
78
+ margin-bottom: 20px; position: relative;
79
+ background: #141e28; border: 1px solid #1f2d3a; border-top: 4px solid #555;
80
+ }
81
+ .signal-card.strong-buy {
82
+ border-top-color: #ff4444;
83
+ background: linear-gradient(180deg, rgba(255,68,68,0.10), rgba(255,68,68,0) 55%), #141e28;
84
+ }
85
+ .signal-card.buy {
86
+ border-top-color: #ff7043;
87
+ background: linear-gradient(180deg, rgba(255,112,67,0.07), rgba(255,112,67,0) 55%), #141e28;
88
+ }
89
+ .signal-card.weak-buy {
90
+ border-top-color: #ff9800;
91
+ background: linear-gradient(180deg, rgba(255,152,0,0.06), rgba(255,152,0,0) 55%), #141e28;
92
+ }
93
+ .signal-card.neutral { border-top-color: #888; }
94
+ .signal-card.sell {
95
+ border-top-color: #26c281;
96
+ background: linear-gradient(180deg, rgba(38,194,129,0.06), rgba(38,194,129,0) 55%), #141e28;
97
+ }
98
+ .signal-card.strong-sell {
99
+ border-top-color: #00cc66;
100
+ background: linear-gradient(180deg, rgba(0,204,102,0.10), rgba(0,204,102,0) 55%), #141e28;
101
+ }
102
+
103
+ .signal-card .stock-info { margin-bottom: 14px; }
93
104
  .signal-card .stock-name { font-size: 22px; font-weight: bold; color: #fff; }
94
105
  .signal-card .stock-code { font-size: 13px; color: #888; margin-left: 8px; }
95
- .signal-card .stock-price { font-size: 28px; font-weight: bold; margin: 8px 0; }
106
+ .signal-card .stock-price { font-size: 30px; font-weight: bold; margin: 6px 0 2px; letter-spacing: 0.5px; }
96
107
  .signal-card .stock-change { font-size: 14px; }
97
- .signal-card .signal-text { font-size: 24px; font-weight: bold; margin: 15px 0 5px; position: relative; z-index: 1; }
98
- .signal-card .signal-score { font-size: 13px; color: #aaa; position: relative; z-index: 1; }
99
- .signal-card .signal-advice { font-size: 14px; color: #ccc; margin-top: 10px; position: relative; z-index: 1; }
108
+ .signal-card .signal-text { font-size: 24px; font-weight: bold; margin: 16px 0 4px; letter-spacing: 1px; }
109
+ .signal-card .signal-score { font-size: 13px; color: #8a9aa8; }
110
+ .signal-card .signal-advice { font-size: 14px; color: #c4d0db; margin-top: 10px; }
100
111
 
101
112
  .up { color: #ff4444; }
102
113
  .down { color: #00cc66; }
@@ -218,9 +229,6 @@
218
229
  <button onclick="quickAnalyze('sh600111')">北方稀土</button>
219
230
  <button onclick="quickAnalyze('sh601318')">中国平安</button>
220
231
  <button onclick="quickAnalyze('sh601066')">中信建投</button>
221
- <button onclick="quickAnalyze('2330')">台积电</button>
222
- <button onclick="quickAnalyze('2317')">鸿海</button>
223
- <button onclick="quickAnalyze('2454')">联发科</button>
224
232
  </div>
225
233
  <div class="shortcuts" id="customShortcuts"></div>
226
234
 
@@ -333,38 +341,48 @@
333
341
  // 回测结果卡片
334
342
  if (backtestData && !backtestData.error) {
335
343
  const bt = backtestData;
336
- const s3 = bt.summary['3'] || {winRate:0,avgReturn:0};
337
- const s5 = bt.summary['5'] || {winRate:0,avgReturn:0};
338
- const s10 = bt.summary['10'] || {winRate:0,avgReturn:0};
344
+ const long = bt.long || {total:0,winRate:0,avgReturn:0,totalReturn:0,avgWin:0,avgLoss:0,avgHoldDays:0};
339
345
  const gradeColor = bt.grade === '优秀' ? '#4caf50' : bt.grade === '良好' ? '#2196f3' : bt.grade === '一般' ? '#ff9800' : '#f44336';
346
+ const winRateColor = long.winRate >= 55 ? '#4caf50' : long.winRate >= 50 ? '#ff9800' : '#f44336';
347
+ const avgReturnColor = long.avgReturn > 1.5 ? '#4caf50' : long.avgReturn > 0 ? '#ff9800' : '#f44336';
340
348
  const safeName = (bt.name||'').replace(/['"<>&]/g, '');
341
349
  html += `<div style="background:#1a2530;border:1px solid ${gradeColor};margin-bottom:20px;padding:20px;border-radius:10px">
342
350
  <h3 style="color:${gradeColor};margin-bottom:12px;font-size:15px">回测验证: ${safeName} (${bt.code}) — 评级: ${bt.grade}</h3>
343
- <div style="font-size:12px;color:#888;margin-bottom:10px">数据: ${bt.dataRange} | 买入信号${bt.buySignals} | 卖出信号${bt.sellSignals}次</div>
344
- <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px">
351
+ <div style="font-size:12px;color:#888;margin-bottom:10px">数据: ${bt.dataRange || '-'} | 引擎: ${bt.engine || '-'} | 做多交易 ${long.total} 笔 | 平均持仓 ${long.avgHoldDays || 0} 天</div>
352
+ ${long.total > 0 ? `<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:12px">
345
353
  <div style="text-align:center;padding:8px;background:#141e28;border-radius:6px">
346
- <div style="font-size:11px;color:#888">3日胜率</div>
347
- <div style="font-size:18px;font-weight:bold;color:${s3.winRate>=55?'#4caf50':'#ff9800'}">${s3.winRate}%</div>
348
- <div style="font-size:11px;color:#888">均收益 ${s3.avgReturn}%</div>
354
+ <div style="font-size:11px;color:#888">做多胜率</div>
355
+ <div style="font-size:18px;font-weight:bold;color:${winRateColor}">${long.winRate}%</div>
356
+ <div style="font-size:11px;color:#888">${long.wins||0}胜 / ${long.losses||0}负</div>
349
357
  </div>
350
358
  <div style="text-align:center;padding:8px;background:#141e28;border-radius:6px">
351
- <div style="font-size:11px;color:#888">5日胜率</div>
352
- <div style="font-size:18px;font-weight:bold;color:${s5.winRate>=55?'#4caf50':'#ff9800'}">${s5.winRate}%</div>
353
- <div style="font-size:11px;color:#888">均收益 ${s5.avgReturn}%</div>
359
+ <div style="font-size:11px;color:#888">单笔均收益</div>
360
+ <div style="font-size:18px;font-weight:bold;color:${avgReturnColor}">${long.avgReturn}%</div>
361
+ <div style="font-size:11px;color:#888">累计 ${long.totalReturn}%</div>
354
362
  </div>
355
363
  <div style="text-align:center;padding:8px;background:#141e28;border-radius:6px">
356
- <div style="font-size:11px;color:#888">10日胜率</div>
357
- <div style="font-size:18px;font-weight:bold;color:${s10.winRate>=55?'#4caf50':'#ff9800'}">${s10.winRate}%</div>
358
- <div style="font-size:11px;color:#888">均收益 ${s10.avgReturn}%</div>
364
+ <div style="font-size:11px;color:#888">盈亏比</div>
365
+ <div style="font-size:18px;font-weight:bold;color:#fff">${long.avgWin || 0} : ${Math.abs(long.avgLoss || 0)}</div>
366
+ <div style="font-size:11px;color:#888">均盈 : 均亏</div>
359
367
  </div>
360
- </div>
361
- <button onclick="saveToShortcuts('${bt.code}','${safeName}','${s5.winRate}','${bt.grade}')" style="padding:8px 16px;border:1px solid ${gradeColor};border-radius:6px;background:transparent;color:${gradeColor};cursor:pointer;font-size:13px">+ 加入快捷列表</button>
368
+ </div>` : `<div style="padding:12px;text-align:center;color:#888;font-size:13px;background:#141e28;border-radius:6px;margin-bottom:12px">回测周期内无做多入场信号</div>`}
369
+ <button onclick="saveToShortcuts('${bt.code}','${safeName}','${long.winRate}','${bt.grade}')" style="padding:8px 16px;border:1px solid ${gradeColor};border-radius:6px;background:transparent;color:${gradeColor};cursor:pointer;font-size:13px">+ 加入快捷列表</button>
362
370
  <span style="font-size:11px;color:#666;margin-left:10px">加入后可在顶部快速访问</span>
363
371
  </div>`;
364
372
  }
365
373
 
366
- // 信号卡片
367
- html += `<div class="signal-card ${summary.signalClass}">
374
+ // 信号卡片 - 按 totalScore 派生 class(后端不返回 signalClass)
375
+ const ts = summary.totalScore;
376
+ const signalClass = summary.signalClass
377
+ || (ts >= 10 ? 'strong-buy'
378
+ : ts >= 5 ? 'buy'
379
+ : ts >= 1 ? 'weak-buy'
380
+ : ts >= -4 ? 'neutral'
381
+ : ts >= -9 ? 'sell'
382
+ : 'strong-sell');
383
+ const signalIsBullish = ts >= 1;
384
+ const signalIsBearish = ts < -4;
385
+ html += `<div class="signal-card ${signalClass}">
368
386
  <div class="stock-info">
369
387
  <span class="stock-name">${rt.name}</span>
370
388
  <span class="stock-code">${rt.code}</span>
@@ -374,13 +392,13 @@
374
392
  <div class="stock-change ${direction}">
375
393
  ${rt.change > 0 ? '+' : ''}${rt.change.toFixed(2)} (${rt.changePct > 0 ? '+' : ''}${rt.changePct.toFixed(2)}%)
376
394
  </div>
377
- <div class="signal-text ${direction === 'up' ? 'up' : direction === 'down' ? 'down' : ''}">${summary.signal}</div>
378
- <div class="signal-score">综合评分: ${summary.totalScore} | ${summary.positionAdvice || ''}</div>
395
+ <div class="signal-text ${signalIsBullish ? 'up' : signalIsBearish ? 'down' : 'flat'}">${summary.signal}</div>
396
+ <div class="signal-score">综合评分: ${summary.totalScore} 分${rr && rr.positionAdvice ? ' | ' + rr.positionAdvice : ''}</div>
379
397
  <div class="signal-advice">${summary.advice}</div>
380
398
  </div>`;
381
399
 
382
400
  // 操作建议大卡片(最显眼)
383
- const posAdvice = summary.positionAdvice || '';
401
+ const posAdvice = (rr && rr.positionAdvice) || '';
384
402
  const holdAdvice = summary.totalScore >= 10 ? '建议持仓5-10天,趋势跟踪' : summary.totalScore >= 5 ? '建议持仓3-5天,短线波段' : summary.totalScore >= 1 ? '建议持仓1-3天,快进快出' : '不建议开仓';
385
403
  const signalColor = summary.totalScore >= 5 ? '#4caf50' : summary.totalScore >= 1 ? '#ff9800' : summary.totalScore >= -4 ? '#888' : '#f44336';
386
404
 
@@ -406,11 +424,11 @@
406
424
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
407
425
  <div style="background:#122a1a;border:1px solid #2d5a2d;border-radius:10px;padding:16px">
408
426
  <div style="font-size:14px;font-weight:bold;color:#4caf50;margin-bottom:10px">买入操作</div>
409
- ${buyConditions.map(c => `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:13px;color:#ccc"><span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;background:#2d4a2d;color:#8bc34a">${c.priority}</span>${c.condition}</div>`).join('')}
427
+ ${buyConditions.map(c => { const m = (typeof c === 'string' ? c : '').match(/^\[([^\]]+)\]\s*(.*)$/); const p = m ? m[1] : ''; const t = m ? m[2] : (c || ''); return `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:13px;color:#ccc">${p ? `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;background:#2d4a2d;color:#8bc34a">${p}</span>` : ''}${t}</div>`; }).join('')}
410
428
  </div>
411
429
  <div style="background:#2a1a1a;border:1px solid #5a2d2d;border-radius:10px;padding:16px">
412
430
  <div style="font-size:14px;font-weight:bold;color:#f44336;margin-bottom:10px">卖出/止损</div>
413
- ${sellConditions.map(c => `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:13px;color:#ccc"><span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;background:#4a2d2d;color:#ff8a80">${c.priority}</span>${c.condition}</div>`).join('')}
431
+ ${sellConditions.map(c => { const m = (typeof c === 'string' ? c : '').match(/^\[([^\]]+)\]\s*(.*)$/); const p = m ? m[1] : ''; const t = m ? m[2] : (c || ''); return `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.05);font-size:13px;color:#ccc">${p ? `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;margin-right:6px;background:#4a2d2d;color:#ff8a80">${p}</span>` : ''}${t}</div>`; }).join('')}
414
432
  </div>
415
433
  </div>
416
434
  ${rr ? `<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px">
@@ -450,7 +468,15 @@
450
468
 
451
469
  // 支撑压力位
452
470
  html += `<div class="levels">
453
- ${sr.map(s => `<div class="level-item"><div class="level-label">${s.label || s.description || ''}</div><div class="level-value">${s.value || ''}</div>${s.reliability ? `<div style="font-size:11px;color:#888;margin-top:2px">触及${s.touchCount}次 反弹${s.bounceCount}次 可靠性:${s.reliability}</div>` : ''}</div>`).join('')}
471
+ ${(sr || []).map(s => {
472
+ if (typeof s === 'string') {
473
+ const idx = s.lastIndexOf(': ');
474
+ const label = idx > -1 ? s.slice(0, idx) : s;
475
+ const value = idx > -1 ? s.slice(idx + 2) : '';
476
+ return `<div class="level-item"><div class="level-label">${label}</div><div class="level-value">${value}</div></div>`;
477
+ }
478
+ return `<div class="level-item"><div class="level-label">${s.label || s.description || ''}</div><div class="level-value">${s.value || ''}</div>${s.reliability ? `<div style="font-size:11px;color:#888;margin-top:2px">触及${s.touchCount}次 反弹${s.bounceCount}次 可靠性:${s.reliability}</div>` : ''}</div>`;
479
+ }).join('')}
454
480
  </div>`;
455
481
 
456
482
  // 各指标详情
@@ -484,7 +510,16 @@
484
510
  ${scoreText}
485
511
  </div>
486
512
  <div class="section-body">
487
- ${d.signals.map(s => `<div class="signal-item">${s}</div>`).join('')}
513
+ ${d.signals.map(s => {
514
+ const m = (typeof s === 'string' ? s : '').match(/^\[([^\]]+)\]\s*(.*)$/);
515
+ if (!m) return `<div class="signal-item">${s}</div>`;
516
+ const tag = m[1], rest = m[2];
517
+ const bullish = /多|涨|强|底/.test(tag);
518
+ const bearish = /空|跌|弱|顶/.test(tag);
519
+ const bg = bullish ? '#1a3a1a' : bearish ? '#3a1a1a' : '#1a2a3a';
520
+ const fg = bullish ? '#8bc34a' : bearish ? '#ff8a80' : '#7fa9d4';
521
+ return `<div class="signal-item"><span style="display:inline-block;padding:1px 7px;border-radius:4px;font-size:11px;margin-right:6px;background:${bg};color:${fg}">${tag}</span>${rest}</div>`;
522
+ }).join('')}
488
523
  </div>
489
524
  </div>`;
490
525
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kamuira/stock-analyzer",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "preferGlobal": true,
5
5
  "description": "A股/台股综合技术分析工具 - 支持实时分析、回测验证、买卖建议",
6
6
  "main": "server.js",