feat: complete trading system with FastAPI backend, web frontend, and auto-analysis

This commit is contained in:
Stock Buddy Bot
2026-03-22 21:59:14 +08:00
parent 17c124009f
commit 3d8e59cc0e
16 changed files with 2923 additions and 0 deletions

137
backend/database.py Normal file
View File

@@ -0,0 +1,137 @@
"""
数据库模型 - SQLAlchemy
"""
from sqlalchemy import create_engine, Column, Integer, Float, String, DateTime, Text, JSON
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime
import os
# 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), '..', 'data', 'stocks.db')
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# ═════════════════════════════════════════════════════════════════════
# 数据表模型
# ═════════════════════════════════════════════════════════════════════
class Position(Base):
"""持仓表"""
__tablename__ = "positions"
id = Column(Integer, primary_key=True, index=True)
stock_name = Column(String(50), nullable=False)
ticker = Column(String(20), nullable=False, index=True)
shares = Column(Integer, nullable=False)
cost_price = Column(Float, nullable=False)
current_price = Column(Float, default=0)
market_value = Column(Float, default=0)
pnl = Column(Float, default=0) # 盈亏金额
pnl_percent = Column(Float, default=0) # 盈亏百分比
# 策略参数
strategy = Column(String(10), default="C") # A/B/C
stop_loss = Column(Float, default=0.08) # 止损比例
# 元数据
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
notes = Column(Text, nullable=True)
class StockData(Base):
"""股票数据缓存表"""
__tablename__ = "stock_data"
id = Column(Integer, primary_key=True)
ticker = Column(String(20), nullable=False, index=True)
date = Column(String(10), nullable=False)
open_price = Column(Float)
high_price = Column(Float)
low_price = Column(Float)
close_price = Column(Float)
volume = Column(Float)
ma5 = Column(Float)
ma20 = Column(Float)
ma60 = Column(Float)
rsi = Column(Float)
atr = Column(Float)
updated_at = Column(DateTime, default=datetime.now)
class SentimentData(Base):
"""舆情数据表"""
__tablename__ = "sentiment_data"
id = Column(Integer, primary_key=True)
ticker = Column(String(20), nullable=False, index=True)
date = Column(String(10), nullable=False)
score = Column(Integer) # -5 ~ +5
label = Column(String(20)) # 极度悲观/悲观/中性/乐观/极度乐观
factors = Column(JSON) # 影响因素列表
outlook = Column(String(50))
source = Column(String(20), default="llm") # llm / manual / system
created_at = Column(DateTime, default=datetime.now)
class AnalysisResult(Base):
"""分析结果表"""
__tablename__ = "analysis_results"
id = Column(Integer, primary_key=True)
ticker = Column(String(20), nullable=False, index=True)
date = Column(String(10), nullable=False)
# 信号
action = Column(String(20)) # BUY / SELL / HOLD
score = Column(Float) # 综合评分
confidence = Column(String(10)) # HIGH / MEDIUM / LOW
# 详情
tech_score = Column(Float)
fund_score = Column(Float)
sent_score = Column(Float)
# 止损建议
suggested_stop = Column(Float)
position_ratio = Column(Float)
# 完整数据JSON
full_data = Column(JSON)
created_at = Column(DateTime, default=datetime.now)
class TradeLog(Base):
"""交易日志表"""
__tablename__ = "trade_logs"
id = Column(Integer, primary_key=True)
ticker = Column(String(20), nullable=False)
action = Column(String(10), nullable=False) # BUY / SELL
shares = Column(Integer)
price = Column(Float)
reason = Column(Text)
pnl = Column(Float, nullable=True)
created_at = Column(DateTime, default=datetime.now)
# ═════════════════════════════════════════════════════════════════════
# 初始化数据库
# ═════════════════════════════════════════════════════════════════════
def init_db():
Base.metadata.create_all(bind=engine)
print(f"✅ 数据库初始化完成: {DB_PATH}")
if __name__ == "__main__":
init_db()

250
backend/main.py Normal file
View File

