@kamuira/stock-analyzer 1.0.0
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/analyze.js +1222 -0
- package/backtest.js +423 -0
- package/bin/analyze.js +4 -0
- package/bin/backtest.js +4 -0
- package/bin/server.js +4 -0
- package/dev.js +50 -0
- package/index.html +503 -0
- package/package.json +47 -0
- package/readme.md +166 -0
- package/server.js +1699 -0
- package/stock.js +181 -0
package/index.html
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>A股综合分析工具</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
|
11
|
+
background: #0f1923;
|
|
12
|
+
color: #e0e0e0;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
padding: 20px;
|
|
15
|
+
}
|
|
16
|
+
.container { max-width: 900px; margin: 0 auto; }
|
|
17
|
+
|
|
18
|
+
/* 头部 */
|
|
19
|
+
.header { text-align: center; padding: 30px 0 20px; }
|
|
20
|
+
.header h1 { font-size: 26px; color: #00d4ff; margin-bottom: 8px; }
|
|
21
|
+
.header p { color: #666; font-size: 13px; }
|
|
22
|
+
|
|
23
|
+
/* 搜索框 */
|
|
24
|
+
.search-box {
|
|
25
|
+
display: flex; gap: 10px; justify-content: center;
|
|
26
|
+
margin: 20px 0 30px; flex-wrap: wrap;
|
|
27
|
+
}
|
|
28
|
+
.search-box input {
|
|
29
|
+
padding: 12px 16px; border: 1px solid #2a3a4a; border-radius: 8px;
|
|
30
|
+
background: #1a2a3a; color: #fff; font-size: 15px; width: 280px;
|
|
31
|
+
outline: none; transition: border-color 0.2s;
|
|
32
|
+
}
|
|
33
|
+
.search-box input:focus { border-color: #00d4ff; }
|
|
34
|
+
.search-box button {
|
|
35
|
+
padding: 12px 24px; border: none; border-radius: 8px;
|
|
36
|
+
background: #0066ff; color: #fff; font-size: 15px;
|
|
37
|
+
cursor: pointer; transition: background 0.2s;
|
|
38
|
+
}
|
|
39
|
+
.search-box button:hover { background: #0055dd; }
|
|
40
|
+
.search-box button:disabled { background: #333; cursor: not-allowed; }
|
|
41
|
+
|
|
42
|
+
/* 快捷按钮 */
|
|
43
|
+
.shortcuts {
|
|
44
|
+
display: flex; gap: 8px; justify-content: center;
|
|
45
|
+
margin-bottom: 20px; flex-wrap: wrap;
|
|
46
|
+
}
|
|
47
|
+
.shortcuts button {
|
|
48
|
+
padding: 6px 14px; border: 1px solid #2a3a4a; border-radius: 20px;
|
|
49
|
+
background: transparent; color: #aaa; font-size: 12px;
|
|
50
|
+
cursor: pointer; transition: all 0.2s;
|
|
51
|
+
}
|
|
52
|
+
.shortcuts button:hover { border-color: #00d4ff; color: #00d4ff; }
|
|
53
|
+
|
|
54
|
+
/* 加载状态 */
|
|
55
|
+
.loading { text-align: center; padding: 40px; color: #666; display: none; }
|
|
56
|
+
.loading.active { display: block; }
|
|
57
|
+
.spinner {
|
|
58
|
+
width: 30px; height: 30px; border: 3px solid #333;
|
|
59
|
+
border-top-color: #00d4ff; border-radius: 50%;
|
|
60
|
+
animation: spin 0.8s linear infinite; margin: 0 auto 10px;
|
|
61
|
+
}
|
|
62
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
63
|
+
|
|
64
|
+
/* 错误 */
|
|
65
|
+
.error {
|
|
66
|
+
text-align: center; padding: 20px; color: #ff6b6b;
|
|
67
|
+
background: #2a1a1a; border-radius: 8px; margin: 20px 0; display: none;
|
|
68
|
+
}
|
|
69
|
+
.error.active { display: block; }
|
|
70
|
+
|
|
71
|
+
/* 结果区域 */
|
|
72
|
+
.result { display: none; }
|
|
73
|
+
.result.active { display: block; }
|
|
74
|
+
|
|
75
|
+
/* 信号卡片 */
|
|
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; }
|
|
93
|
+
.signal-card .stock-name { font-size: 22px; font-weight: bold; color: #fff; }
|
|
94
|
+
.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; }
|
|
96
|
+
.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; }
|
|
100
|
+
|
|
101
|
+
.up { color: #ff4444; }
|
|
102
|
+
.down { color: #00cc66; }
|
|
103
|
+
.flat { color: #999; }
|
|
104
|
+
|
|
105
|
+
/* 评分条 */
|
|
106
|
+
.score-bar-container { margin: 20px 0; }
|
|
107
|
+
.score-bar {
|
|
108
|
+
height: 8px; background: #1a2a3a; border-radius: 4px;
|
|
109
|
+
position: relative; overflow: hidden;
|
|
110
|
+
}
|
|
111
|
+
.score-bar-fill {
|
|
112
|
+
height: 100%; border-radius: 4px; transition: width 0.5s ease;
|
|
113
|
+
}
|
|
114
|
+
.score-labels {
|
|
115
|
+
display: flex; justify-content: space-between;
|
|
116
|
+
font-size: 11px; color: #666; margin-top: 4px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* 分析模块 */
|
|
120
|
+
.section { margin-bottom: 16px; }
|
|
121
|
+
.section-header {
|
|
122
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
123
|
+
padding: 12px 16px; background: #1a2a3a; border-radius: 8px 8px 0 0;
|
|
124
|
+
cursor: pointer; user-select: none;
|
|
125
|
+
}
|
|
126
|
+
.section-header:hover { background: #1e3040; }
|
|
127
|
+
.section-title { font-size: 14px; font-weight: bold; }
|
|
128
|
+
.section-score {
|
|
129
|
+
font-size: 12px; padding: 2px 8px; border-radius: 10px;
|
|
130
|
+
}
|
|
131
|
+
.section-score.positive { background: #1a3a1a; color: #4caf50; }
|
|
132
|
+
.section-score.negative { background: #3a1a1a; color: #f44336; }
|
|
133
|
+
.section-score.zero { background: #2a2a2a; color: #999; }
|
|
134
|
+
.section-body {
|
|
135
|
+
padding: 12px 16px; background: #141e28; border-radius: 0 0 8px 8px;
|
|
136
|
+
border: 1px solid #1a2a3a; border-top: none;
|
|
137
|
+
}
|
|
138
|
+
.signal-item {
|
|
139
|
+
padding: 6px 0; font-size: 13px; color: #bbb;
|
|
140
|
+
border-bottom: 1px solid #1a2530;
|
|
141
|
+
}
|
|
142
|
+
.signal-item:last-child { border-bottom: none; }
|
|
143
|
+
.signal-item::before { content: '· '; color: #555; }
|
|
144
|
+
|
|
145
|
+
/* 买卖条件 */
|
|
146
|
+
.conditions { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 20px 0; }
|
|
147
|
+
@media (max-width: 600px) { .conditions { grid-template-columns: 1fr; } }
|
|
148
|
+
.condition-card {
|
|
149
|
+
padding: 16px; border-radius: 10px;
|
|
150
|
+
}
|
|
151
|
+
.condition-card.buy-card { background: #1a2e1a; border: 1px solid #2d4a2d; }
|
|
152
|
+
.condition-card.sell-card { background: #2e1a1a; border: 1px solid #4a2d2d; }
|
|
153
|
+
.condition-card h3 { font-size: 14px; margin-bottom: 10px; }
|
|
154
|
+
.condition-card.buy-card h3 { color: #4caf50; }
|
|
155
|
+
.condition-card.sell-card h3 { color: #f44336; }
|
|
156
|
+
.condition-item {
|
|
157
|
+
padding: 8px 0; font-size: 12px; border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
158
|
+
}
|
|
159
|
+
.condition-item:last-child { border-bottom: none; }
|
|
160
|
+
.condition-priority {
|
|
161
|
+
display: inline-block; padding: 1px 6px; border-radius: 3px;
|
|
162
|
+
font-size: 11px; margin-right: 6px;
|
|
163
|
+
}
|
|
164
|
+
.buy-card .condition-priority { background: #2d4a2d; color: #8bc34a; }
|
|
165
|
+
.sell-card .condition-priority { background: #4a2d2d; color: #ff8a80; }
|
|
166
|
+
|
|
167
|
+
/* 支撑压力 */
|
|
168
|
+
.levels {
|
|
169
|
+
display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
170
|
+
gap: 10px; margin: 20px 0;
|
|
171
|
+
}
|
|
172
|
+
.level-item {
|
|
173
|
+
padding: 10px 14px; background: #1a2a3a; border-radius: 8px;
|
|
174
|
+
font-size: 13px;
|
|
175
|
+
}
|
|
176
|
+
.level-label { color: #888; font-size: 11px; }
|
|
177
|
+
.level-value { font-size: 16px; font-weight: bold; margin-top: 4px; color: #fff; }
|
|
178
|
+
|
|
179
|
+
/* 免责声明 */
|
|
180
|
+
.disclaimer {
|
|
181
|
+
text-align: center; padding: 20px; margin-top: 30px;
|
|
182
|
+
font-size: 11px; color: #555; border-top: 1px solid #1a2a3a;
|
|
183
|
+
}
|
|
184
|
+
</style>
|
|
185
|
+
</head>
|
|
186
|
+
<body>
|
|
187
|
+
<div class="container">
|
|
188
|
+
<div class="header">
|
|
189
|
+
<h1>A股综合分析工具</h1>
|
|
190
|
+
<p>输入股票代码,获取技术面综合分析和买卖建议(支持A股/台股)</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="search-box">
|
|
194
|
+
<input type="text" id="codeInput" placeholder="A股: 名称/代码/拼音 | 台股: 4位代码如2330" autocomplete="off">
|
|
195
|
+
<button id="analyzeBtn" onclick="doAnalyze()">分析</button>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="shortcuts">
|
|
199
|
+
<button onclick="quickAnalyze('sz002049')">紫光国微</button>
|
|
200
|
+
<button onclick="quickAnalyze('sh603893')">瑞芯微</button>
|
|
201
|
+
<button onclick="quickAnalyze('sz300750')">宁德时代</button>
|
|
202
|
+
<button onclick="quickAnalyze('sz300274')">阳光电源</button>
|
|
203
|
+
<button onclick="quickAnalyze('sh603698')">航天工程</button>
|
|
204
|
+
<button onclick="quickAnalyze('sh601138')">工业富联</button>
|
|
205
|
+
<button onclick="quickAnalyze('sh600011')">华能国际</button>
|
|
206
|
+
<button onclick="quickAnalyze('sh601600')">中国铝业</button>
|
|
207
|
+
<button onclick="quickAnalyze('sz002138')">顺络电子</button>
|
|
208
|
+
<button onclick="quickAnalyze('sh603986')">兆易创新</button>
|
|
209
|
+
<button onclick="quickAnalyze('sz002716')">湖南白银</button>
|
|
210
|
+
<button onclick="quickAnalyze('sh603256')">宏和科技</button>
|
|
211
|
+
<button onclick="quickAnalyze('sz001309')">德明利</button>
|
|
212
|
+
<button onclick="quickAnalyze('sh601899')">紫金矿业</button>
|
|
213
|
+
<button onclick="quickAnalyze('sz000426')">兴业银锡</button>
|
|
214
|
+
<button onclick="quickAnalyze('sz002428')">云南锗业</button>
|
|
215
|
+
<button onclick="quickAnalyze('sh600259')">中稀有色</button>
|
|
216
|
+
<button onclick="quickAnalyze('sh600362')">江西铜业</button>
|
|
217
|
+
<button onclick="quickAnalyze('sh600206')">有研新材</button>
|
|
218
|
+
<button onclick="quickAnalyze('sh600111')">北方稀土</button>
|
|
219
|
+
<button onclick="quickAnalyze('sh601318')">中国平安</button>
|
|
220
|
+
<button onclick="quickAnalyze('sh601066')">中信建投</button>
|
|
221
|
+
<button onclick="quickAnalyze('2330')">台积电</button>
|
|
222
|
+
<button onclick="quickAnalyze('2317')">鸿海</button>
|
|
223
|
+
<button onclick="quickAnalyze('2454')">联发科</button>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="shortcuts" id="customShortcuts"></div>
|
|
226
|
+
|
|
227
|
+
<div class="loading" id="loading">
|
|
228
|
+
<div class="spinner"></div>
|
|
229
|
+
<p id="loadingText">正在获取数据并分析中...</p>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div class="error" id="error"></div>
|
|
233
|
+
|
|
234
|
+
<div class="result" id="result"></div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<script>
|
|
238
|
+
const input = document.getElementById('codeInput');
|
|
239
|
+
const btn = document.getElementById('analyzeBtn');
|
|
240
|
+
const loading = document.getElementById('loading');
|
|
241
|
+
const loadingText = document.getElementById('loadingText');
|
|
242
|
+
const errorDiv = document.getElementById('error');
|
|
243
|
+
const resultDiv = document.getElementById('result');
|
|
244
|
+
|
|
245
|
+
// 从 localStorage 加载自定义快捷按钮
|
|
246
|
+
function loadCustomShortcuts() {
|
|
247
|
+
let saved = [];
|
|
248
|
+
try { saved = JSON.parse(localStorage.getItem('customStocks') || '[]'); } catch(e) { saved = []; }
|
|
249
|
+
const container = document.getElementById('customShortcuts');
|
|
250
|
+
container.innerHTML = '';
|
|
251
|
+
for (const s of saved) {
|
|
252
|
+
const b = document.createElement('button');
|
|
253
|
+
b.textContent = s.name;
|
|
254
|
+
b.onclick = () => quickAnalyze(s.code);
|
|
255
|
+
b.title = `${s.code} | 5日胜率${s.winRate}% | 评级:${s.grade}`;
|
|
256
|
+
if (s.grade === '优秀') b.style.borderColor = '#4caf50';
|
|
257
|
+
else if (s.grade === '良好') b.style.borderColor = '#2196f3';
|
|
258
|
+
else if (s.grade === '较差') b.style.borderColor = '#f44336';
|
|
259
|
+
container.appendChild(b);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
loadCustomShortcuts();
|
|
263
|
+
|
|
264
|
+
function saveToShortcuts(code, name, winRate, grade) {
|
|
265
|
+
let saved = [];
|
|
266
|
+
try { saved = JSON.parse(localStorage.getItem('customStocks') || '[]'); } catch(e) { saved = []; }
|
|
267
|
+
if (saved.find(s => s.code === code)) return;
|
|
268
|
+
saved.push({ code, name: name.replace(/['"<>]/g, ''), winRate, grade });
|
|
269
|
+
localStorage.setItem('customStocks', JSON.stringify(saved));
|
|
270
|
+
loadCustomShortcuts();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
input.addEventListener('keypress', e => { if (e.key === 'Enter') doAnalyze(); });
|
|
274
|
+
|
|
275
|
+
function quickAnalyze(code) {
|
|
276
|
+
input.value = code;
|
|
277
|
+
doAnalyze(false); // 所有快捷按钮都显示回测
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function doAnalyze(skipBacktest = false) {
|
|
281
|
+
let code = input.value.trim();
|
|
282
|
+
if (!code) return;
|
|
283
|
+
|
|
284
|
+
btn.disabled = true;
|
|
285
|
+
loading.classList.add('active');
|
|
286
|
+
errorDiv.classList.remove('active');
|
|
287
|
+
resultDiv.classList.remove('active');
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
// Step 1: 先做回测(除非是快捷按钮跳过)
|
|
291
|
+
let backtestData = null;
|
|
292
|
+
if (!skipBacktest) {
|
|
293
|
+
loadingText.textContent = '正在回测历史数据...';
|
|
294
|
+
const btRes = await fetch(`/api/backtest?code=${encodeURIComponent(code)}`);
|
|
295
|
+
backtestData = await btRes.json();
|
|
296
|
+
if (backtestData.error) {
|
|
297
|
+
showError(backtestData.error);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Step 2: 实时分析
|
|
303
|
+
loadingText.textContent = '正在分析实时数据...';
|
|
304
|
+
const res = await fetch(`/api/analyze?code=${encodeURIComponent(code)}`);
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
|
|
307
|
+
if (data.error) {
|
|
308
|
+
showError(data.error);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
renderResult(data, backtestData);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
showError('网络请求失败: ' + e.message);
|
|
315
|
+
} finally {
|
|
316
|
+
btn.disabled = false;
|
|
317
|
+
loading.classList.remove('active');
|
|
318
|
+
loadingText.textContent = '正在获取数据并分析中...';
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function showError(msg) {
|
|
323
|
+
errorDiv.textContent = msg;
|
|
324
|
+
errorDiv.classList.add('active');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderResult(data, backtestData) {
|
|
328
|
+
const { realtime: rt, indicators: ind, supportResistance: sr, buyConditions, sellConditions, summary, riskReward: rr, marketState, marketEnv } = data;
|
|
329
|
+
const direction = rt.change > 0 ? 'up' : rt.change < 0 ? 'down' : 'flat';
|
|
330
|
+
|
|
331
|
+
let html = '';
|
|
332
|
+
|
|
333
|
+
// 回测结果卡片
|
|
334
|
+
if (backtestData && !backtestData.error) {
|
|
335
|
+
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};
|
|
339
|
+
const gradeColor = bt.grade === '优秀' ? '#4caf50' : bt.grade === '良好' ? '#2196f3' : bt.grade === '一般' ? '#ff9800' : '#f44336';
|
|
340
|
+
const safeName = (bt.name||'').replace(/['"<>&]/g, '');
|
|
341
|
+
html += `<div style="background:#1a2530;border:1px solid ${gradeColor};margin-bottom:20px;padding:20px;border-radius:10px">
|
|
342
|
+
<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">
|
|
345
|
+
<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>
|
|
349
|
+
</div>
|
|
350
|
+
<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>
|
|
354
|
+
</div>
|
|
355
|
+
<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>
|
|
359
|
+
</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>
|
|
362
|
+
<span style="font-size:11px;color:#666;margin-left:10px">加入后可在顶部快速访问</span>
|
|
363
|
+
</div>`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 信号卡片
|
|
367
|
+
html += `<div class="signal-card ${summary.signalClass}">
|
|
368
|
+
<div class="stock-info">
|
|
369
|
+
<span class="stock-name">${rt.name}</span>
|
|
370
|
+
<span class="stock-code">${rt.code}</span>
|
|
371
|
+
<span style="font-size:12px;color:#888;margin-left:10px">${marketState} | ${summary.marketEnv || ''}</span>
|
|
372
|
+
</div>
|
|
373
|
+
<div class="stock-price ${direction}">${rt.price.toFixed(2)}</div>
|
|
374
|
+
<div class="stock-change ${direction}">
|
|
375
|
+
${rt.change > 0 ? '+' : ''}${rt.change.toFixed(2)} (${rt.changePct > 0 ? '+' : ''}${rt.changePct.toFixed(2)}%)
|
|
376
|
+
</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>
|
|
379
|
+
<div class="signal-advice">${summary.advice}</div>
|
|
380
|
+
</div>`;
|
|
381
|
+
|
|
382
|
+
// 操作建议大卡片(最显眼)
|
|
383
|
+
const posAdvice = summary.positionAdvice || '';
|
|
384
|
+
const holdAdvice = summary.totalScore >= 10 ? '建议持仓5-10天,趋势跟踪' : summary.totalScore >= 5 ? '建议持仓3-5天,短线波段' : summary.totalScore >= 1 ? '建议持仓1-3天,快进快出' : '不建议开仓';
|
|
385
|
+
const signalColor = summary.totalScore >= 5 ? '#4caf50' : summary.totalScore >= 1 ? '#ff9800' : summary.totalScore >= -4 ? '#888' : '#f44336';
|
|
386
|
+
|
|
387
|
+
html += `<div style="background:linear-gradient(135deg,#1a2e3a,#1a2530);border:2px solid ${signalColor};border-radius:14px;padding:24px;margin-bottom:20px">
|
|
388
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:10px">
|
|
389
|
+
<div style="font-size:20px;font-weight:bold;color:${signalColor}">${summary.signal}</div>
|
|
390
|
+
<div style="font-size:13px;color:#aaa">${marketState} | ${summary.marketEnv || ''}</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:18px">
|
|
393
|
+
<div style="background:#141e28;border-radius:10px;padding:14px;text-align:center">
|
|
394
|
+
<div style="font-size:11px;color:#888;margin-bottom:4px">仓位建议</div>
|
|
395
|
+
<div style="font-size:16px;font-weight:bold;color:#fff">${posAdvice || '观望不动'}</div>
|
|
396
|
+
</div>
|
|
397
|
+
<div style="background:#141e28;border-radius:10px;padding:14px;text-align:center">
|
|
398
|
+
<div style="font-size:11px;color:#888;margin-bottom:4px">持仓周期</div>
|
|
399
|
+
<div style="font-size:16px;font-weight:bold;color:#fff">${holdAdvice}</div>
|
|
400
|
+
</div>
|
|
401
|
+
<div style="background:#141e28;border-radius:10px;padding:14px;text-align:center">
|
|
402
|
+
<div style="font-size:11px;color:#888;margin-bottom:4px">风险收益比</div>
|
|
403
|
+
<div style="font-size:16px;font-weight:bold;color:${rr && rr.riskRewardRatio >= 2 ? '#4caf50' : rr && rr.riskRewardRatio >= 1 ? '#ff9800' : '#f44336'}">1:${rr ? rr.riskRewardRatio : '-'}</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
|
407
|
+
<div style="background:#122a1a;border:1px solid #2d5a2d;border-radius:10px;padding:16px">
|
|
408
|
+
<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('')}
|
|
410
|
+
</div>
|
|
411
|
+
<div style="background:#2a1a1a;border:1px solid #5a2d2d;border-radius:10px;padding:16px">
|
|
412
|
+
<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('')}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
${rr ? `<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px">
|
|
417
|
+
<div style="text-align:center;padding:10px;background:#141e28;border-radius:8px;border-left:3px solid #f44336">
|
|
418
|
+
<div style="font-size:11px;color:#f44336">止损位</div>
|
|
419
|
+
<div style="font-size:18px;font-weight:bold;color:#fff">${rr.stopLoss}</div>
|
|
420
|
+
<div style="font-size:11px;color:#888">亏损 ${rr.riskPct||''}%</div>
|
|
421
|
+
</div>
|
|
422
|
+
<div style="text-align:center;padding:10px;background:#141e28;border-radius:8px;border-left:3px solid #ff9800">
|
|
423
|
+
<div style="font-size:11px;color:#ff9800">止盈1</div>
|
|
424
|
+
<div style="font-size:18px;font-weight:bold;color:#fff">${rr.takeProfit1}</div>
|
|
425
|
+
<div style="font-size:11px;color:#888">收益 ${rr.reward1Pct||''}%</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div style="text-align:center;padding:10px;background:#141e28;border-radius:8px;border-left:3px solid #4caf50">
|
|
428
|
+
<div style="font-size:11px;color:#4caf50">止盈2</div>
|
|
429
|
+
<div style="font-size:18px;font-weight:bold;color:#fff">${rr.takeProfit2}</div>
|
|
430
|
+
<div style="font-size:11px;color:#888">收益 ${rr.reward2Pct||''}%</div>
|
|
431
|
+
</div>
|
|
432
|
+
</div>` : ''}
|
|
433
|
+
</div>`;
|
|
434
|
+
|
|
435
|
+
// 评分条
|
|
436
|
+
const barColor = summary.totalScore >= 5 ? '#4caf50' : summary.totalScore >= 0 ? '#ff9800' : '#f44336';
|
|
437
|
+
html += `<div class="score-bar-container">
|
|
438
|
+
<div class="score-bar">
|
|
439
|
+
<div class="score-bar-fill" style="width:${summary.normalizedScore}%; background:${barColor}"></div>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="score-labels"><span>强烈卖出</span><span>观望</span><span>强烈买入</span></div>
|
|
442
|
+
</div>`;
|
|
443
|
+
|
|
444
|
+
// 大盘环境
|
|
445
|
+
if (marketEnv && marketEnv.signals) {
|
|
446
|
+
html += `<div style="background:#1a2530;border:1px solid #2a3a4a;border-radius:8px;padding:12px 16px;margin-bottom:16px;font-size:13px;color:#aaa">
|
|
447
|
+
<span style="color:#888">大盘环境:</span> ${marketEnv.signals.join(' | ')}
|
|
448
|
+
</div>`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 支撑压力位
|
|
452
|
+
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('')}
|
|
454
|
+
</div>`;
|
|
455
|
+
|
|
456
|
+
// 各指标详情
|
|
457
|
+
const sections = [
|
|
458
|
+
{ key: 'ma', title: '均线系统' },
|
|
459
|
+
{ key: 'macd', title: 'MACD' },
|
|
460
|
+
{ key: 'rsi', title: 'RSI' },
|
|
461
|
+
{ key: 'kdj', title: 'KDJ' },
|
|
462
|
+
{ key: 'boll', title: '布林带' },
|
|
463
|
+
{ key: 'volume', title: '量价关系' },
|
|
464
|
+
{ key: 'trend', title: '趋势分析' },
|
|
465
|
+
{ key: 'adx', title: 'ADX趋势强度' },
|
|
466
|
+
{ key: 'atr', title: 'ATR波动率', noScore: true },
|
|
467
|
+
{ key: 'divergence', title: '背离检测' },
|
|
468
|
+
{ key: 'volumeDivergence', title: '量价背离' },
|
|
469
|
+
{ key: 'patterns', title: '形态识别' },
|
|
470
|
+
{ key: 'momentum', title: '动量分析' },
|
|
471
|
+
{ key: 'gaps', title: '缺口分析' },
|
|
472
|
+
{ key: 'fibonacci', title: '斐波那契', noScore: true },
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
for (const sec of sections) {
|
|
476
|
+
const d = ind[sec.key];
|
|
477
|
+
if (!d) continue;
|
|
478
|
+
const score = d.score || 0;
|
|
479
|
+
const scoreClass = score > 0 ? 'positive' : score < 0 ? 'negative' : 'zero';
|
|
480
|
+
const scoreText = sec.noScore ? '' : `<span class="section-score ${scoreClass}">${score > 0 ? '+' : ''}${score}分</span>`;
|
|
481
|
+
html += `<div class="section">
|
|
482
|
+
<div class="section-header" onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'">
|
|
483
|
+
<span class="section-title">${sec.title}</span>
|
|
484
|
+
${scoreText}
|
|
485
|
+
</div>
|
|
486
|
+
<div class="section-body">
|
|
487
|
+
${d.signals.map(s => `<div class="signal-item">${s}</div>`).join('')}
|
|
488
|
+
</div>
|
|
489
|
+
</div>`;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 免责声明
|
|
493
|
+
html += `<div class="disclaimer">
|
|
494
|
+
免责声明:以上分析仅基于技术面指标,不构成投资建议。投资有风险,入市需谨慎。<br>
|
|
495
|
+
数据来源:新浪财经(实时) / 腾讯财经(历史K线) | 分析时间: ${rt.time}
|
|
496
|
+
</div>`;
|
|
497
|
+
|
|
498
|
+
resultDiv.innerHTML = html;
|
|
499
|
+
resultDiv.classList.add('active');
|
|
500
|
+
}
|
|
501
|
+
</script>
|
|
502
|
+
</body>
|
|
503
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kamuira/stock-analyzer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A股/台股综合技术分析工具 - 支持实时分析、回测验证、买卖建议",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"stock-analyze": "./bin/analyze.js",
|
|
8
|
+
"stock-server": "./bin/server.js",
|
|
9
|
+
"stock-backtest": "./bin/backtest.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node server.js",
|
|
13
|
+
"dev": "node dev.js",
|
|
14
|
+
"analyze": "node analyze.js",
|
|
15
|
+
"backtest": "node backtest.js"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"stock",
|
|
19
|
+
"technical-analysis",
|
|
20
|
+
"A-share",
|
|
21
|
+
"Taiwan-stock",
|
|
22
|
+
"MACD",
|
|
23
|
+
"RSI",
|
|
24
|
+
"KDJ",
|
|
25
|
+
"ADX",
|
|
26
|
+
"backtest"
|
|
27
|
+
],
|
|
28
|
+
"author": "guiwzh",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": ""
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"server.js",
|
|
37
|
+
"analyze.js",
|
|
38
|
+
"backtest.js",
|
|
39
|
+
"dev.js",
|
|
40
|
+
"stock.js",
|
|
41
|
+
"index.html",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=14.0.0"
|
|
46
|
+
}
|
|
47
|
+
}
|