Files
aiagents-stock/monitor_db.py
T

573 lines
21 KiB
Python

import sqlite3
import json
from datetime import datetime
from typing import Dict, List, Optional
import os
class StockMonitorDatabase:
"""股票监测数据库管理类"""
def __init__(self, db_path: str = "stock_monitor.db"):
self.db_path = db_path
# 确保数据库所在目录存在
db_dir = os.path.dirname(self.db_path)
if db_dir and not os.path.exists(db_dir):
os.makedirs(db_dir, exist_ok=True)
self.init_database()
def init_database(self):
"""初始化数据库表结构"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建监测股票表
cursor.execute('''
CREATE TABLE IF NOT EXISTS monitored_stocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
symbol TEXT NOT NULL,
name TEXT NOT NULL,
rating TEXT NOT NULL,
entry_range TEXT NOT NULL, -- JSON格式: {"min": 10.0, "max": 12.0}
take_profit REAL,
stop_loss REAL,
current_price REAL,
last_checked TIMESTAMP,
check_interval INTEGER DEFAULT 30, -- 分钟
notification_enabled BOOLEAN DEFAULT TRUE,
trading_hours_only BOOLEAN DEFAULT TRUE, -- 仅交易时段监控
quant_enabled BOOLEAN DEFAULT FALSE, -- 量化交易开关
quant_config TEXT, -- 量化配置JSON
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 检查并添加trading_hours_only字段(兼容已有数据库)
try:
cursor.execute("SELECT trading_hours_only FROM monitored_stocks LIMIT 1")
except sqlite3.OperationalError:
cursor.execute("ALTER TABLE monitored_stocks ADD COLUMN trading_hours_only BOOLEAN DEFAULT TRUE")
print("✅ 已添加trading_hours_only字段")
# 创建价格历史表
cursor.execute('''
CREATE TABLE IF NOT EXISTS price_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_id INTEGER,
price REAL NOT NULL,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (stock_id) REFERENCES monitored_stocks (id)
)
''')
# 创建提醒记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_id INTEGER,
type TEXT NOT NULL, -- entry/take_profit/stop_loss
message TEXT NOT NULL,
triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
sent BOOLEAN DEFAULT FALSE,
FOREIGN KEY (stock_id) REFERENCES monitored_stocks (id)
)
''')
conn.commit()
conn.close()
def add_monitored_stock(self, symbol: str, name: str, rating: str,
entry_range: Dict, take_profit: float,
stop_loss: float, check_interval: int = 30,
notification_enabled: bool = True,
trading_hours_only: bool = True,
quant_enabled: bool = False,
quant_config: Dict = None) -> int:
"""添加监测股票"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
quant_config_json = json.dumps(quant_config) if quant_config else None
cursor.execute('''
INSERT INTO monitored_stocks
(symbol, name, rating, entry_range, take_profit, stop_loss, check_interval,
notification_enabled, trading_hours_only, quant_enabled, quant_config)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (symbol, name, rating, json.dumps(entry_range), take_profit, stop_loss,
check_interval, notification_enabled, trading_hours_only, quant_enabled, quant_config_json))
stock_id = cursor.lastrowid
conn.commit()
conn.close()
return stock_id
def get_monitored_stocks(self) -> List[Dict]:
"""获取所有监测股票"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT id, symbol, name, rating, entry_range, take_profit, stop_loss,
current_price, last_checked, check_interval, notification_enabled,
trading_hours_only, quant_enabled, quant_config, created_at, updated_at
FROM monitored_stocks
ORDER BY created_at DESC
''')
stocks = []
for row in cursor.fetchall():
try:
quant_config = json.loads(row[13]) if row[13] else None
entry_range = json.loads(row[4]) if row[4] else None
except (json.JSONDecodeError, TypeError) as e:
print(f"警告: 股票 {row[1]} 的JSON解析失败: {e}")
entry_range = None
quant_config = None
stocks.append({
'id': row[0],
'symbol': row[1],
'name': row[2],
'rating': row[3],
'entry_range': entry_range,
'take_profit': row[5],
'stop_loss': row[6],
'current_price': row[7],
'last_checked': row[8],
'check_interval': row[9],
'notification_enabled': bool(row[10]),
'trading_hours_only': bool(row[11]) if row[11] is not None else True,
'quant_enabled': bool(row[12]),
'quant_config': quant_config,
'created_at': row[14],
'updated_at': row[15]
})
conn.close()
return stocks
def update_stock_price(self, stock_id: int, price: float):
"""更新股票价格"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 更新当前价格
cursor.execute('''
UPDATE monitored_stocks
SET current_price = ?, last_checked = CURRENT_TIMESTAMP
WHERE id = ?
''', (price, stock_id))
# 记录价格历史
cursor.execute('''
INSERT INTO price_history (stock_id, price)
VALUES (?, ?)
''', (stock_id, price))
conn.commit()
conn.close()
def update_last_checked(self, stock_id: int):
"""仅更新最后检查时间(用于获取失败的情况)"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
UPDATE monitored_stocks
SET last_checked = CURRENT_TIMESTAMP
WHERE id = ?
''', (stock_id,))
conn.commit()
conn.close()
def has_recent_notification(self, stock_id: int, notification_type: str, minutes: int = 60) -> bool:
"""检查是否在最近X分钟内已有相同类型的通知"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT COUNT(*) FROM notifications
WHERE stock_id = ? AND type = ?
AND datetime(triggered_at) > datetime('now', '-' || ? || ' minutes')
''', (stock_id, notification_type, minutes))
count = cursor.fetchone()[0]
conn.close()
return count > 0
def add_notification(self, stock_id: int, notification_type: str, message: str):
"""添加提醒记录"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO notifications (stock_id, type, message)
VALUES (?, ?, ?)
''', (stock_id, notification_type, message))
conn.commit()
conn.close()
def get_pending_notifications(self) -> List[Dict]:
"""获取待发送的提醒"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT n.id, n.stock_id, s.symbol, s.name, n.type, n.message, n.triggered_at
FROM notifications n
JOIN monitored_stocks s ON n.stock_id = s.id
WHERE n.sent = FALSE
ORDER BY n.triggered_at
''')
notifications = []
for row in cursor.fetchall():
notifications.append({
'id': row[0],
'stock_id': row[1],
'symbol': row[2],
'name': row[3],
'type': row[4],
'message': row[5],
'triggered_at': row[6]
})
conn.close()
return notifications
def get_all_recent_notifications(self, limit: int = 10) -> List[Dict]:
"""获取最近的所有通知(包括已发送和未发送的)"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT n.id, n.stock_id, s.symbol, s.name, n.type, n.message, n.triggered_at, n.sent
FROM notifications n
JOIN monitored_stocks s ON n.stock_id = s.id
ORDER BY n.triggered_at DESC
LIMIT ?
''', (limit,))
notifications = []
for row in cursor.fetchall():
notifications.append({
'id': row[0],
'stock_id': row[1],
'symbol': row[2],
'name': row[3],
'type': row[4],
'message': row[5],
'triggered_at': row[6],
'sent': bool(row[7])
})
conn.close()
return notifications
def mark_notification_sent(self, notification_id: int):
"""标记提醒已发送"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
UPDATE notifications SET sent = TRUE WHERE id = ?
''', (notification_id,))
conn.commit()
conn.close()
def mark_all_notifications_sent(self):
"""标记所有通知为已读"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('UPDATE notifications SET sent = TRUE WHERE sent = FALSE')
conn.commit()
conn.close()
return cursor.rowcount
def clear_all_notifications(self):
"""清空所有通知"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('DELETE FROM notifications')
conn.commit()
conn.close()
return cursor.rowcount
def remove_monitored_stock(self, stock_id: int):
"""移除监测股票"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 删除相关记录
cursor.execute('DELETE FROM price_history WHERE stock_id = ?', (stock_id,))
cursor.execute('DELETE FROM notifications WHERE stock_id = ?', (stock_id,))
cursor.execute('DELETE FROM monitored_stocks WHERE id = ?', (stock_id,))
affected_rows = cursor.rowcount
conn.commit()
conn.close()
return affected_rows > 0
except Exception as e:
print(f"删除股票失败: {e}")
return False
def update_monitored_stock(self, stock_id: int, rating: str, entry_range: Dict,
take_profit: float, stop_loss: float,
check_interval: int, notification_enabled: bool,
trading_hours_only: bool = None,
quant_enabled: bool = None,
quant_config: Dict = None):
"""更新监测股票"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
if quant_enabled is not None and quant_config is not None:
quant_config_json = json.dumps(quant_config) if quant_config else None
trading_hours_sql = ", trading_hours_only = ?" if trading_hours_only is not None else ""
params = [rating, json.dumps(entry_range), take_profit, stop_loss,
check_interval, notification_enabled, quant_enabled, quant_config_json]
if trading_hours_only is not None:
params.append(trading_hours_only)
params.append(stock_id)
cursor.execute(f'''
UPDATE monitored_stocks
SET rating = ?, entry_range = ?, take_profit = ?, stop_loss = ?,
check_interval = ?, notification_enabled = ?,
quant_enabled = ?, quant_config = ?{trading_hours_sql},
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', tuple(params))
else:
trading_hours_sql = ", trading_hours_only = ?" if trading_hours_only is not None else ""
params = [rating, json.dumps(entry_range), take_profit, stop_loss, check_interval, notification_enabled]
if trading_hours_only is not None:
params.append(trading_hours_only)
params.append(stock_id)
cursor.execute(f'''
UPDATE monitored_stocks
SET rating = ?, entry_range = ?, take_profit = ?, stop_loss = ?,
check_interval = ?, notification_enabled = ?{trading_hours_sql},
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', tuple(params))
conn.commit()
conn.close()
return cursor.rowcount > 0
def toggle_notification(self, stock_id: int, enabled: bool):
"""切换通知状态"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
UPDATE monitored_stocks
SET notification_enabled = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
''', (enabled, stock_id))
conn.commit()
conn.close()
return cursor.rowcount > 0
def get_stock_by_id(self, stock_id: int) -> Optional[Dict]:
"""根据ID获取股票信息"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT id, symbol, name, rating, entry_range, take_profit, stop_loss,
current_price, last_checked, check_interval, notification_enabled,
trading_hours_only, quant_enabled, quant_config
FROM monitored_stocks WHERE id = ?
''', (stock_id,))
row = cursor.fetchone()
conn.close()
if row:
try:
quant_config = json.loads(row[13]) if row[13] else None
entry_range = json.loads(row[4]) if row[4] else None
except (json.JSONDecodeError, TypeError) as e:
print(f"警告: 股票 {row[1]} 的JSON解析失败: {e}")
entry_range = None
quant_config = None
return {
'id': row[0],
'symbol': row[1],
'name': row[2],
'rating': row[3],
'entry_range': entry_range,
'take_profit': row[5],
'stop_loss': row[6],
'current_price': row[7],
'last_checked': row[8],
'check_interval': row[9],
'notification_enabled': bool(row[10]),
'trading_hours_only': bool(row[11]) if row[11] is not None else True,
'quant_enabled': bool(row[12]),
'quant_config': quant_config
}
return None
def get_monitor_by_code(self, symbol: str) -> Optional[Dict]:
"""
根据股票代码获取监测信息
Args:
symbol: 股票代码
Returns:
监测股票信息字典,不存在则返回None
"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT * FROM monitored_stocks WHERE symbol = ?
''', (symbol,))
row = cursor.fetchone()
conn.close()
if row:
try:
entry_range = json.loads(row[4]) if row[4] else None
quant_config = json.loads(row[12]) if row[12] else None
except (json.JSONDecodeError, TypeError) as e:
print(f"警告: 股票 {row[1]} 的JSON解析失败: {e}")
entry_range = None
quant_config = None
return {
'id': row[0],
'symbol': row[1],
'name': row[2],
'rating': row[3],
'entry_range': entry_range,
'take_profit': row[5],
'stop_loss': row[6],
'current_price': row[7],
'last_checked': row[8],
'check_interval': row[9],
'notification_enabled': row[10],
'quant_enabled': row[11],
'quant_config': quant_config
}
return None
def batch_add_or_update_monitors(self, monitors_data: List[Dict]) -> Dict[str, int]:
"""
批量添加或更新监测股票
Args:
monitors_data: 监测股票数据列表,每个字典包含:
- code/symbol: 股票代码
- name: 股票名称
- rating: 投资评级
- entry_min, entry_max: 进场区间
- take_profit: 止盈位
- stop_loss: 止损位
- check_interval: 检查间隔(可选,默认60秒)
- notification_enabled: 是否启用通知(可选,默认True)
Returns:
统计字典 {"added": X, "updated": Y, "failed": Z, "total": N}
"""
added = 0
updated = 0
failed = 0
for data in monitors_data:
try:
# 兼容code和symbol两种字段名
symbol = data.get('code') or data.get('symbol')
name = data.get('name', symbol)
rating = data.get('rating', '持有')
entry_min = data.get('entry_min')
entry_max = data.get('entry_max')
take_profit = data.get('take_profit')
stop_loss = data.get('stop_loss')
check_interval = data.get('check_interval', 60)
notification_enabled = data.get('notification_enabled', True)
trading_hours_only = data.get('trading_hours_only', True)
# 验证必需字段
if not symbol or not all([entry_min, entry_max, take_profit, stop_loss]):
print(f"[WARN] {symbol} 参数不完整,跳过")
failed += 1
continue
# 构建entry_range
entry_range = {"min": entry_min, "max": entry_max}
# 检查是否已存在
existing = self.get_monitor_by_code(symbol)
if existing:
# 更新现有监测
self.update_monitored_stock(
existing['id'],
rating=rating,
entry_range=entry_range,
take_profit=take_profit,
stop_loss=stop_loss,
check_interval=check_interval,
notification_enabled=notification_enabled,
trading_hours_only=trading_hours_only
)
updated += 1
print(f"[OK] 更新监测: {symbol}")
else:
# 添加新监测
self.add_monitored_stock(
symbol=symbol,
name=name,
rating=rating,
entry_range=entry_range,
take_profit=take_profit,
stop_loss=stop_loss,
check_interval=check_interval,
notification_enabled=notification_enabled,
trading_hours_only=trading_hours_only
)
added += 1
print(f"[OK] 添加监测: {symbol}")
except Exception as e:
symbol_str = data.get('code') or data.get('symbol', 'Unknown')
print(f"[ERROR] 处理监测失败 ({symbol_str}): {str(e)}")
failed += 1
result = {
"added": added,
"updated": updated,
"failed": failed,
"total": added + updated + failed
}
print(f"\n[OK] 批量同步完成: 新增{added}只, 更新{updated}只, 失败{failed}只")
return result
# 全局数据库实例
monitor_db = StockMonitorDatabase()