278 lines
8.7 KiB
Python
278 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
低价擒牛量化交易策略
|
|
实现基于MA均线的买卖择时策略
|
|
"""
|
|
|
|
import pandas as pd
|
|
from datetime import datetime, timedelta
|
|
from typing import Dict, List, Optional
|
|
import logging
|
|
|
|
|
|
class LowPriceBullStrategy:
|
|
"""低价擒牛量化交易策略"""
|
|
|
|
def __init__(self, initial_capital: float = 1000000.0):
|
|
"""
|
|
初始化策略
|
|
|
|
Args:
|
|
initial_capital: 初始资金(默认100万)
|
|
"""
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
# 策略参数
|
|
self.initial_capital = initial_capital
|
|
self.available_cash = initial_capital
|
|
self.max_stocks = 4 # 账户最大持股数
|
|
self.max_position_per_stock = 0.4 # 个股最大持仓比例(4成)
|
|
self.max_daily_buy = 2 # 单日最大买入数
|
|
self.holding_period = 5 # 持股周期(天)
|
|
|
|
# 持仓信息
|
|
self.positions: Dict[str, Dict] = {} # {股票代码: {买入价, 数量, 买入日期, 持有天数}}
|
|
self.trade_history: List[Dict] = [] # 交易历史
|
|
|
|
# 当日交易计数
|
|
self.daily_buy_count = 0
|
|
self.current_date = None
|
|
|
|
def reset_daily_counter(self, date):
|
|
"""重置当日计数器"""
|
|
if self.current_date != date:
|
|
self.current_date = date
|
|
self.daily_buy_count = 0
|
|
|
|
def can_buy(self, stock_code: str) -> tuple[bool, str]:
|
|
"""
|
|
检查是否可以买入
|
|
|
|
Returns:
|
|
(是否可买, 原因)
|
|
"""
|
|
# 检查是否已持有
|
|
if stock_code in self.positions:
|
|
return False, "已持有该股票"
|
|
|
|
# 检查持股数量
|
|
if len(self.positions) >= self.max_stocks:
|
|
return False, f"已达最大持股数限制({self.max_stocks}只)"
|
|
|
|
# 检查当日买入数量
|
|
if self.daily_buy_count >= self.max_daily_buy:
|
|
return False, f"今日已达最大买入数限制({self.max_daily_buy}只)"
|
|
|
|
# 检查资金
|
|
if self.available_cash <= 0:
|
|
return False, "可用资金不足"
|
|
|
|
return True, "可以买入"
|
|
|
|
def calculate_buy_amount(self, stock_price: float) -> tuple[int, float]:
|
|
"""
|
|
计算买入数量(满仓策略)
|
|
|
|
Args:
|
|
stock_price: 股票价格
|
|
|
|
Returns:
|
|
(买入股数, 买入金额)
|
|
"""
|
|
# 满仓策略:使用所有可用资金
|
|
max_amount = self.available_cash
|
|
|
|
# 但不能超过个股最大持仓
|
|
max_per_stock = self.initial_capital * self.max_position_per_stock
|
|
target_amount = min(max_amount, max_per_stock)
|
|
|
|
# 计算股数(A股100股为1手)
|
|
shares = int(target_amount / stock_price / 100) * 100
|
|
|
|
if shares < 100:
|
|
return 0, 0
|
|
|
|
actual_amount = shares * stock_price
|
|
return shares, actual_amount
|
|
|
|
def buy(self, stock_code: str, stock_name: str, price: float, date: str) -> tuple[bool, str, Optional[Dict]]:
|
|
"""
|
|
执行买入操作
|
|
|
|
Returns:
|
|
(是否成功, 消息, 交易详情)
|
|
"""
|
|
# 重置当日计数
|
|
self.reset_daily_counter(date)
|
|
|
|
# 检查是否可买入
|
|
can_buy, reason = self.can_buy(stock_code)
|
|
if not can_buy:
|
|
return False, reason, None
|
|
|
|
# 计算买入数量
|
|
shares, amount = self.calculate_buy_amount(price)
|
|
|
|
if shares == 0:
|
|
return False, "资金不足,无法买入100股", None
|
|
|
|
# 执行买入
|
|
self.positions[stock_code] = {
|
|
'name': stock_name,
|
|
'shares': shares,
|
|
'buy_price': price,
|
|
'buy_date': date,
|
|
'holding_days': 0
|
|
}
|
|
|
|
self.available_cash -= amount
|
|
self.daily_buy_count += 1
|
|
|
|
# 记录交易
|
|
trade = {
|
|
'type': 'BUY',
|
|
'code': stock_code,
|
|
'name': stock_name,
|
|
'price': price,
|
|
'shares': shares,
|
|
'amount': amount,
|
|
'date': date,
|
|
'cash_after': self.available_cash
|
|
}
|
|
self.trade_history.append(trade)
|
|
|
|
message = f"✅ 买入成功: {stock_code} {stock_name} | 价格:{price:.2f} | 数量:{shares}股 | 金额:{amount:.2f}元"
|
|
self.logger.info(message)
|
|
|
|
return True, message, trade
|
|
|
|
def should_sell(self, stock_code: str, ma5: float, ma20: float, current_date: str) -> tuple[bool, str]:
|
|
"""
|
|
判断是否应该卖出
|
|
|
|
策略:
|
|
1. MA5下穿MA20时卖出
|
|
2. 持股满5天强制卖出
|
|
|
|
Returns:
|
|
(是否卖出, 原因)
|
|
"""
|
|
if stock_code not in self.positions:
|
|
return False, "未持有该股票"
|
|
|
|
position = self.positions[stock_code]
|
|
|
|
# 更新持有天数
|
|
# 简化处理,按交易日计算
|
|
position['holding_days'] += 1
|
|
|
|
# 检查持股周期
|
|
if position['holding_days'] >= self.holding_period:
|
|
return True, f"持股满{self.holding_period}天,到期卖出"
|
|
|
|
# 检查MA5下穿MA20
|
|
if ma5 is not None and ma20 is not None:
|
|
if ma5 < ma20:
|
|
return True, "MA5下穿MA20,技术信号卖出"
|
|
|
|
return False, "持有"
|
|
|
|
def sell(self, stock_code: str, price: float, date: str, reason: str = "") -> tuple[bool, str, Optional[Dict]]:
|
|
"""
|
|
执行卖出操作
|
|
|
|
Returns:
|
|
(是否成功, 消息, 交易详情)
|
|
"""
|
|
if stock_code not in self.positions:
|
|
return False, "未持有该股票", None
|
|
|
|
position = self.positions[stock_code]
|
|
shares = position['shares']
|
|
buy_price = position['buy_price']
|
|
|
|
# 计算盈亏
|
|
amount = shares * price
|
|
cost = shares * buy_price
|
|
profit = amount - cost
|
|
profit_pct = (profit / cost) * 100 if cost > 0 else 0
|
|
|
|
# 归还资金
|
|
self.available_cash += amount
|
|
|
|
# 移除持仓
|
|
del self.positions[stock_code]
|
|
|
|
# 记录交易
|
|
trade = {
|
|
'type': 'SELL',
|
|
'code': stock_code,
|
|
'name': position['name'],
|
|
'price': price,
|
|
'shares': shares,
|
|
'amount': amount,
|
|
'date': date,
|
|
'reason': reason,
|
|
'buy_price': buy_price,
|
|
'profit': profit,
|
|
'profit_pct': profit_pct,
|
|
'cash_after': self.available_cash
|
|
}
|
|
self.trade_history.append(trade)
|
|
|
|
profit_str = f"+{profit:.2f}" if profit >= 0 else f"{profit:.2f}"
|
|
message = f"✅ 卖出成功: {stock_code} {position['name']} | 价格:{price:.2f} | 数量:{shares}股 | 盈亏:{profit_str}元({profit_pct:+.2f}%) | 原因:{reason}"
|
|
self.logger.info(message)
|
|
|
|
return True, message, trade
|
|
|
|
def get_portfolio_summary(self) -> Dict:
|
|
"""
|
|
获取投资组合摘要
|
|
|
|
Returns:
|
|
组合摘要信息
|
|
"""
|
|
# 计算持仓市值(需要当前价格,这里用买入价估算)
|
|
position_value = sum(
|
|
pos['shares'] * pos['buy_price']
|
|
for pos in self.positions.values()
|
|
)
|
|
|
|
total_value = self.available_cash + position_value
|
|
|
|
# 计算收益
|
|
total_profit = total_value - self.initial_capital
|
|
total_profit_pct = (total_profit / self.initial_capital) * 100
|
|
|
|
return {
|
|
'initial_capital': self.initial_capital,
|
|
'available_cash': self.available_cash,
|
|
'position_value': position_value,
|
|
'total_value': total_value,
|
|
'total_profit': total_profit,
|
|
'total_profit_pct': total_profit_pct,
|
|
'positions_count': len(self.positions),
|
|
'max_stocks': self.max_stocks,
|
|
'trade_count': len(self.trade_history)
|
|
}
|
|
|
|
def get_positions(self) -> List[Dict]:
|
|
"""获取当前持仓列表"""
|
|
return [
|
|
{
|
|
'code': code,
|
|
'name': pos['name'],
|
|
'shares': pos['shares'],
|
|
'buy_price': pos['buy_price'],
|
|
'buy_date': pos['buy_date'],
|
|
'holding_days': pos['holding_days']
|
|
}
|
|
for code, pos in self.positions.items()
|
|
]
|
|
|
|
def get_trade_history(self) -> List[Dict]:
|
|
"""获取交易历史"""
|
|
return self.trade_history.copy()
|