1.需求
-
Baostock 拉历史日线,筛出"截至昨日,历史最低价已接近或低于当前价"的候选
-
对新浪批量请求候选股的今日实时最低价
-
交叉比对:今日实时最低价 <= 历史前低,才算真创新低
-
附加:上市>1年、剔除科创/北证/ST、最新季度净利润>0
2.脚本
vim bottom_fishing.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
A 股创历史新低筛选器(Baostock 历史 + 新浪财经实时 混合版)
流程:
1. Baostock 拉历史日线,筛出"截至昨日,历史最低价已接近或低于当前价"的候选
2. 对新浪批量请求候选股的今日实时最低价
3. 交叉比对:今日实时最低价 <= 历史前低,才算真创新低
4. 附加:上市>1年、剔除科创/北证/ST、最新季度净利润>0
用法: python3 bottom_fishing_sina.py
依赖: pip install baostock requests pandas
"""
import baostock as bs
import requests
import pandas as pd
from datetime import datetime, timedelta
import time
import sys
import re
# ==================== 配置 ====================
MIN_LIST_DAYS = 365
HIST_DELAY = 0.25
SINA_BATCH_SIZE = 50 # 每次请求新浪的股票数量
SINA_DELAY = 1.0 # 新浪请求间隔
MAX_HIST_LOOKBACK_DAYS = 7
SINA_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://finance.sina.com.cn/",
}
class C:
RESET = "\033[0m"; BOLD = "\033[1m"; DIM = "\033[2m"
RED = "\033[91m"; GREEN = "\033[92m"; YELLOW = "\033[93m"
BLUE = "\033[94m"; MAGENTA = "\033[95m"; CYAN = "\033[96m"; GRAY = "\033[90m"
def banner():
print(f"""
{C.CYAN}{C.BOLD}╔══════════════════════════════════════════════════════════╗
║ 📉 A 股创历史新低筛选器(Baostock + 新浪财经)📉 ║
║ 历史候选 → 新浪批量实时 → 今日真创新低 ║
╚══════════════════════════════════════════════════════════╝{C.RESET}
""")
def progress_bar(current, total, width=34, prefix="", suffix=""):
ratio = current / total if total else 0
filled = int(width * ratio)
bar = "█" * filled + C.GRAY + "░" * (width - filled) + C.RESET
color = C.GREEN if ratio == 1 else C.CYAN
print(f"\r {prefix} {color}{bar}{C.RESET} {ratio*100:5.1f}% {suffix} ", end="")
sys.stdout.flush()
def stage(tag, msg, color=C.CYAN):
print(f" {color}{C.BOLD}[{tag}]{C.RESET} {msg}")
# ==================== Baostock 部分(同上一版) ====================
def get_stock_list_baostock():
for offset in range(MAX_HIST_LOOKBACK_DAYS):
day = (datetime.now() - timedelta(days=offset)).strftime("%Y-%m-%d")
rs = bs.query_all_stock(day=day)
if rs.error_code != "0":
continue
rows = []
while rs.next():
rows.append(rs.get_row_data())
if rows:
df = pd.DataFrame(rows, columns=rs.fields)
print(f" 使用 {day} 的股票列表,共 {len(df)} 条")
return df
return None
def is_excluded_baostock(code, name):
pure = code.split(".")[-1]
if pure.startswith("688"):
return True
if pure.startswith(("8", "4")):
return True
if "ST" in name.upper() or "退" in name:
return True
return False
def get_listing_date(code):
rs = bs.query_stock_basic(code=code)
if rs.error_code != "0" or not rs.next():
return None
row = rs.get_row_data()
ipo = row[2]
if not ipo:
return None
try:
return datetime.strptime(ipo, "%Y-%m-%d")
except ValueError:
return None
def get_hist_low(code):
"""拉前复权历史日线,返回截至昨日的历史最低价"""
rs = bs.query_history_k_data_plus(
code, "date,low",
start_date="1990-01-01",
end_date=datetime.now().strftime("%Y-%m-%d"),
frequency="d", adjustflag="2",
)
if rs.error_code != "0":
return None
rows = []
while rs.next():
rows.append(rs.get_row_data())
if len(rows) < 2:
return None
df = pd.DataFrame(rows, columns=rs.fields)
df["low"] = pd.to_numeric(df["low"], errors="coerce")
df = df.dropna(subset=["low"])
if len(df) < 2:
return None
hist_min = df["low"].iloc[:-1].min()
return {"hist_min": round(float(hist_min), 2), "last_date": df["date"].iloc[-1]}
def check_fundamental(code):
try:
year = datetime.now().year
quarter = (datetime.now().month - 1) // 3
if quarter == 0:
year -= 1
quarter = 4
rs = bs.query_profit_data(code=code, year=year, quarter=quarter)
if rs.error_code != "0" or not rs.next():
return None
row = rs.get_row_data()
idx = None
for i, f in enumerate(rs.fields):
if "netProfit" in f or f == "np":
idx = i
break
if idx is None or row[idx] == "":
return None
return float(row[idx]) > 0
except Exception:
return None
# ==================== 新浪财经实时部分 ====================
def sina_code(pure_code):
"""6位代码转新浪格式:sh/sz 前缀"""
if pure_code.startswith(("6", "9")):
return f"sh{pure_code}"
return f"sz{pure_code}"
def fetch_sina_batch(codes):
"""
批量请求新浪实时行情,返回 {纯代码: {"low": 今日最低, "name": 名称, ...}}
codes 为纯 6 位代码列表
"""
sina_codes = [sina_code(c) for c in codes]
url = f"https://hq.sinajs.cn/list={','.join(sina_codes)}"
try:
r = requests.get(url, headers=SINA_HEADERS, timeout=10)
r.encoding = "gbk"
if r.status_code != 200:
return None
result = {}
for line in r.text.strip().split("\n"):
m = re.match(r'var hq_str_([a-z]{2}\d{6})="([^"]*)"', line)
if not m:
continue
code_full = m.group(1)
pure = code_full[2:]
fields = m.group(2).split(",")
if len(fields) < 6 or fields[0] == "":
continue
try:
result[pure] = {
"name": fields[0],
"today_low": float(fields[5]), # 今日最低价
"today_open": float(fields[1]),
"yesterday_close": float(fields[2]),
"current": float(fields[3]),
}
except (ValueError, IndexError):
continue
return result
except Exception as e:
print(f" ⚠️ 新浪请求失败: {e}")
return None
# ==================== 主流程 ====================
def main():
banner()
started = time.time()
# ---------- 第 1 步:Baostock 历史扫描,收集候选 ----------
stage("1/4", "Baostock 登录...")
lg = bs.login()
if lg.error_code != "0":
print(f"{C.RED}登录失败: {lg.error_msg}{C.RESET}")
return
stage("2/4", "获取 A 股列表(Baostock)...")
stock_df = get_stock_list_baostock()
if stock_df is None or stock_df.empty:
print(f"{C.RED}获取股票列表失败{C.RESET}")
bs.logout()
return
stock_df = stock_df[stock_df["code"].str.startswith(("sh.", "sz."))].copy()
stock_df = stock_df[~stock_df.apply(
lambda r: is_excluded_baostock(r["code"], r["code_name"]), axis=1)]
stage("2/4", f"过滤后剩余 {C.GREEN}{len(stock_df)}{C.RESET} 只")
cutoff = datetime.now() - timedelta(days=MIN_LIST_DAYS)
# 候选:历史最低价较低的股票(不要求今天创新低,只要求历史低位)
candidates = []
total = len(stock_df)
print(f"\n{C.GRAY}Baostock 历史扫描中(共 {total} 只),找出接近历史低位的候选...{C.RESET}\n")
for pos, (_, row) in enumerate(stock_df.iterrows(), start=1):
code_bs = row["code"]
pure = code_bs.split(".")[-1]
listing = get_listing_date(code_bs)
if listing is None or listing > cutoff:
progress_bar(pos, total, prefix="历史扫描", suffix=f"候选 {len(candidates)}")
time.sleep(HIST_DELAY)
continue
hist = get_hist_low(code_bs)
if hist is None:
progress_bar(pos, total, prefix="历史扫描", suffix=f"候选 {len(candidates)}")
time.sleep(HIST_DELAY)
continue
candidates.append({
"pure": pure,
"name": row["code_name"],
"listing": listing.strftime("%Y-%m-%d"),
"hist_min": hist["hist_min"],
})
progress_bar(pos, total, prefix="历史扫描", suffix=f"候选 {len(candidates)}")
time.sleep(HIST_DELAY)
print()
bs.logout()
stage("2/4", f"历史扫描完成,候选 {C.GREEN}{len(candidates)}{C.RESET} 只", C.GREEN)
if not candidates:
print(f"{C.YELLOW}无候选,退出{C.RESET}")
return
# ---------- 第 2 步:新浪批量请求候选股的实时最低价 ----------
stage("3/4", f"新浪财经批量查询实时行情({len(candidates)} 只,分批 {SINA_BATCH_SIZE} 只/次)...")
realtime = {}
for i in range(0, len(candidates), SINA_BATCH_SIZE):
batch = candidates[i:i + SINA_BATCH_SIZE]
codes = [c["pure"] for c in batch]
data = fetch_sina_batch(codes)
if data:
realtime.update(data)
progress_bar(min(i + SINA_BATCH_SIZE, len(candidates)), len(candidates),
prefix="新浪查询", suffix=f"已获取 {len(realtime)}")
time.sleep(SINA_DELAY)
print()
stage("3/4", f"新浪实时数据 {C.GREEN}{len(realtime)}{C.RESET} 条", C.GREEN)
# ---------- 第 3 步:交叉比对 ----------
stage("4/4", "交叉比对 + 基本面验证...")
hits = []
for c in candidates:
rt = realtime.get(c["pure"])
if not rt:
continue
# 今日实时最低 <= 历史前低
if rt["today_low"] <= c["hist_min"] + 1e-4:
print(f" {C.YELLOW}🔍{C.RESET} {c['name']}({c['pure']}) "
f"今日低 {C.RED}{rt['today_low']}{C.RESET} <= 历史前低 {c['hist_min']}")
# 基本面验证(Baostock)
bs.login()
fund = check_fundamental(f"sh.{c['pure']}" if c["pure"].startswith("6") else f"sz.{c['pure']}")
if fund:
hits.append({
"代码": c["pure"],
"名称": rt["name"] or c["name"],
"上市日期": c["listing"],
"今日最低": round(rt["today_low"], 2),
"历史前低": c["hist_min"],
"现价": rt["current"],
"昨收": rt["yesterday_close"],
})
print(f" {C.GREEN}✓ 通过{C.RESET}")
else:
print(f" {C.RED}✗ 基本面未通过{C.RESET}")
elapsed = time.time() - started
# ---------- 输出 ----------
if hits:
print(f"\n{C.BOLD}{C.GREEN}🎯 命中 {len(hits)} 只{C.RESET}")
print(f"{C.GRAY}{'─'*76}{C.RESET}")
print(f"{C.BOLD} {'#':<3}{'代码':<8}{'名称':<10}{'上市日期':<12}{'今日低':<9}{'历史前低':<9}{'现价'}{C.RESET}")
print(f"{C.GRAY}{'─'*76}{C.RESET}")
for i, h in enumerate(hits, 1):
print(f" {C.MAGENTA}{i:>2}{C.RESET} {C.CYAN}{h['代码']}{C.RESET} "
f"{h['名称']:<8} {C.DIM}{h['上市日期']}{C.RESET} "
f"{C.RED}{C.BOLD}{h['今日最低']:<7}{C.RESET} "
f"{C.DIM}{h['历史前低']:<8}{C.RESET} {h['现价']}")
df = pd.DataFrame(hits)
df.to_csv("newlow_sina.csv", index=False, encoding="utf-8-sig")
print(f"\n 💾 已保存 {C.GREEN}{C.BOLD}newlow_sina.csv{C.RESET}")
else:
print(f"\n{C.YELLOW}本次未筛出满足全部条件的股票{C.RESET}")
print(f"\n {C.DIM}耗时 {elapsed/60:.1f} 分钟{C.RESET}\n")
if __name__ == "__main__":
main()
3.执行验证
chmod + x bottom_fishing.py
./bottom_fishing.py
4.效果图

截图是这种跑的脚本,耗时比较长