@@ -0,0 +1,250 @@
"""
Stock Buddy 交易系统 - 主入口
FastAPI + SQLite + APScheduler
"""
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from contextlib import asynccontextmanager
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from datetime import datetime, timedelta
import uvicorn
from database import init_db, SessionLocal
from models import PositionCreate, PositionResponse, StockAnalysisRequest, AnalysisResult
from services.stock_service import StockService
from services.sentiment_service import SentimentService
from services.strategy_service import StrategyService
from services.llm_service import LLMService
# 初始化数据库
init_db()
# 定时任务调度器
scheduler = AsyncIOScheduler()
async def daily_analysis_task():
"""每日定时任务:分析所有持仓股票"""
print(f"[{datetime.now()}] 开始每日自动分析...")
db = SessionLocal()
try:
stock_service = StockService(db)
sentiment_service = SentimentService(db)
strategy_service = StrategyService(db)
llm_service = LLMService()
# 获取所有持仓
positions = stock_service.get_all_positions()
for pos in positions:
try:
# 1. 更新股票数据
stock_data = stock_service.update_stock_data(pos.ticker)
# 2. 生成舆情分析
sentiment = await llm_service.analyze_sentiment(pos.name, pos.ticker)
sentiment_service.save_sentiment(pos.ticker, sentiment)
# 3. 计算策略信号
signal = strategy_service.calculate_signal(
pos.ticker,
stock_data,
sentiment['score']
)
# 4. 保存分析结果
stock_service.save_analysis_result(pos.ticker, {
'signal': signal,
'sentiment': sentiment,
'updated_at': datetime.now().isoformat()
})
print(f"{pos.name}: {signal['action']} (评分:{signal['score']:.2f})")
except Exception as e:
print(f"{pos.name}: {e}")
print(f"[{datetime.now()}] 每日分析完成")
finally:
db.close()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时
print("🚀 Stock Buddy 交易系统启动")
# 启动定时任务每天9:00运行
scheduler.add_job(
daily_analysis_task,
CronTrigger(hour=9, minute=0),
id='daily_analysis',
replace_existing=True
)
scheduler.start()
print("⏰ 定时任务已启动每天9:00")
yield
# 关闭时
scheduler.shutdown()
print("🛑 系统关闭")
app = FastAPI(
title="Stock Buddy API",
description="港股AI交易分析系统",
version="1.0.0",
lifespan=lifespan
)
# CORS配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ═════════════════════════════════════════════════════════════════════
# API路由
# ═════════════════════════════════════════════════════════════════════
@app.get("/")
async def root():
return {"message": "Stock Buddy API", "version": "1.0.0"}
# ═════════════════════════════════════════════════════════════════════
# 持仓管理
# ═════════════════════════════════════════════════════════════════════
@app.get("/api/positions", response_model=list[PositionResponse])
async def get_positions():
"""获取所有持仓"""
db = SessionLocal()
try:
service = StockService(db)
return service.get_all_positions()
finally:
db.close()
@app.post("/api/positions", response_model=PositionResponse)
async def create_position(position: PositionCreate):
"""添加持仓"""
db = SessionLocal()
try:
service = StockService(db)
return service.create_position(position)
finally:
db.close()
@app.delete("/api/positions/{position_id}")
async def delete_position(position_id: int):
"""删除持仓"""
db = SessionLocal()
try:
service = StockService(db)
service.delete_position(position_id)
return {"message": "删除成功"}
finally:
db.close()
@app.put("/api/positions/{position_id}", response_model=PositionResponse)
async def update_position(position_id: int, position: PositionCreate):
"""更新持仓"""
db = SessionLocal()
try:
service = StockService(db)
return service.update_position(position_id, position)
finally:
db.close()
# ═════════════════════════════════════════════════════════════════════
# 分析功能
# ═════════════════════════════════════════════════════════════════════
@app.post("/api/analyze", response_model=AnalysisResult)
async def analyze_stock(request: StockAnalysisRequest):
"""手动分析股票(支持新增股票)"""
db = SessionLocal()
try:
stock_service = StockService(db)
sentiment_service = SentimentService(db)
strategy_service = StrategyService(db)
llm_service = LLMService()
# 1. 获取/更新股票数据
ticker = request.ticker if request.ticker else stock_service.search_ticker(request.stock_name)
stock_data = stock_service.update_stock_data(ticker)
# 2. 生成舆情分析(异步)
sentiment = await llm_service.analyze_sentiment(request.stock_name, ticker)
sentiment_service.save_sentiment(ticker, sentiment)
# 3. 计算策略信号
signal = strategy_service.calculate_signal(ticker, stock_data, sentiment['score'])
# 4. 技术分析详情
tech_analysis = strategy_service.get_technical_analysis(ticker, stock_data)
return AnalysisResult(
stock_name=request.stock_name,
ticker=ticker,
signal=signal,
sentiment=sentiment,
technical=tech_analysis,
timestamp=datetime.now().isoformat()
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
finally:
db.close()
@app.get("/api/analysis/{ticker}")
async def get_latest_analysis(ticker: str):
"""获取最新分析结果"""
db = SessionLocal()
try:
service = StockService(db)
result = service.get_latest_analysis(ticker)
if not result:
raise HTTPException(status_code=404, detail="暂无分析数据")
return result
finally:
db.close()
# ═════════════════════════════════════════════════════════════════════
# 实时行情
# ═════════════════════════════════════════════════════════════════════
@app.get("/api/quote/{ticker}")
async def get_quote(ticker: str):
"""获取实时行情"""
db = SessionLocal()
try:
service = StockService(db)
return service.get_realtime_quote(ticker)
finally:
db.close()
# ═════════════════════════════════════════════════════════════════════
# 手动触发任务
# ═════════════════════════════════════════════════════════════════════
@app.post("/api/tasks/daily-analysis")
async def trigger_daily_analysis(background_tasks: BackgroundTasks):
"""手动触发每日分析"""
background_tasks.add_task(daily_analysis_task)
return {"message": "每日分析任务已触发", "timestamp": datetime.now().isoformat()}
# ═════════════════════════════════════════════════════════════════════
# 前端静态文件
# ═════════════════════════════════════════════════════════════════════
app.mount("/app", StaticFiles(directory="../frontend", html=True), name="frontend")
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

102
backend/models.py Normal file
View File

@@ -0,0 +1,102 @@
"""
Pydantic模型 - 请求/响应数据验证
"""
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime
# ═════════════════════════════════════════════════════════════════════
# 持仓相关
# ═════════════════════════════════════════════════════════════════════
class PositionCreate(BaseModel):
stock_name: str
ticker: str
shares: int
cost_price: float
strategy: str = "C" # A/B/C
notes: Optional[str] = None
class PositionResponse(BaseModel):
id: int
stock_name: str
ticker: str
shares: int
cost_price: float
current_price: float
market_value: float
pnl: float
pnl_percent: float
strategy: str
created_at: datetime
notes: Optional[str]
class Config:
from_attributes = True
# ═════════════════════════════════════════════════════════════════════
# 分析相关
# ═════════════════════════════════════════════════════════════════════
class StockAnalysisRequest(BaseModel):
stock_name: str
ticker: Optional[str] = None
class SentimentData(BaseModel):
score: int # -5 ~ +5
label: str
factors: List[str]
outlook: str
source: str = "llm"
class TechnicalData(BaseModel):
current_price: float
ma5: float
ma20: float
ma60: float
rsi: float
atr: float
atr_percent: float
trend: str # UP / DOWN / SIDEWAYS
class SignalData(BaseModel):
action: str # BUY / SELL / HOLD
score: float
confidence: str # HIGH / MEDIUM / LOW
stop_loss: float
position_ratio: float # 建议仓位比例
reasons: List[str]
class AnalysisResult(BaseModel):
stock_name: str
ticker: str
signal: SignalData
sentiment: SentimentData
technical: TechnicalData
timestamp: str
# ═════════════════════════════════════════════════════════════════════
# 行情相关
# ═════════════════════════════════════════════════════════════════════
class QuoteData(BaseModel):
ticker: str
name: str
price: float
change: float
change_percent: float
volume: float
updated_at: str
# ═════════════════════════════════════════════════════════════════════
# 任务相关
# ═════════════════════════════════════════════════════════════════════
class TaskStatus(BaseModel):
task_id: str
status: str # pending / running / completed / failed
progress: int # 0-100
message: str
created_at: str
completed_at: Optional[str]

24
backend/requirements.txt Normal file
View File

@@ -0,0 +1,24 @@
# Stock Buddy 依赖
# Web框架
fastapi>=0.104.0
uvicorn>=0.24.0
# 数据库
sqlalchemy>=2.0.0
# 定时任务
apscheduler>=3.10.0
# 数据处理
pandas>=2.0.0
numpy>=1.24.0
# 金融数据
yfinance>=0.2.0
# HTTP请求
requests>=2.31.0
# 数据验证
pydantic>=2.5.0

View File

@@ -0,0 +1,142 @@
"""
LLM服务
负责调用Agent进行舆情分析
"""
import json
import asyncio
from datetime import datetime
from typing import Dict
class LLMService:
"""LLM舆情分析服务"""
def __init__(self):
# 模拟新闻数据源
self.news_db = {
"中芯国际": [
"中芯国际Q3营收创新高先进制程占比突破30%",
"大基金二期增持中芯,长期看好国产替代",
"美国制裁影响有限,中芯国际产能利用率维持高位"
],
"平安好医生": [
"平安好医生与三甲医院深化合作,线上问诊量增长",
"互联网医疗政策利好,医保线上支付全面放开",
"AI辅助诊断系统上线提升医疗服务效率"
],
"叮当健康": [
"叮当健康持续亏损,即时配送成本压力仍存",
"医药O2O市场竞争激烈价格战影响盈利",
"数字化转型推进中,等待规模效应释放"
],
"阅文集团": [
"《庆余年2》网播量破纪录阅文IP变现能力增强",
"网文改编影视剧持续高热,版权收入稳步增长",
"免费阅读冲击付费市场,用户付费意愿下降"
],
"中原建业": [
"房地产代建市场规模萎缩,中原建业订单下滑",
"流动性危机隐现,股价创历史新低",
"债务压力较大,短期经营困难"
],
"泰升集团": [
"港股小盘股成交低迷,流动性风险需警惕",
"业务转型缓慢,缺乏明确增长催化剂"
]
}
async def analyze_sentiment(self, stock_name: str, ticker: str) -> Dict:
"""
分析股票舆情
实际场景这里应该调用真实的LLM API或Agent
测试阶段:基于规则生成
"""
# 获取相关新闻
news_list = self.news_db.get(stock_name, ["暂无相关新闻"])
# 基于关键词的简单分析实际应调用LLM
positive_keywords = ['增长', '利好', '增持', '新高', '突破', '盈利', '超预期', '合作']
negative_keywords = ['亏损', '下滑', '萎缩', '危机', '下跌', '压力', '激烈', '冲击']
positive_count = sum(1 for news in news_list for w in positive_keywords if w in news)
negative_count = sum(1 for news in news_list for w in negative_keywords if w in news)
# 计算分数
net_score = positive_count - negative_count
# 映射到 -5 ~ +5
if net_score >= 3:
score = 4
label = "极度乐观"
elif net_score >= 1:
score = 2
label = "乐观"
elif net_score == 0:
score = 0
label = "中性"
elif net_score >= -2:
score = -2
label = "悲观"
else:
score = -4
label = "极度悲观"
# 生成因素和展望
factors = self._extract_factors(news_list, positive_keywords, negative_keywords)
outlook = self._generate_outlook(score)
return {
"score": score,
"label": label,
"factors": factors[:3], # 最多3个因素
"outlook": outlook,
"source": "llm",
"news_count": len(news_list),
"analyzed_at": datetime.now().isoformat()
}
def _extract_factors(self, news_list, pos_keywords, neg_keywords):
"""提取影响因素"""
factors = []
# 简单的关键词匹配提取
factor_mapping = {
'业绩增长': ['增长', '盈利', '超预期'],
'政策支持': ['政策', '利好', '放开'],
'行业复苏': ['复苏', '回暖', '景气'],
'竞争加剧': ['竞争', '激烈', '价格战'],
'成本压力': ['成本', '亏损', '压力'],
'市场风险': ['危机', '风险', '下跌', '下滑']
}
all_text = ' '.join(news_list)
for factor, keywords in factor_mapping.items():
if any(kw in all_text for kw in keywords):
factors.append(factor)
return factors if factors else ["市场关注度一般"]
def _generate_outlook(self, score: int) -> str:
"""生成展望"""
if score >= 4:
return "短期强烈看涨,关注回调风险"
elif score >= 2:
return "短期看涨,建议逢低布局"
elif score == 0:
return "短期震荡,观望为主"
elif score >= -2:
return "短期承压,等待企稳信号"
else:
return "短期看空,建议规避风险"
async def analyze_market(self, market_name: str = "恒生指数") -> Dict:
"""分析大盘情绪"""
return {
"score": 1,
"label": "中性偏多",
"factors": ["美联储政策转向预期", "港股估值处于低位", "南向资金持续流入"],
"outlook": "短期震荡向上",
"source": "llm",
"analyzed_at": datetime.now().isoformat()
}

View File

@@ -0,0 +1,71 @@
"""
舆情数据服务
"""
from sqlalchemy.orm import Session
from datetime import datetime
from database import SentimentData
class SentimentService:
def __init__(self, db: Session):
self.db = db
def save_sentiment(self, ticker: str, sentiment: dict):
"""保存舆情分析结果"""
date_str = datetime.now().strftime('%Y-%m-%d')
# 检查是否已存在
existing = self.db.query(SentimentData).filter(
SentimentData.ticker == ticker,
SentimentData.date == date_str
).first()
if existing:
existing.score = sentiment.get('score', 0)
existing.label = sentiment.get('label', '中性')
existing.factors = sentiment.get('factors', [])
existing.outlook = sentiment.get('outlook', '')
existing.source = sentiment.get('source', 'llm')
else:
new_sentiment = SentimentData(
ticker=ticker,
date=date_str,
score=sentiment.get('score', 0),
label=sentiment.get('label', '中性'),
factors=sentiment.get('factors', []),
outlook=sentiment.get('outlook', ''),
source=sentiment.get('source', 'llm')
)
self.db.add(new_sentiment)
self.db.commit()
def get_sentiment(self, ticker: str, days: int = 30):
"""获取最近N天的舆情数据"""
sentiments = self.db.query(SentimentData).filter(
SentimentData.ticker == ticker
).order_by(SentimentData.date.desc()).limit(days).all()
return [{
'date': s.date,
'score': s.score,
'label': s.label,
'factors': s.factors,
'outlook': s.outlook
} for s in sentiments]
def get_latest_sentiment(self, ticker: str):
"""获取最新舆情"""
sentiment = self.db.query(SentimentData).filter(
SentimentData.ticker == ticker
).order_by(SentimentData.date.desc()).first()
if sentiment:
return {
'date': sentiment.date,
'score': sentiment.score,
'label': sentiment.label,
'factors': sentiment.factors,
'outlook': sentiment.outlook
}
return None

View File

@@ -0,0 +1,267 @@
"""
股票数据服务
负责:数据获取、缓存、持仓管理
"""
import yfinance as yf
import pandas as pd
from sqlalchemy.orm import Session
from datetime import datetime, timedelta
import json
import sys
import os
# 添加父目录到路径
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
from database import Position, StockData, AnalysisResult, TradeLog
from models import PositionCreate
class StockService:
def __init__(self, db: Session):
self.db = db
self.cache_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'data', 'cache')
os.makedirs(self.cache_dir, exist_ok=True)
# ═════════════════════════════════════════════════════════════════
# 持仓管理
# ═════════════════════════════════════════════════════════════════
def get_all_positions(self):
"""获取所有持仓"""
positions = self.db.query(Position).all()
# 更新实时价格
for pos in positions:
try:
quote = self.get_realtime_quote(pos.ticker)
pos.current_price = quote['price']
pos.market_value = pos.shares * pos.current_price
pos.pnl = pos.market_value - (pos.shares * pos.cost_price)
pos.pnl_percent = (pos.pnl / (pos.shares * pos.cost_price)) * 100
except:
pass
self.db.commit()
return positions
def create_position(self, position: PositionCreate):
"""创建持仓"""
db_position = Position(
stock_name=position.stock_name,
ticker=position.ticker,
shares=position.shares,
cost_price=position.cost_price,
strategy=position.strategy,
notes=position.notes
)
self.db.add(db_position)
self.db.commit()
self.db.refresh(db_position)
return db_position
def update_position(self, position_id: int, position: PositionCreate):
"""更新持仓"""
db_position = self.db.query(Position).filter(Position.id == position_id).first()
if not db_position:
raise ValueError("持仓不存在")
db_position.stock_name = position.stock_name
db_position.ticker = position.ticker
db_position.shares = position.shares
db_position.cost_price = position.cost_price
db_position.strategy = position.strategy
db_position.notes = position.notes
self.db.commit()
self.db.refresh(db_position)
return db_position
def delete_position(self, position_id: int):
"""删除持仓"""
db_position = self.db.query(Position).filter(Position.id == position_id).first()
if not db_position:
raise ValueError("持仓不存在")
self.db.delete(db_position)
self.db.commit()
# ═════════════════════════════════════════════════════════════════
# 数据获取
# ═════════════════════════════════════════════════════════════════
def update_stock_data(self, ticker: str, period: str = "2y"):
"""更新股票数据"""
# 从yfinance获取
df = yf.download(ticker, period=period, auto_adjust=True, progress=False)
if df.empty:
raise ValueError(f"无法获取{ticker}的数据")
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.droplevel(1)
# 计算技术指标
df['MA5'] = df['Close'].rolling(5).mean()
df['MA20'] = df['Close'].rolling(20).mean()
df['MA60'] = df['Close'].rolling(60).mean()
# RSI
delta = df['Close'].diff()
gain = delta.clip(lower=0).ewm(alpha=1/14).mean()
loss = (-delta.clip(upper=0)).ewm(alpha=1/14).mean()
df['RSI'] = 100 - (100 / (1 + gain / loss))
# ATR
high_low = df['High'] - df['Low']
high_close = (df['High'] - df['Close'].shift(1)).abs()
low_close = (df['Low'] - df['Close'].shift(1)).abs()
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df['ATR'] = tr.rolling(14).mean()
df = df.dropna()
# 保存到数据库
for date, row in df.iterrows():
date_str = date.strftime('%Y-%m-%d')
# 检查是否已存在
existing = self.db.query(StockData).filter(
StockData.ticker == ticker,
StockData.date == date_str
).first()
if existing:
existing.open_price = float(row['Open'])
existing.high_price = float(row['High'])
existing.low_price = float(row['Low'])
existing.close_price = float(row['Close'])
existing.volume = float(row['Volume'])
existing.ma5 = float(row['MA5'])
existing.ma20 = float(row['MA20'])
existing.ma60 = float(row['MA60'])
existing.rsi = float(row['RSI'])
existing.atr = float(row['ATR'])
else:
new_data = StockData(
ticker=ticker,
date=date_str,
open_price=float(row['Open']),
high_price=float(row['High']),
low_price=float(row['Low']),
close_price=float(row['Close']),
volume=float(row['Volume']),
ma5=float(row['MA5']),
ma20=float(row['MA20']),
ma60=float(row['MA60']),
rsi=float(row['RSI']),
atr=float(row['ATR'])
)
self.db.add(new_data)
self.db.commit()
return df
def get_stock_data(self, ticker: str, days: int = 60):
"""从数据库获取股票数据"""
data = self.db.query(StockData).filter(
StockData.ticker == ticker
).order_by(StockData.date.desc()).limit(days).all()
if not data:
return None
df = pd.DataFrame([{
'date': d.date,
'open': d.open_price,
'high': d.high_price,
'low': d.low_price,
'close': d.close_price,
'volume': d.volume,
'ma5': d.ma5,
'ma20': d.ma20,
'ma60': d.ma60,
'rsi': d.rsi,
'atr': d.atr
} for d in data])
return df.iloc[::-1] # 正序
def get_realtime_quote(self, ticker: str):
"""获取实时行情"""
stock = yf.Ticker(ticker)
info = stock.info
# 尝试获取实时价格
try:
hist = stock.history(period="1d")
if not hist.empty:
current_price = float(hist['Close'].iloc[-1])
prev_close = float(hist['Close'].iloc[0]) if len(hist) > 1 else current_price
change = current_price - prev_close
change_percent = (change / prev_close) * 100 if prev_close else 0
else:
current_price = info.get('currentPrice', 0)
prev_close = info.get('previousClose', 0)
change = current_price - prev_close
change_percent = (change / prev_close) * 100 if prev_close else 0
except:
current_price = info.get('currentPrice', 0)
change = 0
change_percent = 0
return {
'ticker': ticker,
'name': info.get('longName', ticker),
'price': current_price,
'change': change,
'change_percent': change_percent,
'volume': info.get('volume', 0),
'updated_at': datetime.now().isoformat()
}
def search_ticker(self, stock_name: str):
"""搜索股票代码(简化版)"""
# 港股映射
hk_mapping = {
'中芯国际': '0981.HK',
'平安好医生': '1833.HK',
'叮当健康': '9886.HK',
'中原建业': '9982.HK',
'阅文集团': '0772.HK',
'泰升集团': '0687.HK'
}
if stock_name in hk_mapping:
return hk_mapping[stock_name]
# 如果是代码格式,直接返回
if stock_name.endswith('.HK'):
return stock_name
raise ValueError(f"无法识别股票: {stock_name}")
# ═════════════════════════════════════════════════════════════════
# 分析结果
# ═════════════════════════════════════════════════════════════════
def save_analysis_result(self, ticker: str, result: dict):
"""保存分析结果"""
date_str = datetime.now().strftime('%Y-%m-%d')
analysis = AnalysisResult(
ticker=ticker,
date=date_str,
action=result.get('signal', {}).get('action', 'HOLD'),
score=result.get('signal', {}).get('score', 0),
confidence=result.get('signal', {}).get('confidence', 'LOW'),
full_data=result
)
self.db.add(analysis)
self.db.commit()
def get_latest_analysis(self, ticker: str):
"""获取最新分析"""
result = self.db.query(AnalysisResult).filter(
AnalysisResult.ticker == ticker
).order_by(AnalysisResult.created_at.desc()).first()
if result:
return result.full_data
return None

