Files
tushare/lite_api_server.py
BrunoGao 904b0698ab feat: 新增持续学习系统与Docker部署优化
- 新增持续学习系统完整架构
- 添加AI训练自动化调度器和模型版本管理
- 优化Docker配置支持实时系统
- 新增数据源管理系统集成
- 改进WebSocket和API端口配置
- 添加系统监控和健康检查功能
2025-09-18 22:08:45 +08:00

504 lines
16 KiB
Python

#!/usr/bin/env python3
"""
轻量级API服务器 - 无数据库依赖版本
提供基本的股票查询和AI分析接口
"""
from flask import Flask, request, jsonify, render_template, send_from_directory, redirect, url_for
from flask_cors import CORS
import requests
import logging
import json
import time
from datetime import datetime
import random
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# 模拟股票数据
MOCK_STOCKS = {
'000001.SZ': {
'ts_code': '000001.SZ',
'name': '平安银行',
'price': 12.50,
'change': 0.12,
'pct_chg': 0.97,
'volume': 45823000,
'market_cap': 245.6,
'pe': 5.2,
'pb': 0.8,
'rsi': 65.4,
'macd': 0.08,
'ma5': 12.35,
'ma20': 12.8
},
'600519.SH': {
'ts_code': '600519.SH',
'name': '贵州茅台',
'price': 1786.30,
'change': -15.20,
'pct_chg': -0.84,
'volume': 232838,
'market_cap': 2245.8,
'pe': 32.5,
'pb': 8.9,
'rsi': 79.2,
'macd': -0.12,
'ma5': 1801.5,
'ma20': 1820.7
},
'000002.SZ': {
'ts_code': '000002.SZ',
'name': '万科A',
'price': 8.45,
'change': 0.08,
'pct_chg': 0.96,
'volume': 89456000,
'market_cap': 925.4,
'pe': 6.8,
'pb': 0.9,
'rsi': 58.3,
'macd': 0.02,
'ma5': 8.38,
'ma20': 8.52
},
'002594.SZ': {
'ts_code': '002594.SZ',
'name': '比亚迪',
'price': 245.80,
'change': 5.60,
'pct_chg': 2.33,
'volume': 15234000,
'market_cap': 715.6,
'pe': 28.9,
'pb': 4.2,
'rsi': 72.1,
'macd': 0.18,
'ma5': 242.3,
'ma20': 238.9
},
'601398.SH': {
'ts_code': '601398.SH',
'name': '工商银行',
'price': 4.32,
'change': -0.02,
'pct_chg': -0.46,
'volume': 45678000,
'market_cap': 1545.8,
'pe': 4.5,
'pb': 0.6,
'rsi': 45.8,
'macd': -0.01,
'ma5': 4.35,
'ma20': 4.38
}
}
def call_ollama_api(prompt: str, timeout: int = 30):
"""调用Ollama API"""
try:
response = requests.post(
'http://localhost:11434/api/generate',
json={
'model': 'ljwx-stock-comprehensive-20250803_2109',
'prompt': prompt,
'stream': False
},
timeout=timeout
)
if response.status_code == 200:
result = response.json()
return result.get('response', '无法获取AI分析结果')
else:
return f"AI服务暂时不可用 (状态码: {response.status_code})"
except requests.exceptions.Timeout:
return "AI分析超时,请稍后重试"
except Exception as e:
logger.error(f"调用Ollama API失败: {e}")
return f"AI服务错误: {str(e)}"
@app.route('/')
def index():
"""首页"""
return {
'service': 'ljwx-stock轻量级API服务',
'version': '1.0.0',
'timestamp': datetime.now().isoformat(),
'endpoints': {
'/api/health': 'GET - 健康检查',
'/api/stocks': 'GET - 获取股票列表',
'/api/stocks/<stock_code>': 'GET - 获取单只股票信息',
'/api/ai/analyze': 'POST - AI智能分析',
'/api/technical/<stock_code>': 'GET - 技术指标',
'/api/recommendations': 'GET - 获取推荐股票'
}
}
@app.route('/api/health')
def health_check():
"""健康检查"""
return {
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'services': {
'api_server': 'running',
'ollama_ai': 'available' if test_ollama_connection() else 'unavailable',
'mock_data': 'loaded',
'stock_count': len(MOCK_STOCKS)
}
}
def test_ollama_connection():
"""测试Ollama连接"""
try:
response = requests.get('http://localhost:11434/api/tags', timeout=3)
return response.status_code == 200
except:
return False
@app.route('/api/stocks')
def get_stocks():
"""获取股票列表"""
search = request.args.get('search', '').lower()
limit = int(request.args.get('limit', 50))
results = []
for code, data in MOCK_STOCKS.items():
if not search or search in code.lower() or search in data['name'].lower():
results.append({
'ts_code': code,
'name': data['name'],
'price': data['price'],
'change': data['change'],
'pct_chg': data['pct_chg']
})
return {
'data': results[:limit],
'total': len(results),
'timestamp': datetime.now().isoformat()
}
@app.route('/api/stocks/<stock_code>')
def get_stock_detail(stock_code):
"""获取单只股票详情"""
if stock_code not in MOCK_STOCKS:
return {'error': f'股票代码 {stock_code} 不存在'}, 404
# 添加一些随机波动模拟实时数据
data = MOCK_STOCKS[stock_code].copy()
change_factor = random.uniform(-0.002, 0.002)
data['price'] = round(data['price'] * (1 + change_factor), 2)
data['rsi'] = round(data['rsi'] + random.uniform(-1, 1), 1)
return {
'data': data,
'timestamp': datetime.now().isoformat()
}
@app.route('/api/technical/<stock_code>')
def get_technical_indicators(stock_code):
"""获取技术指标"""
if stock_code not in MOCK_STOCKS:
return {'error': f'股票代码 {stock_code} 不存在'}, 404
data = MOCK_STOCKS[stock_code]
return {
'ts_code': stock_code,
'name': data['name'],
'indicators': {
'rsi': data['rsi'],
'macd': data['macd'],
'ma5': data['ma5'],
'ma20': data['ma20'],
'pe': data['pe'],
'pb': data['pb']
},
'signals': {
'rsi_signal': 'overbought' if data['rsi'] > 70 else 'oversold' if data['rsi'] < 30 else 'normal',
'macd_signal': 'bullish' if data['macd'] > 0 else 'bearish',
'ma_signal': 'bullish' if data['ma5'] > data['ma20'] else 'bearish'
},
'timestamp': datetime.now().isoformat()
}
@app.route('/api/ai/analyze', methods=['POST'])
def ai_analyze():
"""AI智能分析"""
try:
data = request.get_json()
if not data or 'query' not in data:
return {'error': '请提供查询内容'}, 400
query = data['query']
stock_code = data.get('stock_code', '')
# 构建分析提示词
if stock_code and stock_code in MOCK_STOCKS:
stock_info = MOCK_STOCKS[stock_code]
prompt = f"""
请分析股票 {stock_info['name']}({stock_code}) 的投资价值。
当前数据:
- 价格: {stock_info['price']}元
- 涨跌: {stock_info['change']}({stock_info['pct_chg']}%)
- RSI: {stock_info['rsi']}
- MACD: {stock_info['macd']}
- PE: {stock_info['pe']}
- PB: {stock_info['pb']}
用户问题: {query}
请提供专业的技术分析和投资建议。
"""
else:
prompt = query
start_time = time.time()
ai_response = call_ollama_api(prompt)
response_time = time.time() - start_time
return {
'query': query,
'response': ai_response,
'response_time': response_time,
'timestamp': datetime.now().isoformat(),
'model': 'ljwx-stock-comprehensive-20250803_2109'
}
except Exception as e:
logger.error(f"AI分析失败: {e}")
return {'error': f'AI分析服务错误: {str(e)}'}, 500
@app.route('/api/recommendations')
def get_recommendations():
"""获取推荐股票"""
recommendations = []
for code, data in MOCK_STOCKS.items():
# 简单的推荐逻辑
score = 0
reasons = []
# RSI评分
if 30 < data['rsi'] < 70:
score += 30
reasons.append('RSI处于正常区间')
elif data['rsi'] < 30:
score += 50
reasons.append('RSI超卖,可能反弹')
# MACD评分
if data['macd'] > 0:
score += 25
reasons.append('MACD金叉')
# PE评分
if data['pe'] < 15:
score += 25
reasons.append('PE较低,估值合理')
# PB评分
if data['pb'] < 2:
score += 20
reasons.append('PB较低')
recommendations.append({
'ts_code': code,
'name': data['name'],
'price': data['price'],
'score': min(score, 100),
'reasons': reasons,
'recommendation': 'buy' if score > 70 else 'hold' if score > 40 else 'sell'
})
# 按评分排序
recommendations.sort(key=lambda x: x['score'], reverse=True)
return {
'data': recommendations,
'timestamp': datetime.now().isoformat(),
'algorithm': 'basic_technical_scoring'
}
@app.route('/api/websocket/test')
def websocket_test_page():
"""WebSocket测试页面"""
html = '''
<!DOCTYPE html>
<html>
<head>
<title>ljwx-stock WebSocket测试</title>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 800px; margin: 0 auto; }
.messages { height: 400px; border: 1px solid #ccc; overflow-y: scroll; padding: 10px; margin: 10px 0; }
.controls { margin: 10px 0; }
input, button, select { margin: 5px; padding: 5px; }
.message { margin: 5px 0; padding: 5px; border-left: 3px solid #007cba; }
.error { border-left-color: #d32f2f; }
.success { border-left-color: #388e3c; }
</style>
</head>
<body>
<div class="container">
<h1>ljwx-stock WebSocket测试</h1>
<div class="controls">
<button onclick="connect()">连接WebSocket</button>
<button onclick="disconnect()">断开连接</button>
<span id="status">未连接</span>
</div>
<div class="controls">
<select id="stockSelect">
<option value="000001.SZ">平安银行</option>
<option value="600519.SH">贵州茅台</option>
<option value="000002.SZ">万科A</option>
<option value="002594.SZ">比亚迪</option>
<option value="601398.SH">工商银行</option>
</select>
<button onclick="subscribe()">订阅</button>
<button onclick="unsubscribe()">取消订阅</button>
</div>
<div class="controls">
<input type="text" id="aiQuery" placeholder="输入AI分析问题..." style="width: 300px;">
<button onclick="sendAiQuery()">AI分析</button>
</div>
<div class="controls">
<button onclick="getStocks()">获取股票列表</button>
<button onclick="clearMessages()">清空消息</button>
</div>
<div id="messages" class="messages"></div>
</div>
<script>
let ws = null;
function addMessage(msg, type = 'info') {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = 'message ' + type;
div.innerHTML = '<strong>' + new Date().toLocaleTimeString() + ':</strong> ' + msg;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function connect() {
if (ws) {
ws.close();
}
ws = new WebSocket('ws://localhost:8765');
ws.onopen = function() {
document.getElementById('status').textContent = '已连接';
addMessage('WebSocket连接成功', 'success');
};
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
addMessage(JSON.stringify(data, null, 2));
} catch (e) {
addMessage(event.data);
}
};
ws.onclose = function() {
document.getElementById('status').textContent = '已断开';
addMessage('WebSocket连接关闭', 'error');
};
ws.onerror = function(error) {
addMessage('WebSocket错误: ' + error, 'error');
};
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
}
function subscribe() {
const stockCode = document.getElementById('stockSelect').value;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'subscribe',
stock_code: stockCode
}));
} else {
addMessage('请先连接WebSocket', 'error');
}
}
function unsubscribe() {
const stockCode = document.getElementById('stockSelect').value;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'unsubscribe',
stock_code: stockCode
}));
} else {
addMessage('请先连接WebSocket', 'error');
}
}
function sendAiQuery() {
const query = document.getElementById('aiQuery').value;
if (!query.trim()) {
addMessage('请输入查询内容', 'error');
return;
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'ai_query',
query: query
}));
} else {
addMessage('请先连接WebSocket', 'error');
}
}
function getStocks() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
action: 'get_stocks'
}));
} else {
addMessage('请先连接WebSocket', 'error');
}
}
function clearMessages() {
document.getElementById('messages').innerHTML = '';
}
</script>
</body>
</html>
'''
return html
if __name__ == '__main__':
print("🚀 启动ljwx-stock轻量级API服务器")
print("📡 访问地址: http://localhost:5005")
print("🔧 WebSocket测试: http://localhost:5005/api/websocket/test")
app.run(host='0.0.0.0', port=5005, debug=True)