xiaowei-system/scripts/stock_signal.py

256 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
小唯每日股票信号 — 五粮液(000858)
=================================
每日收盘后自动计算MA20状态推送操作信号
状态说明:
- LONG: 收盘价在20日均线上方 → 持仓信号
- SHORT: 收盘价在20日均线下方 → 空仓信号
- 无信号: 均线附近震荡
用法:
python3 stock_signal.py # 今日五粮液信号
python3 stock_signal.py --watch # 持续监控模式
"""
import json, sys, urllib.request
from datetime import datetime
from pathlib import Path
import numpy as np
import pandas as pd
OUTPUT = Path.home() / ".hermes" / "stock_backtest"
OUTPUT.mkdir(exist_ok=True)
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad"
# 关注股票名称映射(与 stock_portfolio.py 一致)
STOCK_NAMES = {
"000858": "五粮液",
"600519": "贵州茅台",
"000568": "泸州老窖",
"002304": "洋河股份",
"600036": "招商银行",
"601318": "中国平安",
"000001": "平安银行",
}
def send_feishu(msg):
payload = json.dumps({"msg_type": "text", "content": {"text": msg}}).encode()
req = urllib.request.Request(FEISHU_WEBHOOK, data=payload,
headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=10):
pass
except Exception:
pass
def get_data(code, count=30):
mc = f"sh{code}" if code.startswith("6") else f"sz{code}"
today = datetime.now().strftime("%Y-%m-%d")
url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get"
f"?_var=kline_dayqfq&param={mc},day,{today},{today},{count},qfq")
try:
text = urllib.request.urlopen(url, timeout=10).read().decode("utf-8")
data = json.loads(text.replace("kline_dayqfq=", "", 1))
qfq = (data.get("data", {}).get(mc, {}).get("qfqday") or
data.get("data", {}).get(mc, {}).get("day") or [])
rows = []
for item in qfq:
if len(item) < 6:
continue
try:
rows.append({"date": item[0],
"open": float(item[1]), "close": float(item[2]),
"high": float(item[3]), "low": float(item[4]),
"volume": float(item[5])})
except (ValueError, IndexError):
continue
df = pd.DataFrame(rows)
df["date"] = pd.to_datetime(df["date"])
df.set_index("date", inplace=True)
df.sort_index(inplace=True)
return df
except Exception:
return None
def get_long_data(code, start, end):
"""获取一段历史数据用于计算MA20"""
mc = f"sh{code}" if code.startswith("6") else f"sz{code}"
url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get"
f"?_var=kline_dayqfq&param={mc},day,{start},{end},500,qfq")
try:
text = urllib.request.urlopen(url, timeout=10).read().decode("utf-8")
data = json.loads(text.replace("kline_dayqfq=", "", 1))
qfq = (data.get("data", {}).get(mc, {}).get("qfqday") or
data.get("data", {}).get(mc, {}).get("day") or [])
rows = []
for item in qfq:
if len(item) < 6:
continue
try:
rows.append({"date": item[0],
"open": float(item[1]), "close": float(item[2]),
"high": float(item[3]), "low": float(item[4]),
"volume": float(item[5])})
except (ValueError, IndexError):
continue
df = pd.DataFrame(rows)
df["date"] = pd.to_datetime(df["date"])
df.set_index("date", inplace=True)
df.sort_index(inplace=True)
return df
except Exception:
return None
def compute_signal(code="000858", hist_days=60):
"""计算股票MA20信号"""
name = STOCK_NAMES.get(code, code)
end = datetime.now().strftime("%Y-%m-%d")
start = (datetime.now().replace(year=datetime.now().year-1)).strftime("%Y-%m-%d")
df = get_long_data(code, "2025-01-01", end)
if df is None or len(df) < 25:
return None
ma20 = df["close"].rolling(20).mean()
last_close = df["close"].iloc[-1]
last_ma20 = ma20.iloc[-1]
prev_close = df["close"].iloc[-2]
prev_ma20 = ma20.iloc[-2]
# 计算信号
if last_close > last_ma20 and prev_close <= prev_ma20:
signal = "BUY" # 金叉
signal_text = "🟢 买入信号"
signal_desc = "收盘价上穿20日均线金叉买入"
elif last_close < last_ma20 and prev_close >= prev_ma20:
signal = "SELL" # 死叉
signal_text = "🔴 卖出信号"
signal_desc = "收盘价下穿20日均线死叉卖出"
elif last_close > last_ma20:
signal = "HOLD_LONG"
signal_text = "🟢 持仓"
signal_desc = "价格站稳均线上方,持有"
else:
signal = "HOLD_SHORT"
signal_text = "🔴 空仓"
signal_desc = "价格跌破均线,保持空仓"
pct_above = (last_close - last_ma20) / last_ma20 * 100
macd_fast = df["close"].ewm(span=12).mean().iloc[-1]
macd_slow = df["close"].ewm(span=26).mean().iloc[-1]
macd_val = macd_fast - macd_slow
signal_line = pd.Series(df["close"]).ewm(span=12).mean().iloc[-1] - pd.Series(df["close"]).ewm(span=26).mean().iloc[-1]
signal_line = pd.Series(pd.Series(df["close"]).ewm(span=12).mean() - pd.Series(df["close"]).ewm(span=26).mean()).ewm(span=9).mean().iloc[-1]
return {
"code": code,
"name": name,
"date": str(df.index[-1].date()),
"close": last_close,
"ma20": last_ma20,
"pct_above_ma": pct_above,
"signal": signal,
"signal_text": signal_text,
"signal_desc": signal_desc,
"macd": macd_val,
"signal_line": signal_line,
}
def get_price_simple(code):
"""快速获取当前价格"""
mc = f"sh{code}" if code.startswith("6") else f"sz{code}"
try:
url = f"https://qt.gtimg.cn/q={mc}"
data = urllib.request.urlopen(url, timeout=5).read()
text = data.decode("gbk")
parts = text.split("~")
if len(parts) > 10:
return {
"name": parts[1],
"code": parts[2],
"price": float(parts[3]),
"yesterday": float(parts[4]),
"change": float(parts[3]) - float(parts[4]),
"change_pct": (float(parts[3]) - float(parts[4])) / float(parts[4]) * 100,
}
except Exception:
pass
return None
def send_daily_signal(code="000858", auto_trade=False):
"""发送每日信号到飞书,可选自动执行模拟交易"""
info = get_price_simple(code)
sig = compute_signal(code)
name = STOCK_NAMES.get(code, code)
if info is None:
send_feishu(f"⚠️ 无法获取{name}({code})行情数据")
return
if sig is None:
send_feishu(f"⚠️ 无法计算{name}({code})技术信号")
return
change_emoji = "📈" if info["change"] > 0 else "📉"
msg = f"""🍶 {name}({code}) 每日信号
{change_emoji} 今日: {info['price']:.2f} ({info['change']:+.2f}, {info['change_pct']:+.2f}%)
{sig['signal_text']}
{sig['signal_desc']}
均线状态:
• 收盘价: {sig['close']:.2f}
• MA20: {sig['ma20']:.2f}
• 价格偏离: {sig['pct_above_ma']:+.2f}%
• MACD: {sig['macd']:+.2f} vs Signal {sig['signal_line']:.2f}
{'✅ 可买入' if sig['signal'] == 'BUY' else '❌ 继续观察' if sig['signal'] == 'HOLD_SHORT' else '⏸️ 持仓观望'}
生成: {datetime.now().strftime('%Y-%m-%d %H:%M')}
小唯股票投研 · MA20突破策略"""
# 信号 → 模拟账户自动执行(打通两套系统)
if auto_trade:
import stock_paper
# v2弱势行业金叉半仓参与2026-08-01 回测优化:全拦截损失 α,半仓最优)
industry = STOCK_NAMES.get(code, "")
weak_industry = {"白酒", "医药", "通信", "汽车", "地产"} # 与 stock_portfolio.WEAK_TREND_INDUSTRIES 同步
if sig["signal"] == "BUY" and industry in weak_industry:
action, detail = ("BUY_HALF", f"弱势行业({industry}动量负)金叉,半仓买入")
msg += f"\n\n📝 模拟账户: [⚠️ {action}] {detail}"
else:
action, detail = stock_paper.execute_signal(sig)
msg += f"\n\n📝 模拟账户: [{action}] {detail}"
send_feishu(msg)
print(msg)
if __name__ == "__main__":
code = "000858"
auto_trade = False
if "--code" in sys.argv:
idx = sys.argv.index("--code")
if idx + 1 < len(sys.argv):
code = sys.argv[idx + 1]
if "--paper" in sys.argv:
auto_trade = True
if "--watch" in sys.argv:
print("监控模式: 每60秒检查一次 (Ctrl+C退出)")
import time
while True:
send_daily_signal(code, auto_trade)
time.sleep(60)
else:
send_daily_signal(code, auto_trade)