- 新增持续学习系统完整架构 - 添加AI训练自动化调度器和模型版本管理 - 优化Docker配置支持实时系统 - 新增数据源管理系统集成 - 改进WebSocket和API端口配置 - 添加系统监控和健康检查功能
504 lines
16 KiB
Python
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) |