|
|

楼主 |
发表于 2026-6-21 14:04
|
显示全部楼层
import pandas as pd
import numpy as np
# ---------- 辅助函数 ----------
def MA(series, n):
return series.rolling(n).mean()
def REF(series, n):
return series.shift(n)
# ---------- 主选股函数 ----------
def check_stock(df_stock, df_index, df_sector, params, current_date=None,
roe=None, cash_flow=None, shareholder_cnt=None, north_hold=None):
"""
判断股票在指定日期是否属于四种训练形态之一
参数:
df_stock : DataFrame, 必须含 open, high, low, close, volume, capital
df_index : DataFrame, 必须含 close (大盘指数)
df_sector: DataFrame, 必须含 close (板块指数)
params : dict, 包含所有参数 N1~N22
current_date : 日期, 默认最新
roe : list, 最近3年ROE(%),如 [12, 13, 14]
cash_flow : float, 经营现金流/净利润,如 1.2
shareholder_cnt : list, 最近3期股东户数,如 [50000, 48000, 46000]
north_hold : list, 最近3期北向持股比例(%),如 [3.2, 3.8, 4.5]
返回:
dict: {"match": True/False, "type1": True/False, ...}
"""
if current_date is None:
current_date = df_stock.index[-1]
df = df_stock.loc[:current_date].copy()
if len(df) < 250:
return {"match": False, "type1": False, "type2": False, "type3": False, "type4": False,
"score": 0, "level": "D"}
# ========== 提取参数 ==========
N1 = params['N1']; N2 = params['N2']; N3 = params['N3']; N4 = params['N4']; N5 = params['N5']
N6 = params['N6']; N7 = params['N7']; N8 = params['N8'];
N10 = params['N10']
N11 = params['N11']; N12 = params['N12']; N13 = params['N13']; N14 = params['N14'];
N15 = params.get('N15', 25)
N17 = params.get('N17', 0.4)
N18 = params.get('N18', 1.5)
N19 = params.get('N19', 1)
N20 = params.get('N20', 1.5)
N22 = params.get('N22', 100)
# Volume单位:share 或 hand,必须显式指定,不自动猜测
VOLUME_UNIT = params.get('volume_unit', 'share') # 默认share
# ========== 分类型参数 ==========
N9_1 = params.get('N9_1', 40); N16_1 = params.get('N16_1', 1.5); N21_1 = params.get('N21_1', 8)
CHIP_1 = params.get('CHIP_1', 0.85); LEAD_1 = params.get('LEAD_1', 0.15)
N9_2 = params.get('N9_2', 60); N16_2 = params.get('N16_2', 1.5); N21_2 = params.get('N21_2', 5)
CHIP_2 = params.get('CHIP_2', 0.90)
N9_3 = params.get('N9_3', 30); N16_3 = params.get('N16_3', 1.0); N21_3 = params.get('N21_3', 5)
CHIP_3 = params.get('CHIP_3', 0.80); LEAD_3 = params.get('LEAD_3', 0.10)
CHIP_4 = params.get('CHIP_4', 0.75)
# ========== 计算常用均线 ==========
df['MA5'] = MA(df['close'], 5)
df['MA10'] = MA(df['close'], 10)
df['MA20'] = MA(df['close'], 20)
df['MA60'] = MA(df['close'], 60)
df['MA120'] = MA(df['close'], 120)
df['MA250'] = MA(df['close'], 250)
# 最新日数据
o = df['open'].iloc[-1]
c = df['close'].iloc[-1]
h = df['high'].iloc[-1]
l = df['low'].iloc[-1]
v = df['volume'].iloc[-1]
capital = df['capital'].iloc[-1] if 'capital' in df else 1e8
# ========== 1. 前期拉高建仓 ==========
# 固定区间(45~25天前)
vol5_end = df['volume'].rolling(5).mean().iloc[-25]
vol30_start = df['volume'].rolling(30).mean().iloc[-45]
lift_vol = vol5_end > vol30_start * N4
seg = df.iloc[-45:-25]
seg_high = seg['high'].max()
seg_low = seg['low'].min()
lift_amp = (seg_high - seg_low) / seg_low * 100 if seg_low != 0 else 0
end_price = df['close'].iloc[-25]
ma250_end = df['MA250'].iloc[-25]
lift_over_ma = end_price > ma250_end * 1.1
fixed_build_1 = (lift_amp > N9_1) & lift_vol & lift_over_ma
fixed_build_2 = (lift_amp > N9_2) & lift_vol & lift_over_ma
fixed_build_3 = (lift_amp > N9_3) & lift_vol & lift_over_ma
# 动态区间(最近30天)
vol5_now = df['volume'].rolling(5).mean().iloc[-1]
vol20_10_ago = df['volume'].rolling(20).mean().iloc[-10]
dyn_vol = vol5_now > vol20_10_ago * N4
dyn_low = df['low'].iloc[-30:].min()
dyn_high = df['high'].iloc[-30:].max()
dyn_amp = (dyn_high - dyn_low) / dyn_low * 100 if dyn_low != 0 else 0
dyn_over_ma = dyn_high > df['MA250'].iloc[-1] * 1.1
dyn_build_1 = (dyn_amp > N9_1) & dyn_vol & dyn_over_ma
dyn_build_2 = (dyn_amp > N9_2) & dyn_vol & dyn_over_ma
dyn_build_3 = (dyn_amp > N9_3) & dyn_vol & dyn_over_ma
if N10 == 1:
has_build_1, has_build_2, has_build_3 = fixed_build_1, fixed_build_2, fixed_build_3
elif N10 == 0:
has_build_1, has_build_2, has_build_3 = dyn_build_1, dyn_build_2, dyn_build_3
else:
has_build_1 = fixed_build_1 or dyn_build_1
has_build_2 = fixed_build_2 or dyn_build_2
has_build_3 = fixed_build_3 or dyn_build_3
# ========== 2. 高位横盘 ==========
seg25 = df.iloc[-25:]
hh25 = seg25['high'].max()
ll25 = seg25['low'].min()
amp25 = (hh25 - ll25) / ll25 * 100 if ll25 != 0 else 0
amp_cond = amp25 < N1
mid_neg = ((seg25['close'] < seg25['open']) & ((seg25['open'] - seg25['close']) / seg25['open'] > 0.02)).sum()
no_mid = (mid_neg == 0)
upper_shadow = (seg25['high'] - seg25[['close','open']].max(axis=1)) / REF(seg25['close'], 1) * 100
lower_shadow = (seg25[['close','open']].min(axis=1) - seg25['low']) / REF(seg25['close'], 1) * 100
long_shadow_cnt = ((upper_shadow > 2.5) | (lower_shadow > 2.5)).sum()
no_long_shadow = long_shadow_cnt <= N2
ma5_series = df['volume'].rolling(5).mean()
max_ma5_30 = ma5_series.iloc[-31:-1].max()
vol_shrink = df['volume'].iloc[-10:].mean() < max_ma5_30 * N3
prev_high = df['close'].iloc[-60:-30].max()
not_break = ll25 > prev_high * 0.96
horiz_cond = amp_cond & no_mid & no_long_shadow & vol_shrink & not_break
# ========== 3. 今日突破 ==========
below_ma = (o < df['MA5'].iloc[-1]) + (o < df['MA10'].iloc[-1]) + (o < df['MA20'].iloc[-1])
break_cond = (c > o) and (c > df['MA5'].iloc[-1]) and (c > df['MA10'].iloc[-1]) and (c > df['MA20'].iloc[-1]) and (c > df['MA60'].iloc[-1]) and (c > df['MA120'].iloc[-1]) and (below_ma >= 2)
vol_ma10_yest = df['volume'].rolling(10).mean().iloc[-2]
vol_break = v > vol_ma10_yest * N5
ma250_now = df['MA250'].iloc[-1]
on_year = (c > ma250_now) and ((c - ma250_now) / ma250_now < 0.4)
# ========== 4. 均线多头排列 ==========
multi = (df['MA5'].iloc[-1] > df['MA10'].iloc[-1] > df['MA20'].iloc[-1] > df['MA60'].iloc[-1] > df['MA120'].iloc[-1])
multi_ok = True if N11 == 0 else multi
# ========== 5. 创30日新高 ==========
new_high_cond = (c >= df['close'].iloc[-30:].max() * 0.995)
new_high_ok = True if N12 == 0 else new_high_cond
# ========== 6. 实体占比 ==========
body = abs(c - o); range_ = h - l
body_pct = 100 if range_ == 0 else body / range_ * 100
body_ok = True if N13 == 0 else (body_pct >= N13)
# ========== 7. 强势突破 ==========
high60 = df['high'].iloc[-60:-1].max()
break_strength = (c - high60) / high60 * 100 if high60 != 0 else 0
strong_break = break_strength > N18
# ========== 8. 竞价与换手 ==========
open_pct = (o / df['close'].iloc[-2] - 1) * 100
gap_up = o > df['high'].iloc[-2]
bid_ok = True if N14 == 0 else (open_pct > 1.0 or gap_up)
# 换手率:显式指定单位,不自动猜测
if VOLUME_UNIT == 'hand':
turn = (v * 100) / capital * 100
else: # 'share'
turn = v / capital * 100
turn_ok = True if N15 == 0 else ((turn >= N20) and (turn <= N15))
# ========== 9. 大盘过滤 ==========
index_close = df_index.loc[:current_date]['close']
index_ma = MA(index_close, N8).iloc[-1]
index_ok = True if N6 == 0 else (index_close.iloc[-1] > index_ma)
# ========== 10. 板块共振 ==========
sector_close = df_sector.loc[:current_date]['close']
sector_ma = MA(sector_close, N8).iloc[-1]
sector_ok = True if N7 == 0 else (sector_close.iloc[-1] > sector_ma)
# ========== 核心条件 ==========
# ① 年线60日斜率
ma250_60 = df['MA250'].iloc[-60]
year_slope = (ma250_now - ma250_60) / ma250_60 * 100 if ma250_60 != 0 else 0
year_angle_ok_1 = year_slope > N16_1
year_angle_ok_2 = year_slope > N16_2
year_angle_ok_3 = year_slope > N16_3
# ② 底部抬高
low1 = df['low'].iloc[-180:-120].min()
low2 = df['low'].iloc[-120:-60].min()
low3 = df['low'].iloc[-60:].min()
higher_low_1 = (low3 > low2 * (1 + N21_1/100)) and (low2 > low1 * (1 + N21_1/100))
higher_low_2 = (low3 > low2 * (1 + N21_2/100)) and (low2 > low1 * (1 + N21_2/100))
higher_low_3 = (low3 > low2 * (1 + N21_3/100)) and (low2 > low1 * (1 + N21_3/100))
# ③ 回踩年线不破
touch_year = (abs(df['low'] - df['MA250']) / df['MA250']) < 0.03
touch_cnt = touch_year.iloc[-180:].sum()
break_days = (df['close'].iloc[-180:] < df['MA250'].iloc[-180:] * 0.97).sum()
not_real_break = (break_days <= 3)
year_retest_ok = (touch_cnt >= N19) and not_real_break
# ④ 提前大盘见底
stock_return = c / df['close'].iloc[-120] - 1 if len(df) >= 120 else 0
index_return = index_close.iloc[-1] / index_close.iloc[-120] - 1 if len(index_close) >= 120 else 0
lead_market_1 = stock_return > index_return + LEAD_1
lead_market_3 = stock_return > index_return + LEAD_3
# ⑤ 控盘度(放宽到5%)
daily_amp = (df['high'] - df['low']) / df['close']
control_ok = daily_amp.iloc[-25:].mean() < 0.05
# ⑥ 量能**稳
vol_ma20 = df['volume'].rolling(20).mean()
vol_ratio = df['volume'] / vol_ma20
vol_ratio_std = vol_ratio.iloc[-25:].std()
vol_control_ok = vol_ratio_std < N17
# ⑦ 围绕年线震荡
around_year = (abs(df['close'] - df['MA250']) / df['MA250']).iloc[-120:].mean() < 0.12
# ⑧ 年线60日震荡
year_pre = df['MA250'].iloc[-60:-30].mean()
year_post = df['MA250'].iloc[-30:].mean()
year_slope60 = (year_post - year_pre) / year_pre * 100 if year_pre != 0 else 0
year_wavy = abs(year_slope60) < 2
# ⑨ 筹码解放率
high_250 = df['high'].iloc[-250:].max()
release_rate = c / high_250 if high_250 != 0 else 0
chip_release_1 = release_rate > CHIP_1
chip_release_2 = release_rate > CHIP_2
chip_release_3 = release_rate > CHIP_3
chip_release_4 = release_rate > CHIP_4
# ⑩ 年线上方时间占比
above_year_days = (df['close'].iloc[-120:] > df['MA250'].iloc[-120:]).sum()
year_hold_ok = above_year_days >= N22
# ⑪ 机构吸筹(量缩价稳:价格60日均线向上 + 成交量60日均线向下)
price_ma60_slope = (df['close'].iloc[-1] / df['close'].iloc[-60] - 1) * 100 if df['close'].iloc[-60] != 0 else 0
vol_ma60_slope = (df['volume'].iloc[-1] / df['volume'].iloc[-60] - 1) * 100 if df['volume'].iloc[-60] != 0 else 0
institution_ok = (price_ma60_slope > 0) and (vol_ma60_slope < 0)
# ========== 财务质量条件 ==========
fundamental_ok = True
# ROE连续3年 > 12%
if roe is not None and isinstance(roe, list) and len(roe) >= 3:
fundamental_ok = fundamental_ok and all(r > 12 for r in roe)
# 经营现金流 > 净利润
if cash_flow is not None:
fundamental_ok = fundamental_ok and (cash_flow > 1.0)
# 股东户数连续下降
if shareholder_cnt is not None and isinstance(shareholder_cnt, list) and len(shareholder_cnt) >= 3:
fundamental_ok = fundamental_ok and (shareholder_cnt[0] > shareholder_cnt[1] > shareholder_cnt[2])
# 北向持股连续增加
if north_hold is not None and isinstance(north_hold, list) and len(north_hold) >= 3:
fundamental_ok = fundamental_ok and (north_hold[0] < north_hold[1] < north_hold[2])
# ========== 四种形态定义 ==========
type1 = (
has_build_1
and year_angle_ok_1
and higher_low_1
and year_retest_ok
and lead_market_1
and control_ok
and vol_control_ok
and on_year
and chip_release_1
and year_hold_ok
and institution_ok
and fundamental_ok
)
type2 = (
has_build_2
and horiz_cond
and break_cond
and vol_break
and multi_ok
and control_ok
and vol_control_ok
and on_year
and new_high_ok
and body_ok
and strong_break
and chip_release_2
and institution_ok
and fundamental_ok
)
type3 = (
has_build_3
and year_angle_ok_3
and higher_low_3
and year_retest_ok
and lead_market_3
and control_ok
and vol_control_ok
and on_year
and not break_cond
and chip_release_3
and year_hold_ok
and institution_ok
and fundamental_ok
)
type4 = (
around_year
and year_wavy
and year_retest_ok
and chip_release_4
and year_hold_ok
and institution_ok
and control_ok # 新增
and vol_control_ok # 新增
and fundamental_ok
)
# ========== 最终结果 ==========
final = (
(type1 or type2 or type3 or type4)
and index_ok
and sector_ok
and turn_ok
and bid_ok
)
# ========== 评分系统(满分130分)==========
score = 0
score += 20 if has_build_1 or has_build_2 or has_build_3 else 0
score += 10 if vol_break else 0
score += 10 if horiz_cond else 0 # 从15降到10
score += 10 if control_ok else 0
score += 5 if vol_control_ok else 0
score += 10 if (higher_low_1 or higher_low_2 or higher_low_3) else 0
score += 10 if (year_angle_ok_1 or year_angle_ok_2 or year_angle_ok_3) else 0
score += 10 if year_retest_ok else 0
score += 5 if (lead_market_1 or lead_market_3) else 0
score += 5 if index_ok else 0
score += 5 if chip_release_1 or chip_release_2 or chip_release_3 or chip_release_4 else 0
score += 5 if fundamental_ok else 0
score += 10 if institution_ok else 0 # 从5升到10
score += 10 if year_hold_ok else 0 # 从5升到10
score += 5 if control_ok and vol_control_ok else 0 # 控盘组合加分
# 等级评定
if score >= 100:
level = "S"
elif score >= 85:
level = "A"
elif score >= 75:
level = "B"
elif score >= 65:
level = "C"
else:
level = "D"
return {
"match": final,
"type1": type1,
"type2": type2,
"type3": type3,
"type4": type4,
"score": score,
"level": level,
"release_rate": release_rate,
"year_slope": year_slope,
"control_rate": daily_amp.iloc[-25:].mean(),
"above_year_days": above_year_days,
"turn": turn
}
|
|