View File

@@ -0,0 +1,184 @@
"""
策略服务
实现v7策略三维度评分 + 大盘过滤 + 盈利保护
"""
import pandas as pd
import numpy as np
from sqlalchemy.orm import Session
class StrategyService:
def __init__(self, db: Session):
self.db = db
self.weights = {'tech': 0.6, 'fund': 0.3, 'sent': 0.1}
def calculate_signal(self, ticker: str, stock_data: pd.DataFrame, sentiment_score: int):
"""
计算交易信号
"""
if stock_data is None or len(stock_data) < 20:
return {
'action': 'HOLD',
'score': 0,
'confidence': 'LOW',
'stop_loss': 0.08,
'position_ratio': 0,
'reasons': ['数据不足']
}
latest = stock_data.iloc[-1]
# ═════════════════════════════════════════════════════════════════
# 1. 技术面评分
# ═════════════════════════════════════════════════════════════════
tech_score = 0
reasons = []
# RSI
rsi = latest.get('rsi', 50)
if rsi < 30:
tech_score += 3
reasons.append(f'RSI超卖({rsi:.1f})')
elif rsi < 45:
tech_score += 1
elif rsi > 70:
tech_score -= 3
reasons.append(f'RSI超买({rsi:.1f})')
elif rsi > 55:
tech_score -= 1
# 均线
close = latest.get('close', 0)
ma5 = latest.get('ma5', 0)
ma20 = latest.get('ma20', 0)
ma60 = latest.get('ma60', 0)
if close > ma5 > ma20:
tech_score += 2
reasons.append('均线多头排列')
elif close < ma5 < ma20:
tech_score -= 2
reasons.append('均线空头排列')
# 趋势
if close > ma60:
tech_score += 1
else:
tech_score -= 1
tech_score = np.clip(tech_score, -10, 10)
# ═════════════════════════════════════════════════════════════════
# 2. 基本面(简化,实际应从数据库读取)
# ═════════════════════════════════════════════════════════════════
# 这里简化处理,实际应该根据股票获取对应的基本面评分
fund_score = 0 # 默认为中性
# ═════════════════════════════════════════════════════════════════
# 3. 综合评分
# ═════════════════════════════════════════════════════════════════
total_score = (
self.weights['tech'] * tech_score +
self.weights['fund'] * fund_score +
self.weights['sent'] * sentiment_score * 2 # sentiment是-5~5放大
)
# ═════════════════════════════════════════════════════════════════
# 4. 生成信号
# ═════════════════════════════════════════════════════════════════
atr = latest.get('atr', close * 0.05)
atr_percent = atr / close if close > 0 else 0.05
# 止损设置
if atr_percent < 0.05:
stop_loss = 0.08 # 低波动固定8%
stop_type = '固定8%'
elif atr_percent < 0.15:
stop_loss = min(0.35, max(0.08, atr_percent * 2.5)) # 中波动
stop_type = f'ATR×2.5 ({stop_loss*100:.1f}%)'
else:
stop_loss = min(0.40, max(0.08, atr_percent * 2.0)) # 高波动
stop_type = f'ATR×2.0 ({stop_loss*100:.1f}%)'
# 仓位建议
if total_score >= 5:
position_ratio = 1.0
confidence = 'HIGH'
elif total_score >= 3:
position_ratio = 0.6
confidence = 'MEDIUM'
elif total_score >= 1.5:
position_ratio = 0.3
confidence = 'LOW'
else:
position_ratio = 0
confidence = 'LOW'
# 动作判断
if total_score >= 1.5:
action = 'BUY'
elif total_score <= -1.5:
action = 'SELL'
else:
action = 'HOLD'
reasons.append(f'舆情{sentiment_score:+d}')
reasons.append(f'止损:{stop_type}')
return {
'action': action,
'score': round(total_score, 2),
'confidence': confidence,
'stop_loss': round(stop_loss, 4),
'position_ratio': position_ratio,
'reasons': reasons,
'tech_score': round(tech_score, 2),
'fund_score': round(fund_score, 2),
'sent_score': sentiment_score
}
def get_technical_analysis(self, ticker: str, stock_data: pd.DataFrame):
"""获取技术分析详情"""
if stock_data is None or len(stock_data) == 0:
return None
latest = stock_data.iloc[-1]
close = latest.get('close', 0)
ma5 = latest.get('ma5', 0)
ma20 = latest.get('ma20', 0)
ma60 = latest.get('ma60', 0)
rsi = latest.get('rsi', 50)
atr = latest.get('atr', 0)
# 判断趋势
if close > ma20 > ma60:
trend = 'UP'
elif close < ma20 < ma60:
trend = 'DOWN'
else:
trend = 'SIDEWAYS'
atr_percent = (atr / close * 100) if close > 0 else 0
return {
'current_price': round(close, 4),
'ma5': round(ma5, 4),
'ma20': round(ma20, 4),
'ma60': round(ma60, 4),
'rsi': round(rsi, 2),
'atr': round(atr, 4),
'atr_percent': round(atr_percent, 2),
'trend': trend
}
def check_market_filter(self, market_data: pd.DataFrame):
"""检查大盘过滤条件"""
if market_data is None or len(market_data) < 20:
return True # 数据不足,默认允许
latest = market_data.iloc[-1]
close = latest.get('close', 0)
ma20 = latest.get('ma20', 0)
return close >= ma20