376 lines
12 KiB
Python
376 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
低价擒牛策略监控模块
|
||
监控持仓股票的卖出信号
|
||
"""
|
||
|
||
import sqlite3
|
||
import pandas as pd
|
||
from datetime import datetime, timedelta
|
||
from typing import Dict, List, Optional, Tuple
|
||
import logging
|
||
import os
|
||
|
||
|
||
class LowPriceBullMonitor:
|
||
"""低价擒牛策略监控器"""
|
||
|
||
def __init__(self, db_path: str = "low_price_bull_monitor.db"):
|
||
"""
|
||
初始化监控器
|
||
|
||
Args:
|
||
db_path: 数据库文件路径
|
||
"""
|
||
self.logger = logging.getLogger(__name__)
|
||
self.db_path = db_path
|
||
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,
|
||
stock_code TEXT NOT NULL,
|
||
stock_name TEXT NOT NULL,
|
||
buy_price REAL NOT NULL,
|
||
buy_date TEXT NOT NULL,
|
||
holding_days INTEGER DEFAULT 0,
|
||
status TEXT DEFAULT 'holding',
|
||
add_time TEXT NOT NULL,
|
||
remove_time TEXT,
|
||
remove_reason TEXT,
|
||
UNIQUE(stock_code, status)
|
||
)
|
||
""")
|
||
|
||
# 创建卖出提醒表
|
||
cursor.execute("""
|
||
CREATE TABLE IF NOT EXISTS sell_alerts (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
stock_code TEXT NOT NULL,
|
||
stock_name TEXT NOT NULL,
|
||
alert_type TEXT NOT NULL,
|
||
alert_reason TEXT NOT NULL,
|
||
current_price REAL,
|
||
ma5 REAL,
|
||
ma20 REAL,
|
||
holding_days INTEGER,
|
||
alert_time TEXT NOT NULL,
|
||
is_sent INTEGER DEFAULT 0
|
||
)
|
||
""")
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
self.logger.info("低价擒牛监控数据库初始化完成")
|
||
|
||
def add_stock(self, stock_code: str, stock_name: str, buy_price: float,
|
||
buy_date: str = None) -> Tuple[bool, str]:
|
||
"""
|
||
添加股票到监控列表
|
||
|
||
Args:
|
||
stock_code: 股票代码(不含后缀)
|
||
stock_name: 股票名称
|
||
buy_price: 买入价格
|
||
buy_date: 买入日期(格式:YYYY-MM-DD)
|
||
|
||
Returns:
|
||
(是否成功, 消息)
|
||
"""
|
||
try:
|
||
if buy_date is None:
|
||
buy_date = datetime.now().strftime("%Y-%m-%d")
|
||
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 检查是否已存在
|
||
cursor.execute("""
|
||
SELECT id FROM monitored_stocks
|
||
WHERE stock_code = ? AND status = 'holding'
|
||
""", (stock_code,))
|
||
|
||
if cursor.fetchone():
|
||
conn.close()
|
||
return False, f"股票 {stock_code} 已在监控列表中"
|
||
|
||
# 添加到监控列表
|
||
cursor.execute("""
|
||
INSERT INTO monitored_stocks
|
||
(stock_code, stock_name, buy_price, buy_date, add_time)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
""", (stock_code, stock_name, buy_price, buy_date,
|
||
datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
self.logger.info(f"添加股票到监控: {stock_code} {stock_name}")
|
||
return True, f"成功添加 {stock_code} {stock_name} 到监控列表"
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"添加股票失败: {e}")
|
||
return False, f"添加失败: {str(e)}"
|
||
|
||
def remove_stock(self, stock_code: str, reason: str = "手动移除") -> Tuple[bool, str]:
|
||
"""
|
||
从监控列表移除股票
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
reason: 移除原因
|
||
|
||
Returns:
|
||
(是否成功, 消息)
|
||
"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 先检查是否存在持仓中的股票
|
||
cursor.execute("""
|
||
SELECT id FROM monitored_stocks
|
||
WHERE stock_code = ? AND status = 'holding'
|
||
""", (stock_code,))
|
||
|
||
if not cursor.fetchone():
|
||
conn.close()
|
||
return False, f"股票 {stock_code} 不在监控列表中"
|
||
|
||
# 先删除该股票的所有'removed'记录(避免UNIQUE约束冲突)
|
||
cursor.execute("""
|
||
DELETE FROM monitored_stocks
|
||
WHERE stock_code = ? AND status = 'removed'
|
||
""", (stock_code,))
|
||
|
||
# 然后更新持仓中的记录为'removed'
|
||
cursor.execute("""
|
||
UPDATE monitored_stocks
|
||
SET status = 'removed',
|
||
remove_time = ?,
|
||
remove_reason = ?
|
||
WHERE stock_code = ? AND status = 'holding'
|
||
""", (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), reason, stock_code))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
self.logger.info(f"移除股票: {stock_code}, 原因: {reason}")
|
||
return True, f"成功移除 {stock_code}"
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"移除股票失败: {e}")
|
||
return False, f"移除失败: {str(e)}"
|
||
|
||
def get_monitored_stocks(self) -> List[Dict]:
|
||
"""
|
||
获取所有监控中的股票
|
||
|
||
Returns:
|
||
股票列表
|
||
"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
df = pd.read_sql_query("""
|
||
SELECT * FROM monitored_stocks
|
||
WHERE status = 'holding'
|
||
ORDER BY add_time DESC
|
||
""", conn)
|
||
conn.close()
|
||
|
||
return df.to_dict('records') if not df.empty else []
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取监控列表失败: {e}")
|
||
return []
|
||
|
||
def update_holding_days(self):
|
||
"""更新所有股票的持有天数"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 获取所有持仓股票
|
||
cursor.execute("""
|
||
SELECT stock_code, buy_date FROM monitored_stocks
|
||
WHERE status = 'holding'
|
||
""")
|
||
|
||
stocks = cursor.fetchall()
|
||
today = datetime.now().date()
|
||
|
||
for stock_code, buy_date in stocks:
|
||
buy_date_obj = datetime.strptime(buy_date, "%Y-%m-%d").date()
|
||
holding_days = (today - buy_date_obj).days
|
||
|
||
cursor.execute("""
|
||
UPDATE monitored_stocks
|
||
SET holding_days = ?
|
||
WHERE stock_code = ? AND status = 'holding'
|
||
""", (holding_days, stock_code))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
self.logger.info("持有天数更新完成")
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"更新持有天数失败: {e}")
|
||
|
||
def add_sell_alert(self, stock_code: str, stock_name: str, alert_type: str,
|
||
alert_reason: str, current_price: float = None,
|
||
ma5: float = None, ma20: float = None,
|
||
holding_days: int = None) -> bool:
|
||
"""
|
||
添加卖出提醒
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
stock_name: 股票名称
|
||
alert_type: 提醒类型(holding_days/ma_cross)
|
||
alert_reason: 提醒原因
|
||
current_price: 当前价格
|
||
ma5: MA5值
|
||
ma20: MA20值
|
||
holding_days: 持有天数
|
||
|
||
Returns:
|
||
是否成功
|
||
"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
# 检查是否已存在相同的提醒
|
||
cursor.execute("""
|
||
SELECT id FROM sell_alerts
|
||
WHERE stock_code = ? AND alert_type = ? AND is_sent = 0
|
||
""", (stock_code, alert_type))
|
||
|
||
if cursor.fetchone():
|
||
conn.close()
|
||
return False
|
||
|
||
cursor.execute("""
|
||
INSERT INTO sell_alerts
|
||
(stock_code, stock_name, alert_type, alert_reason,
|
||
current_price, ma5, ma20, holding_days, alert_time)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
""", (stock_code, stock_name, alert_type, alert_reason,
|
||
current_price, ma5, ma20, holding_days,
|
||
datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
self.logger.info(f"添加卖出提醒: {stock_code} - {alert_reason}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"添加卖出提醒失败: {e}")
|
||
return False
|
||
|
||
def get_pending_alerts(self) -> List[Dict]:
|
||
"""
|
||
获取待发送的提醒
|
||
|
||
Returns:
|
||
提醒列表
|
||
"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
df = pd.read_sql_query("""
|
||
SELECT * FROM sell_alerts
|
||
WHERE is_sent = 0
|
||
ORDER BY alert_time DESC
|
||
""", conn)
|
||
conn.close()
|
||
|
||
return df.to_dict('records') if not df.empty else []
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取提醒失败: {e}")
|
||
return []
|
||
|
||
def mark_alert_sent(self, alert_id: int):
|
||
"""标记提醒已发送"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
cursor.execute("""
|
||
UPDATE sell_alerts
|
||
SET is_sent = 1
|
||
WHERE id = ?
|
||
""", (alert_id,))
|
||
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"标记提醒失败: {e}")
|
||
|
||
def get_history_alerts(self, limit: int = 50) -> List[Dict]:
|
||
"""
|
||
获取历史提醒记录
|
||
|
||
Args:
|
||
limit: 返回记录数
|
||
|
||
Returns:
|
||
提醒列表
|
||
"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
df = pd.read_sql_query(f"""
|
||
SELECT * FROM sell_alerts
|
||
ORDER BY alert_time DESC
|
||
LIMIT {limit}
|
||
""", conn)
|
||
conn.close()
|
||
|
||
return df.to_dict('records') if not df.empty else []
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取历史提醒失败: {e}")
|
||
return []
|
||
|
||
def clear_old_alerts(self, days: int = 30):
|
||
"""
|
||
清理旧的提醒记录
|
||
|
||
Args:
|
||
days: 保留天数
|
||
"""
|
||
try:
|
||
conn = sqlite3.connect(self.db_path)
|
||
cursor = conn.cursor()
|
||
|
||
cutoff_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||
|
||
cursor.execute("""
|
||
DELETE FROM sell_alerts
|
||
WHERE alert_time < ? AND is_sent = 1
|
||
""", (cutoff_date,))
|
||
|
||
deleted = cursor.rowcount
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
self.logger.info(f"清理了 {deleted} 条旧提醒记录")
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"清理旧提醒失败: {e}")
|
||
|
||
|
||
# 全局监控器实例
|
||
low_price_bull_monitor = LowPriceBullMonitor()
|