Upload folder using huggingface_hub
Browse files- README.md +58 -0
- auto_trader.py +489 -0
- daily_reports/report_2026-03-20.json +139 -0
- monitor.py +172 -0
- my_portfolio.json +113 -0
- portfolio.py +271 -0
- stock_screener.py +385 -0
- 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 |
+
]
|