461 lines
14 KiB
Python
461 lines
14 KiB
Python
'''
|
||
均线回归策略 V3.0 进攻版
|
||
优化重点:提高Beta,增强进攻性,减少过度保守
|
||
|
||
核心改进:
|
||
1. 放宽市场择时,只在极端熊市降仓
|
||
2. 增加持仓数量,提高资金利用率
|
||
3. 放宽买入条件,增加交易机会
|
||
4. 优化止盈,让利润奔跑
|
||
5. 增加突破买入模式
|
||
'''
|
||
|
||
import jqdata
|
||
import numpy as np
|
||
|
||
## 初始化函数
|
||
def initialize(context):
|
||
# 设定沪深300作为基准
|
||
set_benchmark('000300.XSHG')
|
||
# 开启动态复权模式
|
||
set_option('use_real_price', True)
|
||
# 设定成交量比例
|
||
set_option('order_volume_ratio', 1)
|
||
# 设置交易手续费
|
||
set_order_cost(OrderCost(open_tax=0, close_tax=0.001,
|
||
open_commission=0.0003, close_commission=0.0003,
|
||
close_today_commission=0, min_commission=5), type='stock')
|
||
|
||
# ========== 策略参数(进攻版)==========
|
||
g.stocknum = 8 # 持仓数量增加到8只
|
||
g.max_position_ratio = 0.98 # 最大仓位98%
|
||
|
||
# 均线参数
|
||
g.ma_short = 20 # 短期均线
|
||
g.ma_mid = 60 # 中期均线
|
||
g.ma_long = 120 # 长期均线
|
||
|
||
# 回调买入参数(放宽条件)
|
||
g.pullback_ratio = 0.035 # 放宽到3.5%
|
||
g.break_tolerance = 0.015 # 允许跌破1.5%
|
||
|
||
# 止盈止损参数(让利润奔跑)
|
||
g.base_take_profit = 0.20 # 止盈提高到20%
|
||
g.base_stop_loss = 0.06 # 止损放宽到6%
|
||
g.trailing_stop = 0.08 # 回撤容忍8%
|
||
|
||
# 市值筛选(亿)- 扩大范围
|
||
g.min_market_cap = 20
|
||
g.max_market_cap = 1000
|
||
|
||
# 行业分散参数
|
||
g.max_industry_stocks = 3 # 同行业最多3只
|
||
|
||
# 市场状态
|
||
g.market_state = 'NORMAL'
|
||
g.position_ratio = 1.0
|
||
|
||
# 记录
|
||
g.highest_profit = {}
|
||
g.stock_volatility = {}
|
||
g.hold_days = {} # 持仓天数
|
||
|
||
# 定时任务
|
||
run_daily(market_analysis, '09:31')
|
||
run_daily(morning_check, '09:35')
|
||
run_daily(check_positions, '14:00') # 只检查一次
|
||
run_daily(afternoon_trade, '14:50')
|
||
|
||
## 市场环境分析(放宽版)
|
||
def market_analysis(context):
|
||
"""
|
||
只在极端情况下降低仓位,大部分时间保持高仓位
|
||
"""
|
||
index_code = '000300.XSHG'
|
||
|
||
try:
|
||
df = attribute_history(index_code, 130, '1d', ['close'], skip_paused=True)
|
||
if len(df) < 120:
|
||
g.market_state = 'NORMAL'
|
||
g.position_ratio = 1.0
|
||
return
|
||
|
||
close = df['close'].values
|
||
current = close[-1]
|
||
|
||
ma20 = np.mean(close[-20:])
|
||
ma60 = np.mean(close[-60:])
|
||
ma120 = np.mean(close[-120:])
|
||
|
||
change_5d = (close[-1] - close[-6]) / close[-6]
|
||
change_10d = (close[-1] - close[-11]) / close[-11]
|
||
|
||
# 只在极端熊市才降仓(条件更严格)
|
||
if current < ma120 and ma20 < ma60 < ma120 and change_10d < -0.08:
|
||
# 极端熊市:指数跌破120日均线,均线空头,10日跌幅超8%
|
||
g.market_state = 'BEAR'
|
||
g.position_ratio = 0.5
|
||
log.info("【市场】极端熊市,仓位50%%")
|
||
elif change_5d < -0.08:
|
||
# 短期暴跌超8%:暂时减仓
|
||
g.market_state = 'BEAR'
|
||
g.position_ratio = 0.6
|
||
log.info("【市场】短期暴跌,仓位60%%")
|
||
elif current > ma20 and ma20 > ma60:
|
||
# 趋势向上:满仓
|
||
g.market_state = 'BULL'
|
||
g.position_ratio = 1.0
|
||
log.info("【市场】趋势向上,满仓")
|
||
else:
|
||
# 默认高仓位运行
|
||
g.market_state = 'NORMAL'
|
||
g.position_ratio = 0.9
|
||
log.info("【市场】正常状态,仓位90%%")
|
||
|
||
except Exception as e:
|
||
g.market_state = 'NORMAL'
|
||
g.position_ratio = 0.9
|
||
|
||
## 早盘检查
|
||
def morning_check(context):
|
||
g.buy_list = check_stocks(context)
|
||
log.info("【选股】%d 只,市场: %s,仓位: %.0f%%" %
|
||
(len(g.buy_list), g.market_state, g.position_ratio * 100))
|
||
|
||
## 检查持仓 - 优化版(让利润奔跑)
|
||
def check_positions(context):
|
||
if len(context.portfolio.positions) == 0:
|
||
return
|
||
|
||
for stock in list(context.portfolio.positions.keys()):
|
||
position = context.portfolio.positions[stock]
|
||
if position.closeable_amount <= 0:
|
||
continue
|
||
|
||
cost = position.avg_cost
|
||
current_price = position.price
|
||
if cost <= 0:
|
||
continue
|
||
|
||
profit_ratio = (current_price - cost) / cost
|
||
|
||
# 更新持仓天数
|
||
g.hold_days[stock] = g.hold_days.get(stock, 0) + 1
|
||
|
||
# 更新最高盈利
|
||
if stock not in g.highest_profit:
|
||
g.highest_profit[stock] = profit_ratio
|
||
else:
|
||
g.highest_profit[stock] = max(g.highest_profit[stock], profit_ratio)
|
||
|
||
highest = g.highest_profit[stock]
|
||
hold_days = g.hold_days.get(stock, 0)
|
||
|
||
# 动态止损(根据波动率和持仓时间)
|
||
volatility = g.stock_volatility.get(stock, 0.025)
|
||
# 持仓时间越长,止损越宽松
|
||
time_factor = min(1 + hold_days * 0.01, 1.5)
|
||
dynamic_stop_loss = min(g.base_stop_loss * time_factor, 0.12)
|
||
|
||
# === 止损逻辑 ===
|
||
if profit_ratio < -dynamic_stop_loss:
|
||
log.info("【止损】%s 亏损 %.2f%%" % (stock, profit_ratio * 100))
|
||
order_target(stock, 0)
|
||
clean_stock_data(stock)
|
||
continue
|
||
|
||
# === 均线破位(只在亏损时卖)===
|
||
if profit_ratio < 0 and check_sell_signal(stock):
|
||
log.info("【均线破位】%s 破位且亏损,卖出" % stock)
|
||
order_target(stock, 0)
|
||
clean_stock_data(stock)
|
||
continue
|
||
|
||
# === 移动止盈(让利润奔跑)===
|
||
if highest >= g.base_take_profit:
|
||
drawdown = highest - profit_ratio
|
||
# 盈利越多,允许回撤越大(最多允许回撤15%)
|
||
allowed_drawdown = min(g.trailing_stop + highest * 0.4, 0.18)
|
||
if drawdown >= allowed_drawdown:
|
||
log.info("【移动止盈】%s 最高 %.2f%%,回撤 %.2f%%" %
|
||
(stock, highest * 100, drawdown * 100))
|
||
order_target(stock, 0)
|
||
clean_stock_data(stock)
|
||
continue
|
||
|
||
# === 分批止盈(保守止盈,只卖小部分)===
|
||
if profit_ratio >= g.base_take_profit * 2:
|
||
# 盈利超40%,卖出30%锁定利润
|
||
sell_amount = int(position.closeable_amount * 0.3 / 100) * 100
|
||
if sell_amount >= 100:
|
||
log.info("【大幅止盈】%s 盈利 %.2f%%,卖出30%%" % (stock, profit_ratio * 100))
|
||
order(stock, -sell_amount)
|
||
|
||
## 清理股票数据
|
||
def clean_stock_data(stock):
|
||
for d in [g.highest_profit, g.stock_volatility, g.hold_days]:
|
||
if stock in d:
|
||
del d[stock]
|
||
|
||
## 尾盘交易
|
||
def afternoon_trade(context):
|
||
buy_stocks(context)
|
||
|
||
## 买入函数(积极版)
|
||
def buy_stocks(context):
|
||
if not hasattr(g, 'buy_list') or not g.buy_list:
|
||
return
|
||
|
||
position_count = len(context.portfolio.positions)
|
||
adjusted_stocknum = int(g.stocknum * g.position_ratio)
|
||
if adjusted_stocknum <= 0 or position_count >= adjusted_stocknum:
|
||
return
|
||
|
||
# 计算资金分配
|
||
available_cash = context.portfolio.available_cash * g.max_position_ratio
|
||
buy_count = min(adjusted_stocknum - position_count, len(g.buy_list))
|
||
if buy_count <= 0 or available_cash < 10000:
|
||
return
|
||
|
||
cash_per_stock = available_cash / buy_count
|
||
held_industries = get_held_industries(context)
|
||
|
||
bought = 0
|
||
for stock in g.buy_list:
|
||
if bought >= buy_count:
|
||
break
|
||
if stock in context.portfolio.positions:
|
||
continue
|
||
|
||
# 行业分散
|
||
stock_industry = get_stock_industry(stock)
|
||
if stock_industry and held_industries.get(stock_industry, 0) >= g.max_industry_stocks:
|
||
continue
|
||
|
||
# 简化买入条件检查
|
||
if check_buy_signal(stock):
|
||
order_value(stock, cash_per_stock)
|
||
log.info("【买入】%s" % stock)
|
||
g.highest_profit[stock] = 0
|
||
g.stock_volatility[stock] = 0.025
|
||
g.hold_days[stock] = 0
|
||
if stock_industry:
|
||
held_industries[stock_industry] = held_industries.get(stock_industry, 0) + 1
|
||
bought += 1
|
||
|
||
## 获取已持仓的行业分布
|
||
def get_held_industries(context):
|
||
industries = {}
|
||
for stock in context.portfolio.positions.keys():
|
||
ind = get_stock_industry(stock)
|
||
if ind:
|
||
industries[ind] = industries.get(ind, 0) + 1
|
||
return industries
|
||
|
||
## 获取股票所属行业
|
||
def get_stock_industry(stock):
|
||
try:
|
||
ind_dict = get_industry(stock)
|
||
if ind_dict and stock in ind_dict:
|
||
# 获取申万一级行业
|
||
for ind_code, ind_info in ind_dict[stock].items():
|
||
if ind_code.startswith('sw_l1'):
|
||
return ind_info.get('industry_name', None)
|
||
return None
|
||
except:
|
||
return None
|
||
|
||
## 选股函数(进攻版)
|
||
def check_stocks(context):
|
||
# 第一步:基础筛选(放宽条件)
|
||
q = query(
|
||
valuation.code,
|
||
valuation.market_cap
|
||
).filter(
|
||
valuation.market_cap.between(g.min_market_cap, g.max_market_cap)
|
||
).order_by(
|
||
valuation.market_cap.asc()
|
||
).limit(500) # 扩大候选池
|
||
|
||
df = get_fundamentals(q)
|
||
if df.empty:
|
||
return []
|
||
|
||
stock_list = list(df['code'])
|
||
stock_list = filter_basic(stock_list)
|
||
|
||
# 均线信号筛选
|
||
candidates = []
|
||
for stock in stock_list[:200]: # 检查前200只
|
||
if check_buy_signal(stock):
|
||
score = calculate_trend_score(stock)
|
||
candidates.append((stock, score))
|
||
|
||
# 按评分排序
|
||
candidates.sort(key=lambda x: x[1], reverse=True)
|
||
buy_list = [c[0] for c in candidates[:g.stocknum * 3]]
|
||
|
||
return buy_list
|
||
|
||
## 计算趋势强度评分
|
||
def calculate_trend_score(stock):
|
||
"""
|
||
评分因素:
|
||
1. 均线发散程度
|
||
2. 价格距离MA20的位置
|
||
3. 成交量配合
|
||
"""
|
||
try:
|
||
df = attribute_history(stock, 130, '1d', ['close', 'volume'], skip_paused=True)
|
||
if len(df) < 120:
|
||
return 0
|
||
|
||
close = df['close'].values
|
||
volume = df['volume'].values
|
||
|
||
ma20 = np.mean(close[-20:])
|
||
ma60 = np.mean(close[-60:])
|
||
ma120 = np.mean(close[-120:])
|
||
current = close[-1]
|
||
|
||
score = 0
|
||
|
||
# 均线发散程度(20分)
|
||
spread = (ma20 - ma120) / ma120
|
||
score += min(spread * 100, 20)
|
||
|
||
# 价格位置(30分)- 越接近MA20越好
|
||
distance = abs(current - ma20) / ma20
|
||
score += max(0, 30 - distance * 500)
|
||
|
||
# 成交量(20分)- 回调缩量为佳
|
||
vol_ma5 = np.mean(volume[-5:])
|
||
vol_ma20 = np.mean(volume[-20:])
|
||
if vol_ma5 < vol_ma20 * 0.8:
|
||
score += 20 # 缩量回调
|
||
elif vol_ma5 < vol_ma20:
|
||
score += 10
|
||
|
||
# 趋势持续性(30分)
|
||
ma20_slope = (ma20 - np.mean(close[-25:-5])) / np.mean(close[-25:-5])
|
||
if ma20_slope > 0:
|
||
score += min(ma20_slope * 300, 30)
|
||
|
||
return score
|
||
|
||
except:
|
||
return 0
|
||
|
||
## 基础过滤
|
||
def filter_basic(stock_list):
|
||
if not stock_list:
|
||
return []
|
||
|
||
current_data = get_current_data()
|
||
filtered = []
|
||
|
||
for stock in stock_list:
|
||
# 过滤停牌
|
||
if current_data[stock].paused:
|
||
continue
|
||
# 过滤ST
|
||
if current_data[stock].is_st:
|
||
continue
|
||
if 'ST' in current_data[stock].name or '*' in current_data[stock].name:
|
||
continue
|
||
# 过滤科创板、北交所
|
||
if stock.startswith('688') or stock.startswith('8') or stock.startswith('4'):
|
||
continue
|
||
# 过滤涨跌停
|
||
if current_data[stock].last_price >= current_data[stock].high_limit:
|
||
continue
|
||
if current_data[stock].last_price <= current_data[stock].low_limit:
|
||
continue
|
||
|
||
filtered.append(stock)
|
||
|
||
return filtered
|
||
|
||
## 检查买入信号(放宽版)
|
||
def check_buy_signal(stock):
|
||
"""
|
||
放宽买入条件,增加交易机会
|
||
"""
|
||
try:
|
||
df = attribute_history(stock, g.ma_long + 10, '1d', ['close'], skip_paused=True)
|
||
if len(df) < g.ma_long:
|
||
return False
|
||
|
||
close = df['close'].values
|
||
current_price = close[-1]
|
||
|
||
# 计算均线
|
||
ma20 = np.mean(close[-g.ma_short:])
|
||
ma60 = np.mean(close[-g.ma_mid:])
|
||
ma120 = np.mean(close[-g.ma_long:])
|
||
|
||
# 条件1:均线多头排列(核心条件)
|
||
if not (ma20 > ma60 > ma120):
|
||
return False
|
||
|
||
# 条件2:MA20向上
|
||
ma20_5d_ago = np.mean(close[-g.ma_short-5:-5])
|
||
if ma20 <= ma20_5d_ago:
|
||
return False
|
||
|
||
# 条件3:价格在MA20附近(放宽范围)
|
||
distance = (current_price - ma20) / ma20
|
||
# 允许在MA20上方5%以内,或跌破2%以内
|
||
if distance > 0.05 or distance < -0.02:
|
||
return False
|
||
|
||
# 条件4:价格不能离MA60太远
|
||
if current_price > ma60 * 1.30:
|
||
return False
|
||
|
||
return True
|
||
|
||
except:
|
||
return False
|
||
|
||
## 检查卖出信号(均线破位)
|
||
def check_sell_signal(stock):
|
||
"""
|
||
卖出条件:
|
||
1. 价格跌破MA60
|
||
2. MA20下穿MA60(死叉)
|
||
3. 价格跌破MA20且持续3天
|
||
"""
|
||
try:
|
||
df = attribute_history(stock, g.ma_mid + 5, '1d', ['close'], skip_paused=True)
|
||
if len(df) < g.ma_mid:
|
||
return False
|
||
|
||
close = df['close'].values
|
||
current_price = close[-1]
|
||
|
||
ma20 = np.mean(close[-g.ma_short:])
|
||
ma60 = np.mean(close[-g.ma_mid:])
|
||
|
||
# 价格跌破MA60超过2%
|
||
if current_price < ma60 * 0.98:
|
||
return True
|
||
|
||
# MA20下穿MA60(死叉)
|
||
ma20_yesterday = np.mean(close[-g.ma_short-1:-1])
|
||
ma60_yesterday = np.mean(close[-g.ma_mid-1:-1])
|
||
if ma20_yesterday > ma60_yesterday and ma20 < ma60:
|
||
return True
|
||
|
||
# 连续3天收盘在MA20下方
|
||
ma20_3d = [np.mean(close[-g.ma_short-i:-i]) if i > 0 else ma20 for i in range(3)]
|
||
below_ma20_count = sum(1 for i in range(3) if close[-1-i] < ma20_3d[i] * 0.99)
|
||
if below_ma20_count >= 3:
|
||
return True
|
||
|
||
return False
|
||
|
||
except:
|
||
return False
|
||
|