jasonfan commited on
Commit
2d3db34
·
verified ·
1 Parent(s): f1df36c

Upload folder using huggingface_hub

Browse files
Files changed (8) hide show
  1. README.md +58 -0
  2. auto_trader.py +489 -0
  3. daily_reports/report_2026-03-20.json +139 -0
  4. monitor.py +172 -0
  5. my_portfolio.json +113 -0
  6. portfolio.py +271 -0
  7. stock_screener.py +385 -0
  8. trade_log.json +141 -0
README.md ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 美股模拟投资系统
2
+
3
+ 基于 [daily_stock_analysis](https://github.com/ZhuLinsen/daily_stock_analysis) 的技术分析体系。
4
+
5
+ ## 文件说明
6
+
7
+ | 文件 | 说明 |
8
+ |------|------|
9
+ | `portfolio.py` | 基础操作:买入/卖出/查看持仓 |
10
+ | `monitor.py` | 实时行情监控(终端刷新) |
11
+ | `stock_screener.py` | 技术面选股器(扫描69只美股评分排序) |
12
+ | `auto_trader.py` | 自动交易Bot(每日定时分析+虚拟买卖) |
13
+ | `my_portfolio.json` | 当前持仓数据 |
14
+ | `trade_log.json` | 历史交易日志 |
15
+ | `daily_reports/` | 每日分析报告 |
16
+
17
+ ## 日常使用
18
+
19
+ ```bash
20
+ cd ~/stock-sim
21
+
22
+ # 手动操作
23
+ python3 portfolio.py show # 看持仓
24
+ python3 portfolio.py buy AAPL 5000 # 买入
25
+ python3 portfolio.py sell TSLA 10 # 卖出
26
+
27
+ # 选股
28
+ python3 stock_screener.py # 技术面筛选Top20
29
+
30
+ # 自动交易Bot
31
+ python3 auto_trader.py # 立即执行一次
32
+ python3 auto_trader.py --daemon # 后台定时(每日21:35北京时间)
33
+ python3 auto_trader.py --history # 查看历史记录
34
+
35
+ # AI深度分析(需要LLMBox)
36
+ cd ~/daily_stock_analysis
37
+ python3 main.py --stocks AAPL,PLTR --force-run --no-notify
38
+ ```
39
+
40
+ ## 评分体系(100分制)
41
+
42
+ | 维度 | 满分 | 说明 |
43
+ |------|------|------|
44
+ | 趋势 | 30 | MA5>MA10>MA20 多头排列 |
45
+ | 乖离率 | 20 | 接近MA5不追高 |
46
+ | 量能 | 15 | 缩量回调最优 |
47
+ | MACD | 15 | 金叉/多头 |
48
+ | RSI | 10 | 超卖反弹/强势 |
49
+ | 支撑 | 10 | 均线支撑有效 |
50
+
51
+ ## Bot 交易规则
52
+
53
+ - 评分>=60 且"买入"信号 → 自动买入
54
+ - 评分<30 或"卖出"信号 → 自动卖出
55
+ - 空头排列且<45 → 减半仓
56
+ - 单只最多占总资产20%
57
+ - 最多持有8只
58
+ - 每日最多用30%现金买入
auto_trader.py ADDED
@@ -0,0 +1,489 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 美股自动交易Bot — 每日定时运行
4
+ 1. 技术面筛选69只美股
5
+ 2. AI深度分析Top候选
6
+ 3. 自动虚拟买卖(用portfolio.py)
7
+ 4. 记录每日操作和收益到日志
8
+
9
+ 用法:
10
+ python3 auto_trader.py # 立即执行一次
11
+ python3 auto_trader.py --daemon # 后台定时运行(每个交易日21:35北京时间)
12
+ python3 auto_trader.py --backtest # 查看历史操作记录
13
+ """
14
+
15
+ import json
16
+ import os
17
+ import sys
18
+ import subprocess
19
+ import time
20
+ import warnings
21
+ from datetime import datetime, timedelta
22
+ from pathlib import Path
23
+
24
+ warnings.filterwarnings("ignore")
25
+
26
+ import numpy as np
27
+ import pandas as pd
28
+ import yfinance as yf
29
+ from concurrent.futures import ThreadPoolExecutor, as_completed
30
+
31
+ # ── 路径 ──
32
+ BASE_DIR = Path(__file__).parent
33
+ PORTFOLIO_FILE = BASE_DIR / "my_portfolio.json"
34
+ TRADE_LOG = BASE_DIR / "trade_log.json"
35
+ DAILY_REPORT_DIR = BASE_DIR / "daily_reports"
36
+ DAILY_REPORT_DIR.mkdir(exist_ok=True)
37
+
38
+ # ── 交易参数 ──
39
+ MAX_POSITION_PCT = 0.20 # 单只最多占总资产20%
40
+ MIN_BUY_SCORE = 60 # 最低买入评分
41
+ SELL_SCORE_THRESHOLD = 30 # 低于此分卖出
42
+ MAX_HOLDINGS = 8 # 最多持有8只
43
+ DAILY_BUY_BUDGET_PCT = 0.30 # 每天最多用30%现金买入
44
+
45
+ # ── 候选池 ──
46
+ US_STOCKS = [
47
+ "AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA",
48
+ "AMD", "AVGO", "QCOM", "INTC", "MU", "MRVL", "ARM", "SMCI", "TSM",
49
+ "CRM", "ORCL", "ADBE", "NOW", "SNOW", "PLTR", "NET", "DDOG", "CRWD",
50
+ "COST", "WMT", "TGT", "NKE", "SBUX", "MCD", "PEP", "KO",
51
+ "JPM", "GS", "MS", "BAC", "V", "MA", "AXP",
52
+ "LLY", "UNH", "JNJ", "PFE", "ABBV", "MRK", "BMY",
53
+ "XOM", "CVX", "SLB", "OXY",
54
+ "BA", "CAT", "DE", "GE", "LMT", "COIN", "UBER", "ABNB",
55
+ "SHOP", "PYPL", "ROKU", "SNAP", "PINS", "RBLX",
56
+ ]
57
+
58
+
59
+ # ══════════════════════════════════════════════════════════
60
+ # 技术分析(与 stock_screener.py 相同的评分体系)
61
+ # ══════════════════════════════════════════════════════════
62
+
63
+ def calc_macd(close, fast=12, slow=26, signal=9):
64
+ ema_fast = close.ewm(span=fast, adjust=False).mean()
65
+ ema_slow = close.ewm(span=slow, adjust=False).mean()
66
+ dif = ema_fast - ema_slow
67
+ dea = dif.ewm(span=signal, adjust=False).mean()
68
+ return dif, dea
69
+
70
+ def calc_rsi(close, period=12):
71
+ delta = close.diff()
72
+ gain = delta.where(delta > 0, 0).rolling(period).mean()
73
+ loss = (-delta.where(delta < 0, 0)).rolling(period).mean()
74
+ rs = gain / loss
75
+ return (100 - 100 / (1 + rs)).fillna(50)
76
+
77
+ def analyze_stock(symbol):
78
+ """技术分析评分,返回 dict 或 None"""
79
+ try:
80
+ df = yf.Ticker(symbol).history(period="6mo", auto_adjust=True)
81
+ if df is None or len(df) < 60:
82
+ return None
83
+
84
+ close = df["Close"]
85
+ volume = df["Volume"]
86
+ last = len(df) - 1
87
+ price = float(close.iloc[last])
88
+
89
+ ma5 = float(close.rolling(5).mean().iloc[last])
90
+ ma10 = float(close.rolling(10).mean().iloc[last])
91
+ ma20 = float(close.rolling(20).mean().iloc[last])
92
+
93
+ # 趋势(30分)
94
+ if ma5 > ma10 > ma20:
95
+ prev_idx = max(0, last - 5)
96
+ prev_s = (float(close.rolling(5).mean().iloc[prev_idx]) - float(close.rolling(20).mean().iloc[prev_idx])) / float(close.rolling(20).mean().iloc[prev_idx]) * 100
97
+ curr_s = (ma5 - ma20) / ma20 * 100
98
+ if curr_s > prev_s and curr_s > 5:
99
+ trend_score, trend = 30, "强势多头"
100
+ else:
101
+ trend_score, trend = 26, "多头排列"
102
+ elif ma5 > ma10:
103
+ trend_score, trend = 18, "弱势多头"
104
+ elif ma5 < ma10 < ma20:
105
+ trend_score, trend = 4, "空头排列"
106
+ else:
107
+ trend_score, trend = 12, "盘整"
108
+
109
+ # 乖离率(20分)
110
+ bias = (price - ma5) / ma5 * 100 if ma5 > 0 else 0
111
+ if bias < 0 and bias > -3:
112
+ bias_score = 20
113
+ elif bias < 0 and bias > -5:
114
+ bias_score = 16
115
+ elif bias < 0:
116
+ bias_score = 8
117
+ elif bias < 2:
118
+ bias_score = 18
119
+ elif bias < 5:
120
+ bias_score = 14
121
+ else:
122
+ bias_score = 4
123
+
124
+ # 量能(15分)
125
+ vol_avg = float(volume.iloc[-6:-1].mean())
126
+ vol_ratio = float(volume.iloc[last]) / vol_avg if vol_avg > 0 else 1
127
+ prev_close = float(close.iloc[last - 1])
128
+ chg = (price - prev_close) / prev_close * 100
129
+ if vol_ratio >= 1.5:
130
+ vol_score = 12 if chg > 0 else 0
131
+ elif vol_ratio <= 0.7:
132
+ vol_score = 15 if chg <= 0 else 6
133
+ else:
134
+ vol_score = 10
135
+
136
+ # MACD(15分)
137
+ dif, dea = calc_macd(close)
138
+ macd_dif, macd_dea = float(dif.iloc[last]), float(dea.iloc[last])
139
+ prev_diff = float(dif.iloc[last-1]) - float(dea.iloc[last-1])
140
+ curr_diff = macd_dif - macd_dea
141
+ golden = prev_diff <= 0 and curr_diff > 0
142
+ if golden and macd_dif > 0:
143
+ macd_score, macd_label = 15, "零轴上金叉"
144
+ elif golden:
145
+ macd_score, macd_label = 12, "金叉"
146
+ elif prev_diff >= 0 and curr_diff < 0:
147
+ macd_score, macd_label = 0, "死叉"
148
+ elif macd_dif > 0 and macd_dea > 0:
149
+ macd_score, macd_label = 8, "多头"
150
+ elif macd_dif < 0 and macd_dea < 0:
151
+ macd_score, macd_label = 2, "空头"
152
+ else:
153
+ macd_score, macd_label = 5, "中性"
154
+
155
+ # RSI(10分)
156
+ rsi = float(calc_rsi(close, 12).iloc[last])
157
+ if rsi > 70: rsi_score = 0
158
+ elif rsi > 60: rsi_score = 8
159
+ elif rsi >= 40: rsi_score = 5
160
+ elif rsi >= 30: rsi_score = 3
161
+ else: rsi_score = 10
162
+
163
+ # 支撑(10分)
164
+ sup_score = 0
165
+ if abs(price - ma5) / ma5 <= 0.02 and price >= ma5: sup_score += 5
166
+ if abs(price - ma10) / ma10 <= 0.02 and price >= ma10: sup_score += 5
167
+
168
+ total = trend_score + bias_score + vol_score + macd_score + rsi_score + sup_score
169
+
170
+ # 信号
171
+ if total >= 75 and trend in ("强势多头", "多头排列"):
172
+ signal = "强烈买入"
173
+ elif total >= 60 and trend in ("强势多头", "多头排列", "弱势多头"):
174
+ signal = "买入"
175
+ elif total >= 45:
176
+ signal = "持有"
177
+ elif total >= 30:
178
+ signal = "观望"
179
+ elif trend in ("空头排列",):
180
+ signal = "卖出"
181
+ else:
182
+ signal = "观望"
183
+
184
+ return {
185
+ "symbol": symbol, "price": price, "score": total, "signal": signal,
186
+ "trend": trend, "bias": bias, "macd": macd_label, "rsi": rsi,
187
+ "ma5": ma5, "ma10": ma10, "ma20": ma20,
188
+ }
189
+ except Exception:
190
+ return None
191
+
192
+
193
+ # ══════════════════════════════════════════════════════════
194
+ # 交易引擎
195
+ # ══════════════════════════════════════════════════════════
196
+
197
+ def load_portfolio():
198
+ if PORTFOLIO_FILE.exists():
199
+ with open(PORTFOLIO_FILE) as f:
200
+ pf = json.load(f)
201
+ if "transactions" not in pf:
202
+ pf["transactions"] = []
203
+ return pf
204
+ return {"cash": 100000, "holdings": {}, "transactions": []}
205
+
206
+ def save_portfolio(pf):
207
+ with open(PORTFOLIO_FILE, "w") as f:
208
+ json.dump(pf, f, indent=2, ensure_ascii=False)
209
+
210
+ def load_trade_log():
211
+ if TRADE_LOG.exists():
212
+ with open(TRADE_LOG) as f:
213
+ return json.load(f)
214
+ return []
215
+
216
+ def save_trade_log(log):
217
+ with open(TRADE_LOG, "w") as f:
218
+ json.dump(log, f, indent=2, ensure_ascii=False)
219
+
220
+ def execute_buy(pf, symbol, price, amount):
221
+ """虚拟买入"""
222
+ shares = int(amount / price)
223
+ if shares <= 0:
224
+ return None
225
+ cost = shares * price
226
+ if cost > pf["cash"]:
227
+ return None
228
+ pf["cash"] -= cost
229
+ if symbol in pf["holdings"]:
230
+ old = pf["holdings"][symbol]
231
+ total_shares = old["shares"] + shares
232
+ old["avg_cost"] = (old["avg_cost"] * old["shares"] + cost) / total_shares
233
+ old["shares"] = total_shares
234
+ else:
235
+ pf["holdings"][symbol] = {"shares": shares, "avg_cost": price}
236
+ pf["transactions"].append({
237
+ "type": "buy", "symbol": symbol, "shares": shares,
238
+ "price": price, "time": datetime.now().isoformat()
239
+ })
240
+ return {"symbol": symbol, "shares": shares, "price": price, "cost": cost}
241
+
242
+ def execute_sell(pf, symbol, price, shares=None):
243
+ """虚拟卖出"""
244
+ if symbol not in pf["holdings"]:
245
+ return None
246
+ h = pf["holdings"][symbol]
247
+ sell_shares = shares or h["shares"]
248
+ sell_shares = min(sell_shares, h["shares"])
249
+ revenue = sell_shares * price
250
+ pf["cash"] += revenue
251
+ pnl = (price - h["avg_cost"]) * sell_shares
252
+ h["shares"] -= sell_shares
253
+ if h["shares"] <= 0:
254
+ del pf["holdings"][symbol]
255
+ pf["transactions"].append({
256
+ "type": "sell", "symbol": symbol, "shares": sell_shares,
257
+ "price": price, "pnl": round(pnl, 2), "time": datetime.now().isoformat()
258
+ })
259
+ return {"symbol": symbol, "shares": sell_shares, "price": price, "revenue": revenue, "pnl": pnl}
260
+
261
+ def is_us_trading_day():
262
+ """检查今天是否是美股交易日"""
263
+ try:
264
+ from zoneinfo import ZoneInfo
265
+ except ImportError:
266
+ from backports.zoneinfo import ZoneInfo
267
+ et = datetime.now(ZoneInfo("America/New_York"))
268
+ return et.weekday() < 5 # 简化:不考虑节假日
269
+
270
+
271
+ # ══════════════════════════════════════════════════════════
272
+ # 每日策略执行
273
+ # ══════════════════════════════════════════════════════════
274
+
275
+ def run_daily_strategy():
276
+ """每日策略核心"""
277
+ today = datetime.now().strftime("%Y-%m-%d")
278
+ print(f"\n{'='*60}")
279
+ print(f" 🤖 自动交易Bot — {today}")
280
+ print(f"{'='*60}")
281
+
282
+ # 1. 扫描全部股票
283
+ print(f"\n 📡 正在扫描 {len(US_STOCKS)} 只美股...")
284
+ results = []
285
+ with ThreadPoolExecutor(max_workers=8) as pool:
286
+ futures = {pool.submit(analyze_stock, s): s for s in US_STOCKS}
287
+ for f in as_completed(futures):
288
+ r = f.result()
289
+ if r:
290
+ results.append(r)
291
+ results.sort(key=lambda x: x["score"], reverse=True)
292
+ print(f" ✅ 扫描完成,{len(results)} 只有效")
293
+
294
+ # 2. 加载持仓
295
+ pf = load_portfolio()
296
+ total_assets = pf["cash"]
297
+ for sym, info in pf["holdings"].items():
298
+ # 用最新价更新
299
+ match = next((r for r in results if r["symbol"] == sym), None)
300
+ if match:
301
+ total_assets += match["price"] * info["shares"]
302
+ else:
303
+ total_assets += info["avg_cost"] * info["shares"]
304
+
305
+ print(f"\n 💼 当前资产: ${total_assets:,.0f} 现金: ${pf['cash']:,.0f} 持仓: {len(pf['holdings'])}只")
306
+
307
+ trades_today = []
308
+
309
+ # 3. 卖出逻辑:持仓中评分低的
310
+ print(f"\n 📉 检查卖出信号...")
311
+ for sym in list(pf["holdings"].keys()):
312
+ match = next((r for r in results if r["symbol"] == sym), None)
313
+ if match:
314
+ if match["score"] < SELL_SCORE_THRESHOLD or match["signal"] == "卖出":
315
+ result = execute_sell(pf, sym, match["price"])
316
+ if result:
317
+ print(f" 🔴 卖出 {sym} x{result['shares']}股 @ ${result['price']:.2f} 盈亏: ${result['pnl']:+,.2f}")
318
+ trades_today.append({"action": "SELL", **result})
319
+ elif match["score"] < 45 and match["trend"] in ("空头排列",):
320
+ # 空头排列减半仓
321
+ half = pf["holdings"][sym]["shares"] // 2
322
+ if half > 0:
323
+ result = execute_sell(pf, sym, match["price"], half)
324
+ if result:
325
+ print(f" 🟡 减仓 {sym} x{result['shares']}股 @ ${result['price']:.2f} 盈亏: ${result['pnl']:+,.2f}")
326
+ trades_today.append({"action": "REDUCE", **result})
327
+ else:
328
+ print(f" ⚠️ {sym} 数据获取失败,保持持仓")
329
+
330
+ # 4. 买入逻辑:选评分最高的买入信号
331
+ print(f"\n 📈 检查买入信号...")
332
+ buy_budget = pf["cash"] * DAILY_BUY_BUDGET_PCT
333
+ buy_candidates = [r for r in results
334
+ if r["score"] >= MIN_BUY_SCORE
335
+ and r["signal"] in ("买入", "强烈买入")
336
+ and r["symbol"] not in pf["holdings"]]
337
+
338
+ bought_count = 0
339
+ for r in buy_candidates:
340
+ if len(pf["holdings"]) >= MAX_HOLDINGS:
341
+ print(f" ⏸️ 已持有{MAX_HOLDINGS}只,不再买入")
342
+ break
343
+ if buy_budget < 1000:
344
+ print(f" ⏸️ 今日买入预算用完")
345
+ break
346
+
347
+ # 单只限额
348
+ max_amount = total_assets * MAX_POSITION_PCT
349
+ amount = min(buy_budget, max_amount, pf["cash"])
350
+ if amount < 500:
351
+ break
352
+
353
+ result = execute_buy(pf, r["symbol"], r["price"], amount)
354
+ if result:
355
+ print(f" 🟢 买入 {r['symbol']} x{result['shares']}股 @ ${result['price']:.2f} = ${result['cost']:,.0f} (评分{r['score']})")
356
+ buy_budget -= result["cost"]
357
+ trades_today.append({"action": "BUY", "score": r["score"], **result})
358
+ bought_count += 1
359
+
360
+ if not trades_today:
361
+ print(f" 🔵 今日无操作")
362
+
363
+ # 5. 保存
364
+ save_portfolio(pf)
365
+
366
+ # 6. 计算总收益
367
+ total_now = pf["cash"]
368
+ holding_details = []
369
+ for sym, info in pf["holdings"].items():
370
+ match = next((r for r in results if r["symbol"] == sym), None)
371
+ cur_price = match["price"] if match else info["avg_cost"]
372
+ mkt = cur_price * info["shares"]
373
+ pnl = (cur_price - info["avg_cost"]) * info["shares"]
374
+ pct = (cur_price / info["avg_cost"] - 1) * 100
375
+ total_now += mkt
376
+ score = match["score"] if match else 0
377
+ holding_details.append({
378
+ "symbol": sym, "shares": info["shares"], "avg_cost": info["avg_cost"],
379
+ "price": cur_price, "mkt": mkt, "pnl": pnl, "pct": pct, "score": score
380
+ })
381
+
382
+ total_pnl = total_now - 100000
383
+ total_pct = total_pnl / 100000 * 100
384
+
385
+ # 7. 显示持仓
386
+ print(f"\n {'─'*60}")
387
+ print(f" 📊 持仓明细:")
388
+ for h in sorted(holding_details, key=lambda x: x["score"], reverse=True):
389
+ sign = "+" if h["pnl"] >= 0 else ""
390
+ print(f" {h['symbol']:<6} {h['shares']:>5}股 成本${h['avg_cost']:.2f} 现价${h['price']:.2f} {sign}${h['pnl']:,.0f}({sign}{h['pct']:.1f}%) 评分{h['score']}")
391
+
392
+ sign = "+" if total_pnl >= 0 else ""
393
+ print(f"\n 💰 现金: ${pf['cash']:,.2f}")
394
+ print(f" 💼 总资产: ${total_now:,.2f} 总盈亏: {sign}${total_pnl:,.2f} ({sign}{total_pct:.2f}%)")
395
+
396
+ # 8. 保存日报
397
+ daily_report = {
398
+ "date": today,
399
+ "total_assets": round(total_now, 2),
400
+ "cash": round(pf["cash"], 2),
401
+ "total_pnl": round(total_pnl, 2),
402
+ "total_pct": round(total_pct, 2),
403
+ "holdings": len(pf["holdings"]),
404
+ "trades": trades_today,
405
+ "top5_scores": [{"symbol": r["symbol"], "score": r["score"], "signal": r["signal"]} for r in results[:5]],
406
+ "portfolio": holding_details,
407
+ }
408
+
409
+ report_file = DAILY_REPORT_DIR / f"report_{today}.json"
410
+ with open(report_file, "w") as f:
411
+ json.dump(daily_report, f, indent=2, ensure_ascii=False)
412
+
413
+ # 9. 追加交易日志
414
+ trade_log = load_trade_log()
415
+ trade_log.append(daily_report)
416
+ save_trade_log(trade_log)
417
+
418
+ print(f"\n 📝 日报已保存: {report_file}")
419
+ print(f"{'='*60}\n")
420
+
421
+ return daily_report
422
+
423
+
424
+ def show_history():
425
+ """显示历史操作记录"""
426
+ log = load_trade_log()
427
+ if not log:
428
+ print(" 暂无交易记录")
429
+ return
430
+
431
+ print(f"\n 📊 交易历史 ({len(log)}个交易日)")
432
+ print(f" {'─'*60}")
433
+ print(f" {'日期':<12} {'总资产':>12} {'盈亏':>10} {'涨幅':>8} {'持仓':>4} {'交易':>4}")
434
+ print(f" {'─'*60}")
435
+
436
+ for day in log:
437
+ sign = "+" if day["total_pnl"] >= 0 else ""
438
+ n_trades = len(day.get("trades", []))
439
+ print(f" {day['date']:<12} ${day['total_assets']:>10,.2f} {sign}${day['total_pnl']:>8,.0f} {sign}{day['total_pct']:>6.2f}% {day['holdings']:>4} {n_trades:>4}")
440
+
441
+ latest = log[-1]
442
+ print(f" {'─'*60}")
443
+ sign = "+" if latest["total_pnl"] >= 0 else ""
444
+ print(f" 最新: ${latest['total_assets']:,.2f} {sign}${latest['total_pnl']:,.2f} ({sign}{latest['total_pct']:.2f}%)")
445
+
446
+
447
+ def daemon_mode():
448
+ """后台定时运行模式"""
449
+ try:
450
+ from zoneinfo import ZoneInfo
451
+ except ImportError:
452
+ from backports.zoneinfo import ZoneInfo
453
+
454
+ print(" 🤖 自动交易Bot已启动(后台模式)")
455
+ print(" ⏰ 每个交易日 21:35 (北京时间) 自动执行")
456
+ print(" 按 Ctrl+C 停止\n")
457
+
458
+ while True:
459
+ try:
460
+ bj = datetime.now(ZoneInfo("Asia/Shanghai"))
461
+
462
+ # 检查是否到了执行时间 (21:35 北京时间 = 开盘后5分钟)
463
+ if bj.hour == 21 and bj.minute == 35:
464
+ if is_us_trading_day():
465
+ print(f"\n ⏰ {bj.strftime('%Y-%m-%d %H:%M')} 触发交易...")
466
+ run_daily_strategy()
467
+ else:
468
+ print(f" 📅 {bj.strftime('%Y-%m-%d')} 非交易日,跳过")
469
+ # 等到下一分钟避免重复
470
+ time.sleep(60)
471
+ else:
472
+ # 每30秒检查一次
473
+ time.sleep(30)
474
+
475
+ except KeyboardInterrupt:
476
+ print("\n\n 👋 Bot已停止\n")
477
+ break
478
+ except Exception as e:
479
+ print(f"\n ⚠️ 出错: {e},60秒后重试...")
480
+ time.sleep(60)
481
+
482
+
483
+ if __name__ == "__main__":
484
+ if "--daemon" in sys.argv:
485
+ daemon_mode()
486
+ elif "--backtest" in sys.argv or "--history" in sys.argv:
487
+ show_history()
488
+ else:
489
+ run_daily_strategy()
daily_reports/report_2026-03-20.json ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "date": "2026-03-20",
3
+ "total_assets": 100000.0,
4
+ "cash": 28385.48,
5
+ "total_pnl": -0.0,
6
+ "total_pct": -0.0,
7
+ "holdings": 6,
8
+ "trades": [
9
+ {
10
+ "action": "REDUCE",
11
+ "symbol": "AAPL",
12
+ "shares": 40,
13
+ "price": 247.99000549316406,
14
+ "revenue": 9919.600219726562,
15
+ "pnl": 0.00021972656213620212
16
+ },
17
+ {
18
+ "action": "REDUCE",
19
+ "symbol": "NVDA",
20
+ "shares": 57,
21
+ "price": 172.6999969482422,
22
+ "revenue": 9843.899826049805,
23
+ "pnl": -0.00017395019466448502
24
+ },
25
+ {
26
+ "action": "REDUCE",
27
+ "symbol": "MSFT",
28
+ "shares": 26,
29
+ "price": 381.8699951171875,
30
+ "revenue": 9928.619873046875,
31
+ "pnl": -0.0001269531251182343
32
+ },
33
+ {
34
+ "action": "REDUCE",
35
+ "symbol": "TSLA",
36
+ "shares": 27,
37
+ "price": 367.9599914550781,
38
+ "revenue": 9934.91976928711,
39
+ "pnl": -0.00023071289007248197
40
+ },
41
+ {
42
+ "action": "BUY",
43
+ "score": 71,
44
+ "symbol": "MS",
45
+ "shares": 74,
46
+ "price": 161.47000122070312,
47
+ "cost": 11948.780090332031
48
+ }
49
+ ],
50
+ "top5_scores": [
51
+ {
52
+ "symbol": "MS",
53
+ "score": 71,
54
+ "signal": "买入"
55
+ },
56
+ {
57
+ "symbol": "PLTR",
58
+ "score": 69,
59
+ "signal": "买入"
60
+ },
61
+ {
62
+ "symbol": "NET",
63
+ "score": 69,
64
+ "signal": "买入"
65
+ },
66
+ {
67
+ "symbol": "PFE",
68
+ "score": 69,
69
+ "signal": "买入"
70
+ },
71
+ {
72
+ "symbol": "CVX",
73
+ "score": 69,
74
+ "signal": "买入"
75
+ }
76
+ ],
77
+ "portfolio": [
78
+ {
79
+ "symbol": "AAPL",
80
+ "shares": 40,
81
+ "avg_cost": 247.99,
82
+ "price": 247.99000549316406,
83
+ "mkt": 9919.600219726562,
84
+ "pnl": 0.00021972656213620212,
85
+ "pct": 2.215074812461637e-06,
86
+ "score": 36
87
+ },
88
+ {
89
+ "symbol": "GOOGL",
90
+ "shares": 66,
91
+ "avg_cost": 301.0,
92
+ "price": 301.0,
93
+ "mkt": 19866.0,
94
+ "pnl": 0.0,
95
+ "pct": 0.0,
96
+ "score": 45
97
+ },
98
+ {
99
+ "symbol": "NVDA",
100
+ "shares": 58,
101
+ "avg_cost": 172.7,
102
+ "price": 172.6999969482422,
103
+ "mkt": 10016.599822998047,
104
+ "pnl": -0.00017700195246561634,
105
+ "pct": -1.7670861662821835e-06,
106
+ "score": 35
107
+ },
108
+ {
109
+ "symbol": "MSFT",
110
+ "shares": 26,
111
+ "avg_cost": 381.87,
112
+ "price": 381.8699951171875,
113
+ "mkt": 9928.619873046875,
114
+ "pnl": -0.0001269531251182343,
115
+ "pct": -1.2786583125645734e-06,
116
+ "score": 34
117
+ },
118
+ {
119
+ "symbol": "TSLA",
120
+ "shares": 27,
121
+ "avg_cost": 367.96,
122
+ "price": 367.9599914550781,
123
+ "mkt": 9934.91976928711,
124
+ "pnl": -0.00023071289007248197,
125
+ "pct": -2.322242054209056e-06,
126
+ "score": 42
127
+ },
128
+ {
129
+ "symbol": "MS",
130
+ "shares": 74,
131
+ "avg_cost": 161.47000122070312,
132
+ "price": 161.47000122070312,
133
+ "mkt": 11948.780090332031,
134
+ "pnl": 0.0,
135
+ "pct": 0.0,
136
+ "score": 71
137
+ }
138
+ ]
139
+ }
monitor.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 美股持仓监控 — 盘中每分钟刷新,盘外自动等待
4
+ 用法: python3 monitor.py [刷新间隔秒数,默认60]
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ import time
11
+ from datetime import datetime, timedelta
12
+
13
+ import yfinance as yf
14
+
15
+ PORTFOLIO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "my_portfolio.json")
16
+ INITIAL_CASH = 100_000.0
17
+ REFRESH_SEC = int(sys.argv[1]) if len(sys.argv) > 1 else 60
18
+
19
+
20
+ def load_portfolio():
21
+ with open(PORTFOLIO_FILE) as f:
22
+ return json.load(f)
23
+
24
+
25
+ def get_prices(symbols):
26
+ """批量获取价格(比逐个快很多)"""
27
+ prices = {}
28
+ if not symbols:
29
+ return prices
30
+ tickers = yf.Tickers(" ".join(symbols))
31
+ for sym in symbols:
32
+ try:
33
+ info = tickers.tickers[sym].fast_info
34
+ p = info.get("lastPrice") or info.get("last_price")
35
+ if p is None:
36
+ hist = tickers.tickers[sym].history(period="1d")
37
+ p = hist["Close"].iloc[-1] if not hist.empty else 0
38
+ prices[sym] = round(float(p), 2)
39
+ except Exception:
40
+ prices[sym] = 0
41
+ return prices
42
+
43
+
44
+ def is_market_open():
45
+ """判断美股是否开盘(美东时间 周一-周五 9:30-16:00)"""
46
+ try:
47
+ from zoneinfo import ZoneInfo
48
+ except ImportError:
49
+ from backports.zoneinfo import ZoneInfo
50
+
51
+ et = datetime.now(ZoneInfo("America/New_York"))
52
+ # 周末
53
+ if et.weekday() >= 5:
54
+ return False, et
55
+ # 盘前/盘后
56
+ market_open = et.replace(hour=9, minute=30, second=0, microsecond=0)
57
+ market_close = et.replace(hour=16, minute=0, second=0, microsecond=0)
58
+ return market_open <= et <= market_close, et
59
+
60
+
61
+ def time_to_next_open():
62
+ """计算距离下次开盘的时间"""
63
+ try:
64
+ from zoneinfo import ZoneInfo
65
+ except ImportError:
66
+ from backports.zoneinfo import ZoneInfo
67
+
68
+ et = datetime.now(ZoneInfo("America/New_York"))
69
+ # 找到下一个工作日的9:30
70
+ target = et.replace(hour=9, minute=30, second=0, microsecond=0)
71
+
72
+ if et.weekday() < 5 and et < target:
73
+ # 今天是工作日且还没开盘
74
+ pass
75
+ else:
76
+ # 找下一个工作日
77
+ days_ahead = 1
78
+ while True:
79
+ target += timedelta(days=1)
80
+ if target.weekday() < 5:
81
+ break
82
+ days_ahead += 1
83
+ target = target.replace(hour=9, minute=30, second=0, microsecond=0)
84
+
85
+ diff = target - et
86
+ return diff
87
+
88
+
89
+ def clear_screen():
90
+ os.system("clear" if os.name != "nt" else "cls")
91
+
92
+
93
+ def display(portfolio, prices, et_now, is_open):
94
+ clear_screen()
95
+ status = "🟢 开盘中" if is_open else "🔴 已休市"
96
+ print(f"""
97
+ ╔══════════════════════════════════════════════════════════════╗
98
+ ║ 📊 美股模拟投资 — 实时监控 {status} ║
99
+ ║ 美东时间: {et_now.strftime('%Y-%m-%d %H:%M:%S'):<20} 刷新间隔: {REFRESH_SEC}秒 ║
100
+ ╠══════════════════════════════════════════════════════════════╣""")
101
+
102
+ total_market = 0
103
+ total_cost = 0
104
+
105
+ if portfolio["holdings"]:
106
+ print(f"║ {'股票':<7} {'股数':>5} {'成本':>9} {'现价':>9} {'市值':>11} {'盈亏':>11} {'涨跌':>7} ║")
107
+ print(f"║ {'─'*62} ║")
108
+
109
+ for sym in sorted(portfolio["holdings"]):
110
+ info = portfolio["holdings"][sym]
111
+ price = prices.get(sym, info["avg_cost"])
112
+ mkt = info["shares"] * price
113
+ cost = info["shares"] * info["avg_cost"]
114
+ pnl = mkt - cost
115
+ pct = (pnl / cost * 100) if cost > 0 else 0
116
+ sign = "+" if pnl >= 0 else ""
117
+ color_pnl = f"{sign}{pnl:,.0f}"
118
+ color_pct = f"{sign}{pct:.1f}%"
119
+
120
+ total_market += mkt
121
+ total_cost += cost
122
+
123
+ print(f"║ {sym:<7} {info['shares']:>5} {info['avg_cost']:>9.2f} {price:>9.2f} {mkt:>11,.2f} {color_pnl:>11} {color_pct:>7} ║")
124
+ else:
125
+ print("║ (空仓) ║")
126
+
127
+ total_assets = portfolio["cash"] + total_market
128
+ total_pnl = total_assets - INITIAL_CASH
129
+ total_pct = (total_pnl / INITIAL_CASH * 100)
130
+ sign = "+" if total_pnl >= 0 else ""
131
+
132
+ print(f"║ {'─'*62} ║")
133
+ print(f"║ 💰 现金: ${portfolio['cash']:>11,.2f} ║")
134
+ print(f"║ 📈 持仓: ${total_market:>11,.2f} ║")
135
+ print(f"║ 💼 总资产: ${total_assets:>11,.2f} 总盈亏: {sign}${total_pnl:>10,.2f} ({sign}{total_pct:.1f}%) ║")
136
+ print(f"╚══════════════════════════════════════════���═══════════════════╝")
137
+
138
+ if not is_open:
139
+ delta = time_to_next_open()
140
+ hours = int(delta.total_seconds() // 3600)
141
+ mins = int((delta.total_seconds() % 3600) // 60)
142
+ print(f"\n ⏳ 距离下次开盘: {hours}小时{mins}分钟(休市期间每5分钟刷新一次)")
143
+
144
+ print(f"\n 按 Ctrl+C 退出")
145
+
146
+
147
+ def main():
148
+ print(" 🚀 启动监控中...")
149
+
150
+ while True:
151
+ try:
152
+ portfolio = load_portfolio()
153
+ symbols = list(portfolio["holdings"].keys())
154
+ prices = get_prices(symbols)
155
+ is_open, et_now = is_market_open()
156
+
157
+ display(portfolio, prices, et_now, is_open)
158
+
159
+ # 盘中按设定间隔刷新,盘外每5分钟刷一次
160
+ wait = REFRESH_SEC if is_open else 300
161
+ time.sleep(wait)
162
+
163
+ except KeyboardInterrupt:
164
+ print("\n\n 👋 监控已停止\n")
165
+ break
166
+ except Exception as e:
167
+ print(f"\n ⚠️ 出错: {e},{REFRESH_SEC}秒后重试...")
168
+ time.sleep(REFRESH_SEC)
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
my_portfolio.json ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cash": 28385.47959777832,
3
+ "holdings": {
4
+ "AAPL": {
5
+ "shares": 40,
6
+ "avg_cost": 247.99
7
+ },
8
+ "GOOGL": {
9
+ "shares": 66,
10
+ "avg_cost": 301.0
11
+ },
12
+ "NVDA": {
13
+ "shares": 58,
14
+ "avg_cost": 172.7
15
+ },
16
+ "MSFT": {
17
+ "shares": 26,
18
+ "avg_cost": 381.87
19
+ },
20
+ "TSLA": {
21
+ "shares": 27,
22
+ "avg_cost": 367.96
23
+ },
24
+ "MS": {
25
+ "shares": 74,
26
+ "avg_cost": 161.47000122070312
27
+ }
28
+ },
29
+ "history": [
30
+ {
31
+ "action": "BUY",
32
+ "symbol": "AAPL",
33
+ "shares": 80,
34
+ "price": 247.99,
35
+ "total": 19839.2,
36
+ "time": "2026-03-20 22:59:24"
37
+ },
38
+ {
39
+ "action": "BUY",
40
+ "symbol": "GOOGL",
41
+ "shares": 66,
42
+ "price": 301.0,
43
+ "total": 19866.0,
44
+ "time": "2026-03-20 22:59:28"
45
+ },
46
+ {
47
+ "action": "BUY",
48
+ "symbol": "NVDA",
49
+ "shares": 115,
50
+ "price": 172.7,
51
+ "total": 19860.5,
52
+ "time": "2026-03-20 22:59:33"
53
+ },
54
+ {
55
+ "action": "BUY",
56
+ "symbol": "MSFT",
57
+ "shares": 52,
58
+ "price": 381.87,
59
+ "total": 19857.24,
60
+ "time": "2026-03-20 22:59:35"
61
+ },
62
+ {
63
+ "action": "BUY",
64
+ "symbol": "TSLA",
65
+ "shares": 54,
66
+ "price": 367.96,
67
+ "total": 19869.84,
68
+ "time": "2026-03-20 22:59:36"
69
+ }
70
+ ],
71
+ "created": "2026-03-20 22:59:24",
72
+ "transactions": [
73
+ {
74
+ "type": "sell",
75
+ "symbol": "AAPL",
76
+ "shares": 40,
77
+ "price": 247.99000549316406,
78
+ "pnl": 0.0,
79
+ "time": "2026-03-20T23:58:37.436602"
80
+ },
81
+ {
82
+ "type": "sell",
83
+ "symbol": "NVDA",
84
+ "shares": 57,
85
+ "price": 172.6999969482422,
86
+ "pnl": -0.0,
87
+ "time": "2026-03-20T23:58:37.436621"
88
+ },
89
+ {
90
+ "type": "sell",
91
+ "symbol": "MSFT",
92
+ "shares": 26,
93
+ "price": 381.8699951171875,
94
+ "pnl": -0.0,
95
+ "time": "2026-03-20T23:58:37.436629"
96
+ },
97
+ {
98
+ "type": "sell",
99
+ "symbol": "TSLA",
100
+ "shares": 27,
101
+ "price": 367.9599914550781,
102
+ "pnl": -0.0,
103
+ "time": "2026-03-20T23:58:37.436634"
104
+ },
105
+ {
106
+ "type": "buy",
107
+ "symbol": "MS",
108
+ "shares": 74,
109
+ "price": 161.47000122070312,
110
+ "time": "2026-03-20T23:58:37.436644"
111
+ }
112
+ ]
113
+ }
portfolio.py ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 美股模拟投资工具
4
+ - 初始资金: $100,000
5
+ - 用真实股价买卖
6
+ - 随时查看持仓和收益
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+ from datetime import datetime
13
+
14
+ import yfinance as yf
15
+
16
+ PORTFOLIO_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "my_portfolio.json")
17
+ INITIAL_CASH = 100_000.0
18
+
19
+
20
+ def load_portfolio():
21
+ """加载投资组合"""
22
+ if os.path.exists(PORTFOLIO_FILE):
23
+ with open(PORTFOLIO_FILE) as f:
24
+ return json.load(f)
25
+ # 初始化: 10万美刀现金,空持仓
26
+ portfolio = {
27
+ "cash": INITIAL_CASH,
28
+ "holdings": {}, # {"AAPL": {"shares": 10, "avg_cost": 150.0}, ...}
29
+ "history": [], # 交易记录
30
+ "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
31
+ }
32
+ save_portfolio(portfolio)
33
+ return portfolio
34
+
35
+
36
+ def save_portfolio(portfolio):
37
+ with open(PORTFOLIO_FILE, "w") as f:
38
+ json.dump(portfolio, f, indent=2, ensure_ascii=False)
39
+
40
+
41
+ def get_price(symbol):
42
+ """获取股票当前价格"""
43
+ ticker = yf.Ticker(symbol)
44
+ info = ticker.fast_info
45
+ price = info.get("lastPrice") or info.get("last_price")
46
+ if price is None:
47
+ hist = ticker.history(period="1d")
48
+ if hist.empty:
49
+ return None
50
+ price = hist["Close"].iloc[-1]
51
+ return round(float(price), 2)
52
+
53
+
54
+ def buy(symbol, amount_usd):
55
+ """用指定金额买入股票(按当前价自动算股数)"""
56
+ symbol = symbol.upper()
57
+ price = get_price(symbol)
58
+ if price is None:
59
+ print(f" ❌ 找不到 {symbol} 的价格,请检查股票代码")
60
+ return
61
+
62
+ portfolio = load_portfolio()
63
+ amount_usd = float(amount_usd)
64
+
65
+ if amount_usd > portfolio["cash"]:
66
+ print(f" ❌ 现金不足!可用: ${portfolio['cash']:,.2f},想买: ${amount_usd:,.2f}")
67
+ return
68
+
69
+ shares = int(amount_usd / price) # 买整数股
70
+ if shares == 0:
71
+ print(f" ❌ 金额太少,{symbol} 当前 ${price},至少需要 ${price}")
72
+ return
73
+
74
+ cost = shares * price
75
+ portfolio["cash"] -= cost
76
+
77
+ if symbol in portfolio["holdings"]:
78
+ old = portfolio["holdings"][symbol]
79
+ total_shares = old["shares"] + shares
80
+ total_cost = old["shares"] * old["avg_cost"] + cost
81
+ portfolio["holdings"][symbol] = {
82
+ "shares": total_shares,
83
+ "avg_cost": round(total_cost / total_shares, 2),
84
+ }
85
+ else:
86
+ portfolio["holdings"][symbol] = {
87
+ "shares": shares,
88
+ "avg_cost": price,
89
+ }
90
+
91
+ portfolio["history"].append({
92
+ "action": "BUY",
93
+ "symbol": symbol,
94
+ "shares": shares,
95
+ "price": price,
96
+ "total": round(cost, 2),
97
+ "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
98
+ })
99
+
100
+ save_portfolio(portfolio)
101
+ print(f" ✅ 买入 {symbol} x {shares}股 @ ${price} = ${cost:,.2f}")
102
+ print(f" 剩余现金: ${portfolio['cash']:,.2f}")
103
+
104
+
105
+ def sell(symbol, shares=None):
106
+ """卖出股票(默认全部卖出)"""
107
+ symbol = symbol.upper()
108
+ portfolio = load_portfolio()
109
+
110
+ if symbol not in portfolio["holdings"]:
111
+ print(f" ❌ 你没有持有 {symbol}")
112
+ return
113
+
114
+ holding = portfolio["holdings"][symbol]
115
+ if shares is None:
116
+ shares = holding["shares"]
117
+ else:
118
+ shares = int(shares)
119
+
120
+ if shares > holding["shares"]:
121
+ print(f" ❌ 只有 {holding['shares']}股,不能卖 {shares}股")
122
+ return
123
+
124
+ price = get_price(symbol)
125
+ if price is None:
126
+ print(f" ❌ 获取 {symbol} 价格失败")
127
+ return
128
+
129
+ revenue = shares * price
130
+ profit = (price - holding["avg_cost"]) * shares
131
+ portfolio["cash"] += revenue
132
+
133
+ if shares == holding["shares"]:
134
+ del portfolio["holdings"][symbol]
135
+ else:
136
+ portfolio["holdings"][symbol]["shares"] -= shares
137
+
138
+ portfolio["history"].append({
139
+ "action": "SELL",
140
+ "symbol": symbol,
141
+ "shares": shares,
142
+ "price": price,
143
+ "total": round(revenue, 2),
144
+ "profit": round(profit, 2),
145
+ "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
146
+ })
147
+
148
+ save_portfolio(portfolio)
149
+ sign = "+" if profit >= 0 else ""
150
+ print(f" ✅ 卖出 {symbol} x {shares}股 @ ${price} = ${revenue:,.2f}")
151
+ print(f" 盈亏: {sign}${profit:,.2f}")
152
+ print(f" 剩余现金: ${portfolio['cash']:,.2f}")
153
+
154
+
155
+ def show():
156
+ """查看持仓和收益"""
157
+ portfolio = load_portfolio()
158
+ print()
159
+ print("=" * 60)
160
+ print(" 📊 我的美股模拟投资组合")
161
+ print("=" * 60)
162
+
163
+ total_market_value = 0
164
+ total_cost = 0
165
+
166
+ if portfolio["holdings"]:
167
+ print(f"\n {'股票':<8} {'股数':>6} {'成本价':>10} {'现价':>10} {'市值':>12} {'盈亏':>12} {'涨跌%':>8}")
168
+ print(" " + "-" * 68)
169
+
170
+ for symbol, info in sorted(portfolio["holdings"].items()):
171
+ price = get_price(symbol)
172
+ if price is None:
173
+ price = info["avg_cost"] # fallback
174
+
175
+ market_val = info["shares"] * price
176
+ cost_val = info["shares"] * info["avg_cost"]
177
+ profit = market_val - cost_val
178
+ pct = (profit / cost_val * 100) if cost_val > 0 else 0
179
+ sign = "+" if profit >= 0 else ""
180
+
181
+ total_market_value += market_val
182
+ total_cost += cost_val
183
+
184
+ print(f" {symbol:<8} {info['shares']:>6} {info['avg_cost']:>10.2f} {price:>10.2f} {market_val:>12,.2f} {sign}{profit:>11,.2f} {sign}{pct:>7.1f}%")
185
+ else:
186
+ print("\n (空仓,还没买任何股票)")
187
+
188
+ total_assets = portfolio["cash"] + total_market_value
189
+ total_profit = total_assets - INITIAL_CASH
190
+ total_pct = (total_profit / INITIAL_CASH * 100)
191
+ sign = "+" if total_profit >= 0 else ""
192
+
193
+ print()
194
+ print(" " + "-" * 68)
195
+ print(f" 💰 现金: ${portfolio['cash']:>12,.2f}")
196
+ print(f" 📈 持仓市值: ${total_market_value:>12,.2f}")
197
+ print(f" 💼 总资产: ${total_assets:>12,.2f}")
198
+ print(f" 📊 总盈亏: {sign}${total_profit:>11,.2f} ({sign}{total_pct:.1f}%)")
199
+ print("=" * 60)
200
+ print()
201
+
202
+
203
+ def history():
204
+ """查看交易记录"""
205
+ portfolio = load_portfolio()
206
+ if not portfolio["history"]:
207
+ print(" 还没有交易记录")
208
+ return
209
+
210
+ print(f"\n {'时间':<20} {'操作':<5} {'股票':<8} {'股数':>6} {'价格':>10} {'金额':>12}")
211
+ print(" " + "-" * 65)
212
+ for tx in portfolio["history"][-20:]: # 最近20条
213
+ print(f" {tx['time']:<20} {tx['action']:<5} {tx['symbol']:<8} {tx['shares']:>6} {tx['price']:>10.2f} ${tx['total']:>11,.2f}")
214
+ print()
215
+
216
+
217
+ def reset():
218
+ """重置投资组合"""
219
+ if os.path.exists(PORTFOLIO_FILE):
220
+ os.remove(PORTFOLIO_FILE)
221
+ load_portfolio()
222
+ print(" 🔄 已重置!初始资金 $100,000.00")
223
+
224
+
225
+ def main():
226
+ if len(sys.argv) < 2:
227
+ print("""
228
+ 美股模拟投资工具 💹
229
+ ──────────────────────────────────
230
+ 用法:
231
+ python portfolio.py show 查看持仓和收益
232
+ python portfolio.py buy AAPL 10000 用$10000买入AAPL
233
+ python portfolio.py sell AAPL 全部卖出AAPL
234
+ python portfolio.py sell AAPL 5 卖出5股AAPL
235
+ python portfolio.py history 查看交易记录
236
+ python portfolio.py reset 重置(重新开始)
237
+
238
+ 示例: 把10万分散投资
239
+ python portfolio.py buy AAPL 20000
240
+ python portfolio.py buy GOOGL 20000
241
+ python portfolio.py buy MSFT 20000
242
+ python portfolio.py buy NVDA 20000
243
+ python portfolio.py buy TSLA 20000
244
+ """)
245
+ return
246
+
247
+ cmd = sys.argv[1].lower()
248
+
249
+ if cmd == "show":
250
+ show()
251
+ elif cmd == "buy":
252
+ if len(sys.argv) < 4:
253
+ print(" 用法: python portfolio.py buy <股票代码> <金额>")
254
+ return
255
+ buy(sys.argv[2], sys.argv[3])
256
+ elif cmd == "sell":
257
+ if len(sys.argv) < 3:
258
+ print(" 用法: python portfolio.py sell <股票代码> [股数]")
259
+ return
260
+ shares = int(sys.argv[3]) if len(sys.argv) > 3 else None
261
+ sell(sys.argv[2], shares)
262
+ elif cmd == "history":
263
+ history()
264
+ elif cmd == "reset":
265
+ reset()
266
+ else:
267
+ print(f" 未知命令: {cmd}")
268
+
269
+
270
+ if __name__ == "__main__":
271
+ main()
stock_screener.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 美股智能选股器 — 基于 daily_stock_analysis 项目的技术分析体系
4
+ 参考: https://github.com/ZhuLinsen/daily_stock_analysis
5
+
6
+ 评分维度(100分制):
7
+ - 趋势排列 30分:MA5>MA10>MA20 多头排列
8
+ - 乖离率 20分:接近 MA5 不追高
9
+ - 量能形态 15分:缩量回调最佳
10
+ - MACD 15分:金叉/多头
11
+ - RSI 10分:超卖反弹/强势
12
+ - 支撑 10分:均线支撑有效
13
+
14
+ 用法: python3 stock_screener.py
15
+ """
16
+
17
+ import sys
18
+ import warnings
19
+ warnings.filterwarnings("ignore")
20
+
21
+ import numpy as np
22
+ import pandas as pd
23
+ import yfinance as yf
24
+ from datetime import datetime, timedelta
25
+ from concurrent.futures import ThreadPoolExecutor, as_completed
26
+
27
+ # ── 候选股票池 ──────────────────────────────────────────
28
+ # 科技/半导体/AI 热门 + 大盘蓝筹 + 消费 + 医药 + 能源 + 金融
29
+ US_STOCKS = [
30
+ # 科技巨头
31
+ "AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA",
32
+ # 半导体/AI
33
+ "AMD", "AVGO", "QCOM", "INTC", "MU", "MRVL", "ARM", "SMCI", "TSM",
34
+ # 软件/云
35
+ "CRM", "ORCL", "ADBE", "NOW", "SNOW", "PLTR", "NET", "DDOG", "CRWD",
36
+ # 消费/零售
37
+ "COST", "WMT", "TGT", "NKE", "SBUX", "MCD", "PEP", "KO",
38
+ # 金融
39
+ "JPM", "GS", "MS", "BAC", "V", "MA", "AXP",
40
+ # 医药/生物
41
+ "LLY", "UNH", "JNJ", "PFE", "ABBV", "MRK", "BMY",
42
+ # 能源
43
+ "XOM", "CVX", "SLB", "OXY",
44
+ # 其他热门
45
+ "BA", "CAT", "DE", "GE", "LMT", "COIN", "MARA", "RIVN", "UBER", "ABNB",
46
+ "SQ", "SHOP", "PYPL", "ROKU", "SNAP", "PINS", "RBLX", "U",
47
+ ]
48
+
49
+
50
+ # ── 技术分析核心(复刻 daily_stock_analysis 的 StockTrendAnalyzer) ──
51
+
52
+ def calc_macd(close, fast=12, slow=26, signal=9):
53
+ ema_fast = close.ewm(span=fast, adjust=False).mean()
54
+ ema_slow = close.ewm(span=slow, adjust=False).mean()
55
+ dif = ema_fast - ema_slow
56
+ dea = dif.ewm(span=signal, adjust=False).mean()
57
+ bar = (dif - dea) * 2
58
+ return dif, dea, bar
59
+
60
+
61
+ def calc_rsi(close, period=12):
62
+ delta = close.diff()
63
+ gain = delta.where(delta > 0, 0).rolling(period).mean()
64
+ loss = (-delta.where(delta < 0, 0)).rolling(period).mean()
65
+ rs = gain / loss
66
+ return (100 - 100 / (1 + rs)).fillna(50)
67
+
68
+
69
+ def analyze_one(symbol):
70
+ """对单只股票做完整技术分析,返回 dict 或 None"""
71
+ try:
72
+ tk = yf.Ticker(symbol)
73
+ df = tk.history(period="6mo", auto_adjust=True)
74
+ if df is None or len(df) < 60:
75
+ return None
76
+
77
+ df = df.reset_index()
78
+ close = df["Close"]
79
+ volume = df["Volume"]
80
+
81
+ # ── 均线 ──
82
+ ma5 = close.rolling(5).mean()
83
+ ma10 = close.rolling(10).mean()
84
+ ma20 = close.rolling(20).mean()
85
+ ma60 = close.rolling(60).mean()
86
+
87
+ last = len(df) - 1
88
+ price = float(close.iloc[last])
89
+ m5, m10, m20, m60 = (
90
+ float(ma5.iloc[last]),
91
+ float(ma10.iloc[last]),
92
+ float(ma20.iloc[last]),
93
+ float(ma60.iloc[last]),
94
+ )
95
+
96
+ # ── 趋势判断(30分)──
97
+ trend_score = 0
98
+ trend_label = ""
99
+ if m5 > m10 > m20:
100
+ # 检查间距是否在扩大
101
+ prev_idx = max(0, last - 5)
102
+ prev_spread = (float(ma5.iloc[prev_idx]) - float(ma20.iloc[prev_idx])) / float(ma20.iloc[prev_idx]) * 100
103
+ curr_spread = (m5 - m20) / m20 * 100
104
+ if curr_spread > prev_spread and curr_spread > 5:
105
+ trend_score = 30
106
+ trend_label = "强势多头"
107
+ else:
108
+ trend_score = 26
109
+ trend_label = "多头排列"
110
+ elif m5 > m10 and m10 <= m20:
111
+ trend_score = 18
112
+ trend_label = "弱势多头"
113
+ elif abs(m5 - m10) / m10 < 0.01 and abs(m10 - m20) / m20 < 0.01:
114
+ trend_score = 12
115
+ trend_label = "盘整"
116
+ elif m5 < m10 and m10 >= m20:
117
+ trend_score = 8
118
+ trend_label = "弱势空头"
119
+ elif m5 < m10 < m20:
120
+ prev_idx = max(0, last - 5)
121
+ prev_spread = (float(ma20.iloc[prev_idx]) - float(ma5.iloc[prev_idx])) / float(ma5.iloc[prev_idx]) * 100
122
+ curr_spread = (m20 - m5) / m5 * 100
123
+ if curr_spread > prev_spread and curr_spread > 5:
124
+ trend_score = 0
125
+ trend_label = "强势空头"
126
+ else:
127
+ trend_score = 4
128
+ trend_label = "空头排列"
129
+ else:
130
+ trend_score = 12
131
+ trend_label = "盘整"
132
+
133
+ # ── 乖离率(20分)──
134
+ bias_ma5 = (price - m5) / m5 * 100 if m5 > 0 else 0
135
+ bias_score = 0
136
+ bias_label = ""
137
+ BIAS_THRESHOLD = 5.0
138
+ if bias_ma5 < 0:
139
+ if bias_ma5 > -3:
140
+ bias_score = 20
141
+ bias_label = f"回踩买点({bias_ma5:+.1f}%)"
142
+ elif bias_ma5 > -5:
143
+ bias_score = 16
144
+ bias_label = f"回踩MA5({bias_ma5:+.1f}%)"
145
+ else:
146
+ bias_score = 8
147
+ bias_label = f"偏离过大({bias_ma5:+.1f}%)"
148
+ elif bias_ma5 < 2:
149
+ bias_score = 18
150
+ bias_label = f"贴近MA5({bias_ma5:+.1f}%)"
151
+ elif bias_ma5 < BIAS_THRESHOLD:
152
+ bias_score = 14
153
+ bias_label = f"略高({bias_ma5:+.1f}%)"
154
+ else:
155
+ bias_score = 4
156
+ bias_label = f"追高危险({bias_ma5:+.1f}%)"
157
+
158
+ # ── 量能(15分)──
159
+ vol_5d_avg = float(volume.iloc[-6:-1].mean())
160
+ vol_ratio = float(volume.iloc[last]) / vol_5d_avg if vol_5d_avg > 0 else 1
161
+ prev_close = float(close.iloc[last - 1])
162
+ price_chg = (price - prev_close) / prev_close * 100
163
+
164
+ vol_score = 0
165
+ vol_label = ""
166
+ if vol_ratio >= 1.5:
167
+ if price_chg > 0:
168
+ vol_score = 12
169
+ vol_label = "放量上涨"
170
+ else:
171
+ vol_score = 0
172
+ vol_label = "放量下跌"
173
+ elif vol_ratio <= 0.7:
174
+ if price_chg > 0:
175
+ vol_score = 6
176
+ vol_label = "缩量上涨"
177
+ else:
178
+ vol_score = 15
179
+ vol_label = "缩量回调"
180
+ else:
181
+ vol_score = 10
182
+ vol_label = "量能正常"
183
+
184
+ # ── MACD(15分)──
185
+ dif, dea, bar = calc_macd(close)
186
+ macd_dif = float(dif.iloc[last])
187
+ macd_dea = float(dea.iloc[last])
188
+ prev_diff = float(dif.iloc[last - 1]) - float(dea.iloc[last - 1])
189
+ curr_diff = macd_dif - macd_dea
190
+
191
+ macd_score = 0
192
+ macd_label = ""
193
+ is_golden = prev_diff <= 0 and curr_diff > 0
194
+ is_death = prev_diff >= 0 and curr_diff < 0
195
+
196
+ if is_golden and macd_dif > 0:
197
+ macd_score = 15
198
+ macd_label = "零轴上金叉"
199
+ elif is_golden:
200
+ macd_score = 12
201
+ macd_label = "金叉"
202
+ elif float(dif.iloc[last - 1]) <= 0 and macd_dif > 0:
203
+ macd_score = 10
204
+ macd_label = "上穿零轴"
205
+ elif is_death:
206
+ macd_score = 0
207
+ macd_label = "死叉"
208
+ elif macd_dif > 0 and macd_dea > 0:
209
+ macd_score = 8
210
+ macd_label = "多头"
211
+ elif macd_dif < 0 and macd_dea < 0:
212
+ macd_score = 2
213
+ macd_label = "空头"
214
+ else:
215
+ macd_score = 5
216
+ macd_label = "中性"
217
+
218
+ # ── RSI(10分)──
219
+ rsi_12 = float(calc_rsi(close, 12).iloc[last])
220
+ rsi_score = 0
221
+ rsi_label = ""
222
+ if rsi_12 > 70:
223
+ rsi_score = 0
224
+ rsi_label = f"超买({rsi_12:.0f})"
225
+ elif rsi_12 > 60:
226
+ rsi_score = 8
227
+ rsi_label = f"强势({rsi_12:.0f})"
228
+ elif rsi_12 >= 40:
229
+ rsi_score = 5
230
+ rsi_label = f"中性({rsi_12:.0f})"
231
+ elif rsi_12 >= 30:
232
+ rsi_score = 3
233
+ rsi_label = f"弱势({rsi_12:.0f})"
234
+ else:
235
+ rsi_score = 10
236
+ rsi_label = f"超卖({rsi_12:.0f})"
237
+
238
+ # ── 支撑(10分)──
239
+ support_score = 0
240
+ support_label = ""
241
+ ma5_dist = abs(price - m5) / m5 if m5 > 0 else 1
242
+ ma10_dist = abs(price - m10) / m10 if m10 > 0 else 1
243
+ supports = []
244
+ if ma5_dist <= 0.02 and price >= m5:
245
+ support_score += 5
246
+ supports.append("MA5")
247
+ if ma10_dist <= 0.02 and price >= m10:
248
+ support_score += 5
249
+ supports.append("MA10")
250
+ support_label = "+".join(supports) if supports else "无"
251
+
252
+ # ── 总分 ──
253
+ total = trend_score + bias_score + vol_score + macd_score + rsi_score + support_score
254
+
255
+ # ── 信号 ──
256
+ if total >= 75 and trend_label in ("强势多头", "多头排列"):
257
+ signal = "强烈买入"
258
+ elif total >= 60 and trend_label in ("强势多头", "多头排列", "弱势多头"):
259
+ signal = "买入"
260
+ elif total >= 45:
261
+ signal = "持有"
262
+ elif total >= 30:
263
+ signal = "观望"
264
+ elif trend_label in ("空头排列", "强势空头"):
265
+ signal = "强烈卖出"
266
+ else:
267
+ signal = "卖出"
268
+
269
+ # 20日涨跌幅
270
+ price_20d_ago = float(close.iloc[max(0, last - 20)])
271
+ chg_20d = (price - price_20d_ago) / price_20d_ago * 100
272
+
273
+ return {
274
+ "symbol": symbol,
275
+ "price": price,
276
+ "total": total,
277
+ "signal": signal,
278
+ "trend": f"{trend_label}({trend_score})",
279
+ "bias": f"{bias_label}({bias_score})",
280
+ "volume": f"{vol_label}({vol_score})",
281
+ "macd": f"{macd_label}({macd_score})",
282
+ "rsi": f"{rsi_label}({rsi_score})",
283
+ "support": f"{support_label}({support_score})",
284
+ "chg_20d": chg_20d,
285
+ "vol_ratio": vol_ratio,
286
+ "trend_label": trend_label,
287
+ }
288
+ except Exception as e:
289
+ return None
290
+
291
+
292
+ def main():
293
+ print("\n 🔍 美股智能选股器 (基于 daily_stock_analysis 技术分析体系)")
294
+ print(" " + "=" * 62)
295
+ print(f" 📅 分析日期: {datetime.now().strftime('%Y-%m-%d %H:%M')}")
296
+ print(f" 📊 候选池: {len(US_STOCKS)} 只美股")
297
+ print(f" ⏳ 正在获取数据并分析(约30秒)...\n")
298
+
299
+ results = []
300
+ failed = []
301
+
302
+ with ThreadPoolExecutor(max_workers=8) as pool:
303
+ futures = {pool.submit(analyze_one, sym): sym for sym in US_STOCKS}
304
+ done_count = 0
305
+ for future in as_completed(futures):
306
+ done_count += 1
307
+ sym = futures[future]
308
+ r = future.result()
309
+ if r:
310
+ results.append(r)
311
+ else:
312
+ failed.append(sym)
313
+ # 进度
314
+ if done_count % 10 == 0 or done_count == len(US_STOCKS):
315
+ sys.stdout.write(f"\r 进度: {done_count}/{len(US_STOCKS)}")
316
+ sys.stdout.flush()
317
+
318
+ print("\n")
319
+
320
+ if not results:
321
+ print(" ❌ 未获取到任何数据")
322
+ return
323
+
324
+ # 按总分降序
325
+ results.sort(key=lambda x: x["total"], reverse=True)
326
+
327
+ # ── 输出Top推荐 ──
328
+ print(" ╔══════════════════════════════════════════════════════════════════════════════╗")
329
+ print(" ║ 📈 明日选股推荐(按综合评分排序) ║")
330
+ print(" ╠══════════════════════════════════════════════════════════════════════════════╣")
331
+ print(f" ║ {'排名':>2} {'代码':<6} {'现价':>8} {'评分':>4} {'信号':<8} {'趋势':<12} {'乖离率':<16} {'MACD':<10} {'RSI':<10} ║")
332
+ print(f" ║ {'─'*74} ║")
333
+
334
+ for i, r in enumerate(results[:20]):
335
+ rank = i + 1
336
+ sig = r["signal"]
337
+ # 信号颜色标记
338
+ if "买入" in sig:
339
+ sig_mark = f"🟢{sig}"
340
+ elif "卖" in sig:
341
+ sig_mark = f"🔴{sig}"
342
+ else:
343
+ sig_mark = f"🟡{sig}"
344
+
345
+ print(f" ║ {rank:>2}. {r['symbol']:<6} ${r['price']:>7.2f} {r['total']:>3}分 {sig_mark:<10} {r['trend']:<12} {r['bias']:<16} {r['macd']:<10} {r['rsi']:<10} ║")
346
+
347
+ print(" ╚══════════════════════════════════════════════════════════════════════════════╝")
348
+
349
+ # ── 强烈买入 ──
350
+ strong_buys = [r for r in results if r["signal"] == "强烈买入"]
351
+ buys = [r for r in results if r["signal"] == "买入"]
352
+
353
+ print(f"\n 🎯 总结")
354
+ print(f" {'─'*60}")
355
+
356
+ if strong_buys:
357
+ print(f"\n 🟢🟢 强烈买入信号({len(strong_buys)}只):")
358
+ for r in strong_buys:
359
+ print(f" {r['symbol']:>6} @ ${r['price']:.2f} 评分{r['total']} {r['trend']} {r['macd']} {r['rsi']}")
360
+ print(f" 20日涨幅: {r['chg_20d']:+.1f}% 量比: {r['vol_ratio']:.2f}")
361
+ else:
362
+ print(f"\n ⚠️ 当前无强烈买入信号")
363
+
364
+ if buys:
365
+ print(f"\n 🟢 买入信号({len(buys)}只):")
366
+ for r in buys:
367
+ print(f" {r['symbol']:>6} @ ${r['price']:.2f} 评分{r['total']} {r['trend']} {r['macd']} {r['rsi']}")
368
+
369
+ # ── 空头警告(已持仓的) ──
370
+ held = ["AAPL", "GOOGL", "MSFT", "NVDA", "TSLA"]
371
+ print(f"\n 📋 你的持仓状态:")
372
+ for r in results:
373
+ if r["symbol"] in held:
374
+ icon = "🟢" if "买" in r["signal"] else ("🔴" if "卖" in r["signal"] else "🟡")
375
+ print(f" {icon} {r['symbol']:>6} 评分{r['total']} {r['signal']} {r['trend']} {r['macd']} 20日涨幅{r['chg_20d']:+.1f}%")
376
+
377
+ if failed:
378
+ print(f"\n ⚠️ {len(failed)}只获取失败: {', '.join(failed[:10])}")
379
+
380
+ print(f"\n ⚠️ 免责声明: 技术分析仅供参考,不构成投资建议,入市有风险!")
381
+ print()
382
+
383
+
384
+ if __name__ == "__main__":
385
+ main()
trade_log.json ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "date": "2026-03-20",
4
+ "total_assets": 100000.0,
5
+ "cash": 28385.48,
6
+ "total_pnl": -0.0,
7
+ "total_pct": -0.0,
8
+ "holdings": 6,
9
+ "trades": [
10
+ {
11
+ "action": "REDUCE",
12
+ "symbol": "AAPL",
13
+ "shares": 40,
14
+ "price": 247.99000549316406,
15
+ "revenue": 9919.600219726562,
16
+ "pnl": 0.00021972656213620212
17
+ },
18
+ {
19
+ "action": "REDUCE",
20
+ "symbol": "NVDA",
21
+ "shares": 57,
22
+ "price": 172.6999969482422,
23
+ "revenue": 9843.899826049805,
24
+ "pnl": -0.00017395019466448502
25
+ },
26
+ {
27
+ "action": "REDUCE",
28
+ "symbol": "MSFT",
29
+ "shares": 26,
30
+ "price": 381.8699951171875,
31
+ "revenue": 9928.619873046875,
32
+ "pnl": -0.0001269531251182343
33
+ },
34
+ {
35
+ "action": "REDUCE",
36
+ "symbol": "TSLA",
37
+ "shares": 27,
38
+ "price": 367.9599914550781,
39
+ "revenue": 9934.91976928711,
40
+ "pnl": -0.00023071289007248197
41
+ },
42
+ {
43
+ "action": "BUY",
44
+ "score": 71,
45
+ "symbol": "MS",
46
+ "shares": 74,
47
+ "price": 161.47000122070312,
48
+ "cost": 11948.780090332031
49
+ }
50
+ ],
51
+ "top5_scores": [
52
+ {
53
+ "symbol": "MS",
54
+ "score": 71,
55
+ "signal": "买入"
56
+ },
57
+ {
58
+ "symbol": "PLTR",
59
+ "score": 69,
60
+ "signal": "买入"
61
+ },
62
+ {
63
+ "symbol": "NET",
64
+ "score": 69,
65
+ "signal": "买入"
66
+ },
67
+ {
68
+ "symbol": "PFE",
69
+ "score": 69,
70
+ "signal": "买入"
71
+ },
72
+ {
73
+ "symbol": "CVX",
74
+ "score": 69,
75
+ "signal": "买入"
76
+ }
77
+ ],
78
+ "portfolio": [
79
+ {
80
+ "symbol": "AAPL",
81
+ "shares": 40,
82
+ "avg_cost": 247.99,
83
+ "price": 247.99000549316406,
84
+ "mkt": 9919.600219726562,
85
+ "pnl": 0.00021972656213620212,
86
+ "pct": 2.215074812461637e-06,
87
+ "score": 36
88
+ },
89
+ {
90
+ "symbol": "GOOGL",
91
+ "shares": 66,
92
+ "avg_cost": 301.0,
93
+ "price": 301.0,
94
+ "mkt": 19866.0,
95
+ "pnl": 0.0,
96
+ "pct": 0.0,
97
+ "score": 45
98
+ },
99
+ {
100
+ "symbol": "NVDA",
101
+ "shares": 58,
102
+ "avg_cost": 172.7,
103
+ "price": 172.6999969482422,
104
+ "mkt": 10016.599822998047,
105
+ "pnl": -0.00017700195246561634,
106
+ "pct": -1.7670861662821835e-06,
107
+ "score": 35
108
+ },
109
+ {
110
+ "symbol": "MSFT",
111
+ "shares": 26,
112
+ "avg_cost": 381.87,
113
+ "price": 381.8699951171875,
114
+ "mkt": 9928.619873046875,
115
+ "pnl": -0.0001269531251182343,
116
+ "pct": -1.2786583125645734e-06,
117
+ "score": 34
118
+ },
119
+ {
120
+ "symbol": "TSLA",
121
+ "shares": 27,
122
+ "avg_cost": 367.96,
123
+ "price": 367.9599914550781,
124
+ "mkt": 9934.91976928711,
125
+ "pnl": -0.00023071289007248197,
126
+ "pct": -2.322242054209056e-06,
127
+ "score": 42
128
+ },
129
+ {
130
+ "symbol": "MS",
131
+ "shares": 74,
132
+ "avg_cost": 161.47000122070312,
133
+ "price": 161.47000122070312,
134
+ "mkt": 11948.780090332031,
135
+ "pnl": 0.0,
136
+ "pct": 0.0,
137
+ "score": 71
138
+ }
139
+ ]
140
+ }
141
+ ]