@clawhub-wangzhi43-f2e391be5c
Edit image to beautify faces or portaits in it. Use when (1) User requests to process an image, (2) User asks to beautify a photo.
---
name: ai-beauty
description: Edit image to beautify faces or portaits in it. Use when (1) User requests to process an image, (2) User asks to beautify a photo.
---
# ai-beauty Skill
Beautify faces or portraits in an image according to user's instructions.
AI智能人像美颜技能,让照片里的你光彩动人
全程本地处理您的照片,不上传到任何服务器,安全放心有保障
处理速度快,效果不满意可通过多轮对话调节,不用抽卡
限时免费使用,登录官网https://www.aicodingyard.com 申请token_key即可免费使用,开通vip可不限量使用
还有若干高端功能待您发掘,可联系官方客服获取
# Token Configuration
This skill requires a valid `BITSOUL_TOKEN` to function properly.
You can register and apply for a token for free at <https://www.aicodingyard.com>, and configure it in your external runtime environment.
## Required Environment Variables
* `BITSOUL_TOKEN`: User token used for remote server permission verification.
## Optional Environment Variables
* `BITSOUL_TOKEN_ENV_FILE`: Points to the env file containing `BITSOUL_TOKEN`.
## Configuration Methods
1. **Method 1: Set environment variable directly**
```bash
export BITSOUL_TOKEN="your_token_here"
```
2. **Method 2: Use env file**
```bash
export BITSOUL_TOKEN_ENV_FILE="/path/to/token.env"
```
The content format of `token.env` file should be:
```
BITSOUL_TOKEN=your_token_here
```
**Note**: If both the environment variable and env file are set, the environment variable takes precedence.
# Installation and Initialization
Before using the skill, ensure that python and `requests` are installed.
For the first time use, an initialization operation is required. After setting up the `BITSOUL_TOKEN`, run the initialization script to download the necessary binary file:
```bash
python ./BitSoulFaceBeautySkill/init.py
```
# Instructions
Execute the following procedures step by step each time an image is processed:
1. Run the initialization script init.py
2. Get the image path to be beautified.
3. Ask the user to make a choice among the recommended parameters files and obtain specific beautification requirements.
4. Generate a temporary beauty parameters file (json format).
5. Create a new directory in the image path to be beautified and save all results into this new directory.
6. Run tool.
```shell
./BitSoulFaceBeautySkill/BitSoulBeauty.exe BITSOUL_TOKEN IMAGE_PATH_TO_BE_BEAUTIFIED BEAUTY_PARAMETERS_FILE_PATH IMAGE_PATH_TO_BE_SAVED
```
you must call init.py before you use any functions in this skill.
# Beauty Parameters File
There are three recommended parameters files.
1. 自然(默认)
```json
{
"磨皮":0.5,
"清晰":0.2,
"白牙":0.4,
"亮眼":0.3,
"美白-自然":0.3,
"祛黑眼圈":0.5,
"祛法令纹":0.3,
"自然脸":0.4,
"女神":0.0,
"男神":0.0,
"小头":0.3,
"小脸":0.3,
"窄脸":0.3,
"瘦下巴":0.0,
"瘦颧骨":0.2,
"瘦下颌":0.2,
"眼睛位置":0.0,
"眼距":0.0,
"大眼":0.3,
"瘦鼻":0.4,
"长鼻":0.0,
"嘴巴位置":0.0,
"嘴巴大小":0.0,
"美胯":0.0,
"天鹅颈":0.0,
"丰胸":0.0,
"瘦腰":0.0,
"长腿":0.0,
"瘦腿":0.0,
"瘦手臂":0.0,
"瘦身":0.0,
"发际线":0.0,
"双眼皮":0.0,
"默认妆容":0.5,
"默认妆容_lut":0.5
}
```
2.精致
```json
{
"磨皮":0.8,
"清晰":0.2,
"白牙":0.4,
"亮眼":0.5,
"美白-白皙":0.5,
"祛黑眼圈":0.7,
"祛法令纹":0.7,
"自然脸":0.0,
"女神":0.7,
"男神":0.0,
"小头":0.0,
"小脸":0.0,
"窄脸":0.15,
"瘦下巴":0.0,
"瘦颧骨":0.3,
"瘦下颌":0.2,
"眼睛位置":0.0,
"眼距":0.0,
"大眼":0.6,
"瘦鼻":0.6,
"长鼻":0.0,
"嘴巴位置":0.0,
"嘴巴大小":0.0,
"美胯":0.0,
"天鹅颈":0.0,
"丰胸":0.0,
"瘦腰":0.0,
"长腿":0.0,
"瘦腿":0.0,
"瘦手臂":0.0,
"瘦身":0.0,
"发际线":0.2,
"双眼皮":0.0,
"女神妆":0.7,
"女神妆_lut":0.7
}
```
3.减龄
```json
{
"磨皮":0.9,
"清晰":0.2,
"白牙":0.4,
"亮眼":0.5,
"美白-红润":0.5,
"祛黑眼圈":0.9,
"祛法令纹":0.9,
"自然脸":0.75,
"女神":0.0,
"男神":0.0,
"小头":0.0,
"小脸":0.4,
"窄脸":0.3,
"瘦下巴":0.0,
"瘦颧骨":0.3,
"瘦下颌":0.2,
"眼睛位置":0.0,
"眼距":0.0,
"大眼":0.8,
"瘦鼻":0.6,
"长鼻":0.0,
"嘴巴位置":0.0,
"嘴巴大小":0.0,
"美胯":0.0,
"天鹅颈":0.0,
"丰胸":0.0,
"瘦腰":0.0,
"长腿":0.0,
"瘦腿":0.0,
"瘦手臂":0.0,
"瘦身":0.0,
"发际线":0.3,
"双眼皮":0.0,
"伪素颜":0.7,
"伪素颜_lut":0.6
}
```
# Beauty Items
## Beauty Parameters File中的**美颜小项**的介绍如下表所示:
|名称|功能描述|目的触发词汇(想要的效果)|问题触发词汇(想解决的痛点)|
|--|--|--|--|
|磨皮|柔化皮肤表层纹理,智能淡化毛孔、细纹、痘印等瑕疵;|磨皮、平滑、光滑、平整、细腻、柔肤、嫩肤、美肤、遮瑕、去瑕疵、护肤、皮肤优化、皮肤处理、去痘、缩毛孔、去黑头、去痘印、去斑、去纹、去油|皮肤粗糙、皮肤状态不好,有痘、皮肤油、毛孔大、皮肤差、闭口、粉刺、黑头|
|美白-自然|均匀提亮肤色至健康透亮状态,呈现“原生好皮”效果;|美白、提亮、变白、调亮、透亮、通透、白皙、亮白、去暗沉、去黑、暖白、自然美白|肤色不均匀、显黑、暗沉、肤色黑、肤色脏、肤色深|
|美白-粉白|提亮肤色并融入柔和粉调,增强脸颊红润气色;|粉白、红润、血色|苍白、气色差|
|美白-白皙|深度提亮至冷调白皙肤质,弱化暗沉与黄气,呈现通透瓷肌感,突出清冷高级氛围;|冷白、雪白、去黄|肤色黄|
|清晰|增强面部轮廓与五官边缘锐度,强化立体光影,避免画面模糊;|清晰,立体、层次、锐化、清楚、去糊|糊、朦胧、画面灰、不清楚|
|自然脸|收窄下半脸,保留个人骨相特征;|瘦脸、脸瘦、推脸、收脸、瓜子脸、鹅蛋脸、小V脸|脸大、大脸、圆脸、方脸、国字脸、显胖、咬肌大、发腮、脸胖、肿脸,垮脸,脸盘大|
|女神|强化V脸轮廓与下颌线流畅度,搭配柔和颧骨修饰,突出女性柔美气质;|女神脸、御姐、精致脸||
|男神|保留男性下颌硬朗线条,适度收窄两侧轮廓,突出轮廓感;|自然脸、高级脸||
|小脸|缩短下半脸长度,缩小下庭的比例;|短脸、脸变短、脸缩短、缩下庭、下庭缩短、提下颌、下颌上移、幼态感、减龄、缩短脸型、小比例、可爱、软萌、短脸、娇小感、脸型紧凑、黄金比例、去成熟感、低龄化|脸长、脸太长、大长脸、下庭长、下脸长、下颌长、下半脸长、老气、脸太长、长辈脸、显老、脸部比例不平衡、下巴过长|
|小头|适度缩小头部整体视觉比例,优化头身比与肩颈线条;|小头、头小、头缩小、头弄小、头整小、头搞小、头调小、头收小、头身比、头肩比、头颈比|头大、头围大、脑袋大、没脖子、大头、|
|窄脸|收缩脸颊两侧轮廓,视觉上缩小脸宽;|窄脸、脸窄、脸变窄、脸收窄、收窄脸型、脸型收窄、脸宽收窄、两侧收窄、两边收窄、脸压窄、脸缩窄、脸往里收|脸宽、脸扁、宽脸、方脸、胖脸|
|发际线|调整发际线高低,优化额头比例,正向为压低,负向为提高;|正:发际线低一点、发际线下来、发际线下移、发际线往前、发际线往下走、发际线压一点、发际线遮一点;负:发际线提升、额头变宽、发际线升高;|正:大脑门、发际线高、发际线太高、发际线后移、发际线后退、发际线上移、;负:发际线低、额头窄;|
|瘦颧骨|柔化颧骨突出感,收窄颧骨区域宽度,使面部线条更流畅柔和;|瘦颧骨、收颧骨、缩颧骨、面部平滑、消灭棱角、脸线条顺、高级脸、削骨感、脸部收紧、磨平骨头、温婉感、面部平顺、自然过渡|颧骨高、太阳穴凹陷、面相硬、脸部崎岖、颧骨凸、颧骨大、颧骨突出、颧骨宽、颧骨太高、颧骨太宽、颧骨肥大、颧骨凸出|
|瘦下颌|收窄下颌角线条,减小下颌宽度;|瘦下颌、下颌线清晰、收紧轮廓、下巴线条、去双下巴、削骨、精致侧脸、紧致感、折叠度、小V脸、上提、立体感、|下巴宽、腮帮子大、方脸、肉多、轮廓模糊、没有下颌线、双下巴、咬肌大、侧脸肥、下颌宽大、脸部下垂、方下巴|
|瘦下巴|缩小下巴体积与宽度,优化下巴尖度,提升脸型精致度;|尖下巴、V字下巴、小尖脸、下巴收一收、精致下巴、蛇精脸(夸张)、瓜子脸、下巴聚拢、收窄下巴、网红尖、下庭收紧|下巴圆、下巴钝、下巴肉、短下巴、方下巴、下巴宽、下巴肥、下半脸笨重、下巴平、下巴大、下巴赘肉、下巴肉感、|
|大眼|整体放大眼睛;|大眼、眼睛放大、眼睛变大、眼睛有神,眼睛扩大,|眼睛小、眼睛太小、眼睛无神、眼睛没神、眼睛小眯眯、眼睛不大、眼睛显小、眼睛没精神、眼睛呆滞、眼睛不亮、眼睛不好看、眼睛没灵气|
|眼距|缩小两眼的间距;|眼距、眼距缩短、眼距协调、眼距优化、眼距修饰、眼距调整、|眼距宽、眼距不协调、眼距奇怪、眼距过大、眼距失调、|
|祛黑眼圈|淡化黑眼圈,同时填充泪沟凹陷区域;|去黑眼圈、遮黑眼圈、淡化黑眼圈、消除黑眼圈、提亮眼下、眼下平整、遮泪沟黑眼圈、眼下不暗沉、改善黑眼圈、去黑眼圈、遮泪沟、填平泪沟|熊猫眼、熬夜眼、眼底黑、泪沟深、眼袋重、黑眼圈太明显、眼下发黑|
|双眼皮|添加双眼皮效果,增强眼部层次感;|变成双眼皮、单变双、割双眼皮||
|亮眼|提亮眼睛,增强眼睛通透度;|亮眼、眼睛提亮、眼睛更有神、眼部提亮、去红血丝、眼睛清澈、眼睛发光|眼睛浑浊、眼睛暗淡、眼神无光、眼睛不亮、眼球浑浊、眼睛无神|
|瘦鼻|收窄或扩大鼻部整体宽度,正向为收窄,负向为增宽;|正:瘦鼻、鼻子变小、鼻子收窄、鼻子精致、鼻型优化、小翘鼻、鼻子线条流畅、缩鼻、鼻子更立体;负:宽鼻、宽鼻梁、宽鼻翼、鼻子宽、;|正:鼻子大、鼻子宽、鼻头大、鼻翼宽、鼻子钝、鼻型粗大;负:鼻子窄、鼻子太窄、鼻子细、鼻子太细;|
|长鼻|微调鼻部纵向长度,正向为缩短,负向为拉长;|正:缩短鼻子、鼻子变短、长鼻调整、鼻长优化、鼻子不太长、鼻型比例协调、缩短鼻长、调整鼻子长度;负:鼻、长鼻子、长鼻梁、高鼻梁、;|正:鼻子太长、鼻子显长;负:鼻子短、鼻梁短、鼻子太短、鼻梁太短、没鼻梁、塌鼻梁、鼻梁塌、;|
|嘴巴大小|放大或缩小嘴唇整体尺寸比例,匹配面部协调度,正向为缩小,负向为放大;|正:樱桃小嘴、缩唇、小嘴、调整嘴型、嘴巴缩小、嘴巴精致;负:放大嘴巴、嘴巴变大;|正:嘴巴大、嘴大、嘴型大;负:嘴巴太小;|
|嘴巴位置|调整嘴唇在面部的垂直位置,嘴巴整体上移,优化人中与唇颏关系;|嘴巴上移、嘴巴位置向上|嘴巴太靠下、嘴巴位置偏下|
|祛法令纹|淡化法令纹,优化鼻基底,平滑肌肤,减少年龄感;|去法令纹、淡化法令纹、遮法令纹、抚平法令纹、消除法令纹、法令纹平整、面部平整、减龄、填充、去沟壑|法令纹深、显老、脸垮、有沟、笑纹重、鼻翼沟深、老态、皮肤下垂、沟壑明显、八字纹、|
|白牙|提亮牙齿,去除黄渍暗沉;|白牙、牙齿美白、牙齿变白、亮白牙齿、去黄牙、洁牙、牙齿增白、牙齿亮白、牙白一点|牙齿黄、牙黄、黄牙|
## 使用“美颜小项”需要遵守的规则
1.“美白-自然”、“美白-粉白”、“美白-白皙”是互斥的,在同一个参数文件中只能出现一种。
FILE:BitSoulFaceBeautySkill/init.py
import os
import requests
from pathlib import Path
from typing import Optional
BASE_URL = "http://info.aicodingyard.com"
def _parse_dotenv_value(env_file: Path, key: str) -> Optional[str]:
if not env_file.exists():
return None
for line in env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
if k.strip() == key:
return v.strip()
return None
def get_token() -> str:
token = os.environ.get("BITSOUL_TOKEN")
if token:
return token
env_file = os.environ.get("BITSOUL_TOKEN_ENV_FILE")
if env_file:
token_from_file = _parse_dotenv_value(Path(env_file).expanduser(), "BITSOUL_TOKEN")
if token_from_file:
return token_from_file
return ""
def request_download_url(file_name: str, token_key: str) -> str:
url = f"{BASE_URL}/api/download_file"
params = {
"file_name": file_name,
"token_key": token_key
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
download_url = data.get("download_url", "")
return download_url
else:
return ""
except Exception as e:
print(f"request_download_url error: {e}")
return ""
def download_data_file(file_name: str, output_path: str, max_retries: int = 3) -> bool:
token = get_token()
if not token:
print("Error: No token set, cannot download data file.")
return False
for retry in range(max_retries):
try:
download_url = request_download_url(file_name, token)
if not download_url:
print(f"Error: Failed to get download url for {file_name}, please check if your token is valid.")
return False
print(f"Starting to download {file_name} ...")
print(f"Download url: {download_url}")
with requests.get(download_url, stream=True, timeout=300) as response:
if response.status_code != 200:
print(f"Download failed, HTTP status code: {response.status_code}")
if retry < max_retries - 1:
print(f"Retrying {retry + 1}/{max_retries} ...")
continue
return False
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
chunk_size = 1024 * 1024
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
pct = (downloaded / total_size) * 100
if downloaded % (10 * chunk_size) < chunk_size:
print(f"Download progress: {pct:.1f}%")
print(f"File downloaded successfully: {output_path}")
return True
except Exception as e:
print(f"Download error: {str(e)}")
if retry < max_retries - 1:
print(f"Retrying {retry + 1}/{max_retries} ...")
else:
print("Download failed, reached maximum retries.")
return False
return False
def init():
import platform
import stat
current_dir = os.path.dirname(os.path.abspath(__file__))
is_windows = platform.system().lower() == 'windows'
exe_name = "BitSoulBeauty.exe" if is_windows else "BitSoulBeauty"
exe_file = os.path.join(current_dir, exe_name)
if not os.path.exists(exe_file):
print(f"Local file {exe_name} not found, downloading from server...")
if not download_data_file(exe_name, exe_file, max_retries=3):
print(f"Error: {exe_name} download failed, initialization aborted.")
return
# Give execution permission on Mac/Linux
if not is_windows:
st = os.stat(exe_file)
os.chmod(exe_file, st.st_mode | stat.S_IEXEC)
if __name__ == "__main__":
init()
将用户的内容一键生成罗振宇风格的ppt演讲稿,当用户需要生成ppt时,输出一个完整科技风格的ppt
--- name: bitsoul-ppt-maker description: 将用户的内容一键生成罗振宇风格的ppt演讲稿,当用户需要生成ppt时,输出一个完整科技风格的ppt --- # PPT maker 将用户的内容一键生成罗振宇风格的ppt演讲稿 ## 设计哲学 - **一屏只讲一件事** - **深色背景 + 白色文字** - **禁止密集排版** ## 生成流程(必须严格遵循) ### Step 1: 读取讲稿 读取用户原始讲稿,不修改原稿内容。 ### Step 2: 生成提炼版讲稿 将内容精简、增强冲击力、适配演示场景,输出ppt描述 ### Step 3: 生成罗振宇风格的标题 为每个章节生成标题,必须满足: - ≤12 字 - 采用以下形式之一:对比式、问题式、断言式、数字式、比喻式 - 自检:是否让人想继续听? ### Step 4: 设计幻灯片结构 规划页面顺序和类型,请参考网上流出的罗振宇ppt ### Step 5: 生成ppt ## 视觉规范速查 | 项目 | 规范 | |------|------| | 比例 | 9:16 竖屏 | | 背景 | #000000 或 #0a0a0a + 模糊光斑动画 | | 主文字 | #ffffff | | 辅助文字 | #9ca3af | | 中文字体 | HarmonyOS Sans SC / 思源黑体 | | 英文字体 | Inter / Roboto | | 标题字重 | font-black / font-bold | | 正文字重 | font-light / font-normal | ## 交互要求 - 键盘 ← → 翻页 - 底部进度导航条 - 平滑切换动画 ## 技术栈 - TailwindCSS(国内CDN) - 复杂页面使用 Vue3(CDN) - 单个HTML文件,可直接打开运行 ## 严禁行为 - 堆字 / 密集排版 - 花哨配色 - 复杂图表 - 横屏比例 - 偏离极简科技风
BitSoul旗下all-in-one的A股市场综合skill,提供股票筛选策略,内置上百种行业常见量化指标, 基于MOE混合因子专家模型的股票买卖点计算判断,个股风险判定,关键指标计算,数据回测,提供准确全面且免费的股票价格与股票历史信息,板块信息与相关交易数据,提供大v交易观察等信息聚合功能
---
name: BitSoulStockSkill
description: BitSoul旗下all-in-one的A股市场综合skill,提供股票筛选策略,内置上百种行业常见量化指标, 基于MOE混合因子专家模型的股票买卖点计算判断,个股风险判定,关键指标计算,数据回测,提供准确全面且免费的股票价格与股票历史信息,板块信息与相关交易数据,提供大v交易观察等信息聚合功能
version: 1.0.0
metadata:
openclaw:
emoji: "📈"
homepage: https://www.aicodingyard.com
requires:
env:
- BITSOUL_TOKEN
bins:
- python3
optional:
env:
- BITSOUL_TOKEN_ENV_FILE
- BITSOUL_CACHE_DIR
pythonPackages:
- pandas
- numpy
- requests
- sqlalchemy
network:
- info.aicodingyard.com
- https://finance.sina.com.cn/
primaryEnv: BITSOUL_TOKEN
---
# 简介
炒股龙虾的最佳搭档,best stock partner forever
## 核心优势
1. 免费稳定且每周更新的A股交易数据:为个股分析、买卖点计算、收益/回撤计算提供坚实的数据基础
2. 基于MOE混合因子专家模型的股票买卖点计算判断
3. 个股风险判定
4. 关键指标计算
5. 数据回测
6. 提供准确全面且免费的股票价格与股票历史信息
7. 板块信息与相关交易数据
8. 提供大V交易观察等信息聚合功能
# Token 配置
本 skill 需要有效的 `BITSOUL_TOKEN` 才能使用功能
token 可前往 <https://www.aicodingyard.com> 免费注册申请,并配置在外部运行环境中
## 必需的环境变量
* `BITSOUL_TOKEN`:用户令牌,用于远程服务器权限验证
## 可选的环境变量
* `BITSOUL_TOKEN_ENV_FILE`:指向包含 `BITSOUL_TOKEN` 的 env 文件
## 配置方式
1. **方式一:直接设置环境变量**
```bash
export BITSOUL_TOKEN="你的令牌"
```
2. **方式二:使用 env 文件**
```bash
export BITSOUL_TOKEN_ENV_FILE="/path/to/token.env"
```
其中 `token.env` 文件内容格式为:
```
BITSOUL_TOKEN=你的令牌
```
**注意**:如果同时设置了环境变量和 env 文件,环境变量优先。
## 运行时描述:
- 从环境变量读取 `BITSOUL_TOKEN`
- 只有在显式提供 `BITSOUL_TOKEN_ENV_FILE` 时,才会从文件中读取 `BITSOUL_TOKEN`
- 根据用户的自然语言,参考references/API_FOR_LLM.md 调用对应接口
- 对“分析 / 估值 / 基本面 / 趋势 / 风险”等请求自动切到综合分析, 需要moe因子计算,返回详细信息
- 对“交易观察 / 技术分析 / 均线 / 动量 / RSI / KDJ / 布林线 / MACD”等请求需要进行moe因子计算,同时需要调用calculate_metrics进行数据回测
- 返回结构化 JSON;查询场景优先给原始数据,分析场景给结论和支撑数据
- 任何返回的股票数据,都应包括个股的完整信息,不应遗漏任何字段
## 安全与运行边界
- 技能所需环境变量已经在本文件 frontmatter 中显式声明
- 策略回测、因子挖矿、实时行情查询等功能会访问 `info.aicodingyard.com` 服务器
- 技能只读取声明过的 token 相关环境变量,以及显式指定的 env 文件路径
- 技能不会主动扫描其他本地凭证文件,也不会写入 token 缓存文件
## 安装
使用前先安装 Python 依赖,依赖参考assets/requirements
首次安装需要执行初始化操作,在设置好BITSOUL_TOKEN后,请进行初始化操作,可参考scripts/data_fetcher
# 注意事项
* api接口文档主要参考 references/API_FOR_LLM.md 对应的代码文件是scripts/stock_api.py 和 scripts/define.py
* **凭证说明**:本skill需要用户Token用于数据访问权限验证。Token通过环境变量 `BITSOUL_TOKEN` 或 `BITSOUL_TOKEN_ENV_FILE` 传入。Token在数据访问时需要保持有效(请自行确保token未过期)。
* **缓存目录**:`BITSOUL_CACHE_DIR`,可选,用于指定缓存目录和数据存储路径。默认值为系统临时目录下的 `BitSoulStockSkill` 子目录
* **因子挖矿**:用户说"因子挖矿"、"挖矿"、"随机挖因子"、"碰碰运气"、"随机推荐"、"挖金矿"、"随机策略"时,直接调用 `api.random_alpha_backtest()`,禁止自己写回测逻辑。返回结果调用 `print(result['summary_text'])` 输出,禁止自行整理摘要。
* **因子挖矿结束后**:在 `print(result['summary_text'])` 之后,用自然语言向用户逐一解释本次使用的每个因子是什么含义、在策略中起什么作用。解释来源是 `result['factor_descriptions']`,格式示例:`alpha022:高价量5日相关的5日变化 × 收盘波动率,用于衡量量价相关动量的衰减程度,在本次策略中作为选股因子使用。`
* **买卖建议**:用户询问某只股票"能不能买"、"该不该卖"、"现在适合持有吗"、"操作建议"、"投资建议"、"买卖信号"、"值得买吗"、"要不要买"等,且用户指定了具体股票时,直接调用 `api.get_trade_signal(code)`,禁止自己计算指标做判断。
* **股票显示格式**:任何场景下输出股票代码时,必须同时附上股票名称,使用 `api.get_symbol_basic_infomation(code).name` 获取,格式如 `600519.SH(贵州茅台)`,禁止只输出代码。
* **买卖信号输出格式(强制执行)**:调用 `get_trade_signal()` 后,必须按以下结构完整输出,禁止简化:
1. **汇总表**:信号、综合评分、置信度、分析日期
2. **专家评分明细表**:列出 `result['experts']` 中所有专家(technical/alpha/fundamental/behavior),每个专家显示:评分、权重、有效指标数(valid_count/total_count)、note(若数据不足)
3. **各专家关键细节**(从 details 中挑重要的展示,不需要逐项列举):
- `technical`:说明看多/看空/中性指标各多少个,点出最关键的 2~3 个指标信号
- `behavior`:列出近5日涨跌幅、涨跌停次数等关键字段
- `fundamental` / `alpha`:若有数据则简要说明核心结论
4. **结论与建议**:引用 `reason` 字段,说明综合评分与阈值关系,给出操作建议
5. 免责声明
## 输出行为
- 默认使用简体中文,以报告的形式输出
- 尽量充分利用接口返回的所有数据,不要随意删减,尽可能多呈现结果内容
- 分析类请求默认返回结论、关键指标、风险提示与支撑摘要
## 示例请求
- `请整理东方财富过去10个交易日的股价信息,并输出成表格`
- `帮我看看同花顺近期最佳买点和卖点分别是多少,并给我些建议`
- `整理中国石油过去半年的财务数据,帮我分析是否具备投资价值`
- `过去一个月上龙虎榜最多的股票是哪只?`
- `请帮我因子挖矿,看看挖出的收益率和最大回撤是多少`
- `调用moe方法,帮我分析工业富联的买入点`
- `最近资金流入最快和涨幅最大的板块是哪些,有什么推荐`
- `给我做一份沪电股份的技术分析报告`
## 参考资料
- 机器可读目录:`references/API_FOR_LLM.dm`
FILE:scripts/backtest_tools.py
"""
backtest_tools.py - 回测工具模块
功能:
1. 交易模拟
2. 持仓管理
3. 组合价值计算
设计原则:
- 函数功能单一、最小粒度
- 纯函数,无副作用
"""
from typing import Dict, List, Tuple
from dataclasses import dataclass
@dataclass
class Position:
"""持仓记录
Attributes:
code: 股票代码,如 '000001.SZ'
shares: 持仓股数(股)
entry_price: 建仓均价(元),加仓时自动更新为加权均价
entry_date: 初次建仓日期,格式 'YYYY-MM-DD'
hold_days: 已持有天数,默认 0
"""
code: str
shares: int
entry_price: float
entry_date: str
hold_days: int = 0
def __repr__(self) -> str:
return (f"Position(code={self.code!r}, shares={self.shares}, "
f"entry_price={self.entry_price}, entry_date={self.entry_date!r}, "
f"hold_days={self.hold_days})")
@dataclass
class TradeResult:
"""单笔交易执行结果
Attributes:
success: 是否成交,False 时 reason 字段说明原因
code: 股票代码
action: 交易方向,'BUY' 或 'SELL'
price: 成交价格(元)
quantity: 成交数量(股)
cost: 买入总成本,含手续费(元);卖出时为 0
fee: 本次手续费(元)
net_proceeds: 卖出净收款,已扣手续费(元);买入时为 0
reason: 失败原因描述;成功时为空字符串
position: 交易后该股票的最新持仓;清仓后为 None
"""
success: bool
code: str
action: str
price: float
quantity: int
cost: float = 0.0
fee: float = 0.0
net_proceeds: float = 0.0
reason: str = ""
position: Position = None
def __repr__(self) -> str:
return (f"TradeResult(success={self.success}, code={self.code!r}, "
f"action={self.action!r}, price={self.price}, quantity={self.quantity}, "
f"cost={self.cost}, fee={self.fee}, net_proceeds={self.net_proceeds}, "
f"reason={self.reason!r}, position={self.position!r})")
def simulate_trade(action: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Dict:
"""模拟单笔交易,计算成本与手续费
买入:总成本 = 价格 × 数量 × (1 + 手续费率)
卖出:净收款 = 价格 × 数量 × (1 - 手续费率)
Args:
action: 交易方向,'BUY' 或 'SELL'(大小写不敏感)
price: 成交价格(元)
quantity: 成交数量(股)
fee_rate: 手续费率,默认 0.0003(即万三)
Returns:
Dict:
- cost (float): 买入总成本(元);卖出时为 0
- fee (float): 手续费(元)
- net_proceeds (float): 卖出净收款(元);买入时为 0
"""
action = action.upper()
fee = price * quantity * fee_rate
if action == 'BUY':
cost = price * quantity * (1 + fee_rate)
return {'cost': cost, 'fee': fee, 'net_proceeds': 0}
else:
net = price * quantity * (1 - fee_rate)
return {'cost': 0, 'fee': fee, 'net_proceeds': net}
def calculate_trade_cost(action: str, price: float, quantity: int, fee_rate: float = 0.0003, slippage: float = 0.0) -> float:
"""计算含滑点的交易总成本
在手续费基础上叠加滑点:买入价上浮、卖出价下浮。
仅返回总成本金额,不区分手续费与滑点。
Args:
action: 交易方向,'BUY' 或 'SELL'(大小写不敏感)
price: 名义价格(元)
quantity: 成交数量(股)
fee_rate: 手续费率,默认 0.0003(万三)
slippage: 滑点比例,默认 0.0;如 0.001 表示 0.1% 的价格偏移
Returns:
float: 含滑点与手续费的总成本(元)
"""
if action.upper() == 'BUY':
actual_price = price * (1 + slippage)
else:
actual_price = price * (1 - slippage)
return actual_price * quantity * (1 + fee_rate)
def create_position(code: str, shares: int, price: float, date: str) -> Position:
"""创建新持仓记录
Args:
code: 股票代码,如 '000001.SZ'
shares: 持仓股数(股)
price: 建仓价格(元)
date: 建仓日期,格式 'YYYY-MM-DD'
Returns:
Position: hold_days=0 的新持仓对象
"""
return Position(code=code, shares=shares, entry_price=price, entry_date=date, hold_days=0)
def update_position(position: Position, days: int = 1) -> Position:
"""更新持仓持有天数
Args:
position: 待更新的持仓对象
days: 本次新增天数,默认 1
Returns:
Position: 已更新 hold_days 的同一持仓对象(原地修改后返回)
"""
position.hold_days += days
return position
def get_position_value(position: Position, current_price: float) -> float:
"""计算持仓当前市值
Args:
position: 持仓对象
current_price: 当前市场价格(元)
Returns:
float: 持仓市值 = 持股数 × 当前价(元)
"""
return position.shares * current_price
def get_position_profit(position: Position, current_price: float) -> Tuple[float, float]:
"""计算持仓浮动盈亏
Args:
position: 持仓对象
current_price: 当前市场价格(元)
Returns:
Tuple[float, float]:
- float: 浮动盈亏金额(元),正数盈利,负数亏损
- float: 浮动盈亏比例(小数),如 0.1 表示盈利 10%
"""
profit = (current_price - position.entry_price) * position.shares
profit_pct = (current_price - position.entry_price) / position.entry_price
return profit, profit_pct
def calculate_portfolio_value(cash: float, positions: Dict[str, Position], prices: Dict[str, float]) -> float:
"""计算组合总价值
总价值 = 现金 + 各持仓市值之和。
若某股票当日无行情,则以建仓价代替当前价。
Args:
cash: 当前现金余额(元)
positions: 持仓字典 {股票代码: Position}
prices: 当日收盘价字典 {股票代码: 价格(元)}
Returns:
float: 组合总价值(元)
"""
position_value = sum(
p.shares * prices.get(p.code, p.entry_price)
for p in positions.values()
)
return cash + position_value
def get_portfolio_positions(positions: Dict[str, Position]) -> List[Dict]:
"""获取组合持仓详情列表
Args:
positions: 持仓字典 {股票代码: Position}
Returns:
List[Dict]: 每个元素包含以下字段:
- code (str): 股票代码
- shares (int): 持仓股数
- entry_price (float): 建仓均价(元)
- entry_date (str): 建仓日期
- hold_days (int): 已持有天数
"""
return [
{
'code': p.code,
'shares': p.shares,
'entry_price': p.entry_price,
'entry_date': p.entry_date,
'hold_days': p.hold_days,
}
for p in positions.values()
]
def build_equity_curve(daily_values: List[Tuple[str, float]]) -> List[float]:
"""从日期-价值序列提取权益曲线
Args:
daily_values: 每日 (日期, 账户总价值) 的有序列表
Returns:
List[float]: 仅含账户总价值的权益曲线,与 metrics.py 中各函数兼容
"""
return [value for _, value in daily_values]
def calculate_daily_returns(equity_curve: List[float]) -> List[float]:
"""计算逐日收益率序列
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
List[float]: 日收益率列表(小数形式),长度比 equity_curve 少 1;
equity_curve 不足 2 条时返回空列表
"""
if len(equity_curve) < 2:
return []
return [
(equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
for i in range(1, len(equity_curve))
if equity_curve[i-1] > 0
]
def should_buy(current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_oversold: float = 30) -> bool:
"""买入信号判断:均线金叉且 RSI 超卖
同时满足以下两个条件时触发买入:
1. 短期均线 > 长期均线(金叉,趋势向上)
2. RSI < rsi_oversold(超卖,价格低估)
Args:
current_price: 当前价格(元,本函数暂未使用,预留扩展)
ma_short: 短期均线值
ma_long: 长期均线值
rsi: 当前 RSI 值(0~100)
rsi_oversold: 超卖阈值,默认 30
Returns:
bool: True 表示触发买入信号
"""
return ma_short > ma_long and rsi < rsi_oversold
def should_sell(current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_overbought: float = 70) -> bool:
"""卖出信号判断:均线死叉或 RSI 超买
满足以下任意一个条件时触发卖出:
1. 短期均线 < 长期均线(死叉,趋势向下)
2. RSI > rsi_overbought(超买,价格高估)
Args:
current_price: 当前价格(元,本函数暂未使用,预留扩展)
ma_short: 短期均线值
ma_long: 长期均线值
rsi: 当前 RSI 值(0~100)
rsi_overbought: 超买阈值,默认 70
Returns:
bool: True 表示触发卖出信号
"""
return ma_short < ma_long or rsi > rsi_overbought
def calculate_drawdown(equity_curve: List[float]) -> List[float]:
"""计算逐日回撤序列
对每个时间点,计算从此前最高点到当日的回撤比例。
与 metrics.get_max_drawdown 的区别:该函数返回完整序列,可用于绘图。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
List[float]: 与 equity_curve 等长的回撤比例序列(0~1);
equity_curve 为空时返回空列表
"""
if not equity_curve:
return []
drawdowns = []
peak = equity_curve[0]
for value in equity_curve:
if value > peak:
peak = value
dd = (peak - value) / peak if peak > 0 else 0
drawdowns.append(dd)
return drawdowns
def buy(cash: float, positions: Dict[str, Position], code: str, price: float, quantity: int, date: str, fee_rate: float = 0.0003) -> Tuple[float, Dict[str, Position], TradeResult]:
"""买入股票
扣除买入成本后更新现金和持仓。若该股票已有持仓,以加权均价合并。
现金不足时不成交,返回失败的 TradeResult。
Args:
cash: 当前现金余额(元)
positions: 持仓字典 {股票代码: Position},会被原地修改
code: 股票代码,如 '000001.SZ'
price: 买入价格(元)
quantity: 买入数量(股)
date: 交易日期,格式 'YYYY-MM-DD'
fee_rate: 手续费率,默认 0.0003(万三)
Returns:
Tuple[float, Dict[str, Position], TradeResult]:
- float: 交易后现金余额(元)
- Dict[str, Position]: 交易后持仓字典
- TradeResult: 交易结果,success=False 时 reason='资金不足'
"""
trade = simulate_trade('BUY', price, quantity, fee_rate)
cost = trade['cost']
if cash < cost:
return cash, positions, TradeResult(
success=False,
code=code,
action='BUY',
price=price,
quantity=quantity,
reason='资金不足',
)
new_cash = cash - cost
if code in positions:
pos = positions[code]
total_cost = pos.entry_price * pos.shares + cost
new_shares = pos.shares + quantity
new_entry_price = total_cost / new_shares
positions[code] = Position(
code=code,
shares=new_shares,
entry_price=new_entry_price,
entry_date=pos.entry_date,
hold_days=0,
)
else:
positions[code] = Position(
code=code,
shares=quantity,
entry_price=price,
entry_date=date,
hold_days=0,
)
return new_cash, positions, TradeResult(
success=True,
code=code,
action='BUY',
price=price,
quantity=quantity,
cost=cost,
fee=trade['fee'],
position=positions[code],
)
def sell(cash: float, positions: Dict[str, Position], code: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Tuple[float, Dict[str, Position], TradeResult]:
"""卖出股票
将卖出净收款加回现金,并减少对应持仓股数。全部卖出时自动删除持仓记录。
无持仓或持仓不足时不成交,返回失败的 TradeResult。
Args:
cash: 当前现金余额(元)
positions: 持仓字典 {股票代码: Position},会被原地修改
code: 股票代码,如 '000001.SZ'
price: 卖出价格(元)
quantity: 卖出数量(股)
fee_rate: 手续费率,默认 0.0003(万三)
Returns:
Tuple[float, Dict[str, Position], TradeResult]:
- float: 交易后现金余额(元)
- Dict[str, Position]: 交易后持仓字典(清仓后该 code 键被删除)
- TradeResult: 交易结果,success=False 时 reason 说明原因
"""
if code not in positions:
return cash, positions, TradeResult(
success=False,
code=code,
action='SELL',
price=price,
quantity=quantity,
reason='无持仓',
)
pos = positions[code]
if pos.shares < quantity:
return cash, positions, TradeResult(
success=False,
code=code,
action='SELL',
price=price,
quantity=quantity,
reason=f'持仓不足(持有{pos.shares}股)',
)
trade = simulate_trade('SELL', price, quantity, fee_rate)
new_cash = cash + trade['net_proceeds']
new_shares = pos.shares - quantity
if new_shares == 0:
del positions[code]
else:
positions[code] = Position(
code=code,
shares=new_shares,
entry_price=pos.entry_price,
entry_date=pos.entry_date,
hold_days=pos.hold_days,
)
return new_cash, positions, TradeResult(
success=True,
code=code,
action='SELL',
price=price,
quantity=quantity,
fee=trade['fee'],
net_proceeds=trade['net_proceeds'],
position=positions.get(code),
)
FILE:scripts/config.py
import json
import utils
import os
from pathlib import Path
from typing import Optional
g_tmp_logic_path: str = ""
def _parse_dotenv_value(env_file: Path, key: str) -> Optional[str]:
if not env_file.exists():
return None
for line in env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
if k.strip() == key:
return v.strip()
return None
def get_token() -> str:
token = os.environ.get("BITSOUL_TOKEN")
if token:
return token
env_file = os.environ.get("BITSOUL_TOKEN_ENV_FILE")
if env_file:
token_from_file = _parse_dotenv_value(Path(env_file).expanduser(), "BITSOUL_TOKEN")
if token_from_file:
return token_from_file
return ""
def get_cache_dir() -> str:
cache_dir = os.environ.get("BITSOUL_CACHE_DIR")
if cache_dir:
return cache_dir
env_file = os.environ.get("BITSOUL_TOKEN_ENV_FILE")
if env_file:
cache_from_file = _parse_dotenv_value(Path(env_file).expanduser(), "BITSOUL_CACHE_DIR")
if cache_from_file:
return cache_from_file
return utils.get_skill_work_dir()
def get_local_version() -> str:
with open(os.path.join(utils.get_skill_assets_dir(), "config.json"), "r", encoding="utf-8") as f:
data = json.load(f)
return data["version"]
def set_tmp_logic_path(fpath: str):
global g_tmp_logic_path
g_tmp_logic_path = fpath
def get_tmp_logic_path() -> str:
global g_tmp_logic_path
return g_tmp_logic_path
FILE:scripts/data_fetcher.py
"""
data_fetcher.py
===============
封装 HTTP 数据获取接口,将远程数据拉取并持久化到本地 SQLite 数据库,
同时提供本地数据库的查询接口。
接口规范参考: API_REFERENCE.md § "通用数据查询接口"
表结构参考: DATABASE_DOCUMENTATION.md
不依赖任何第三方库,仅使用 Python 3 标准库。
"""
import json
import datetime
import sqlite3
import urllib.error
import urllib.parse
import urllib.request
import shutil
import pandas as pd
import requests
import os
import decrypt_patch
from sqlalchemy import create_engine, text, Engine
from typing import List, Optional
from define import BASE_URL, HTTP_TIMEOUT, DB_PATH, StockBasic, DailyKline, HourKline, WeeklyKline, MonthlyKline, DailyBasic, Income, StockLimit, DailyLimitList, DailyBombList, SectorStockMap, TopList, TopInst, SectorFlowDaily, IndexBasic, IndexDaily, IndexWeekly, IndexMonthly
import utils
from logger import log
import config
import remote_api
from remote_api import PatchItem
from urllib.parse import urlparse
_g_engine: Engine = None
g_table_name_to_pk = {
"stock_basic" : ["ts_code"],
"hour_kline" : ["code","date", "time"],
"daily_kline" : ["code","date"],
"weekly_kline" : ["code","date"],
"monthly_kline" : ["code","date"],
"daily_basic": ["ts_code", "trade_date"],
"income": ["ts_code", "report_type", "end_date"],
"stock_limit": ["trade_date", "ts_code"],
"daily_limit_list": ["trade_date", "ts_code"],
"daily_bomb_list": ["trade_date", "ts_code"],
"sector_stock_map": ["sector_code", "stock_code"],
"top_list": ["id"],
"top_inst": ["id"],
"sector_flow_daily": ["trade_date", "ts_code"],
"index_basic": ["ts_code"],
"index_daily": ["trade_date", "ts_code"],
"index_weekly": ["trade_date", "ts_code"],
"index_monthly": ["trade_date", "ts_code"],
}
def getEngine() -> Engine:
global _g_engine
if not _g_engine:
_g_engine = create_engine(f"sqlite:///{DB_PATH}")
return _g_engine
class TablePatch:
"""
各个表目前应用的是哪个补丁的数据
字段说明:
patch 当前表数据是用的哪个patch,patch格式patch0、patch1等
"""
__slots__ = ("patch")
def __init__(self, patch: str):
self.patch = patch
@classmethod
def from_dict(cls, d: dict) -> "TablePatch":
"""从字典(API 响应或数据库行)构造 TablePatch 对象。"""
return cls(
patch=d.get("patch") or "",
)
def __repr__(self) -> str:
return f"TablePatch(patch={self.patch!r})"
# ============================================================
# 本地 SQLite 数据库管理
# ============================================================
def init_db() -> None:
"""
初始化本地 SQLite 数据库,创建 stock_basic 和 daily_kline 表(若不存在)。
若检测到旧版本表结构(字段不匹配),自动删除旧库重建。
"""
assets_dir = utils.get_skill_assets_dir()
base_data_file = os.path.join(assets_dir, "data_1.0.bin")
if not os.path.exists(base_data_file):
log(f"本地基础数据包 data_1.0.bin 不存在,正在从服务器下载...")
if not download_data_file("data_1.0.bin", base_data_file, max_retries=3):
log("错误:基础数据包下载失败,数据库初始化中止")
return
with getEngine().connect() as conn:
conn.execute(text("""
CREATE TABLE IF NOT EXISTS stock_basic (
ts_code TEXT NOT NULL,
symbol TEXT,
name TEXT,
area TEXT,
industry TEXT,
fullname TEXT,
enname TEXT,
cnspell TEXT,
market TEXT,
exchange TEXT,
curr_type TEXT,
list_status TEXT,
list_date TEXT,
delist_date TEXT,
is_hs TEXT,
PRIMARY KEY (ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS hour_kline (
date TEXT NOT NULL,
time TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
code TEXT NOT NULL,
PRIMARY KEY (code,date,time)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_kline (
date TEXT NOT NULL,
code TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
adjustflag TEXT,
turn REAL,
pctChg REAL,
pre_close REAL,
change REAL,
PRIMARY KEY (code,date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS weekly_kline (
date TEXT NOT NULL,
code TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
pctChg REAL,
PRIMARY KEY (code,date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS monthly_kline (
date TEXT NOT NULL,
code TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
pctChg REAL,
PRIMARY KEY (code,date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_basic (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
turnover_rate REAL,
turnover_rate_f REAL,
volume_ratio REAL,
pe REAL,
pe_ttm REAL,
pb REAL,
ps REAL,
ps_ttm REAL,
dv_ratio REAL,
dv_ttm REAL,
total_share REAL,
float_share REAL,
free_share REAL,
total_mv REAL,
circ_mv REAL,
adj_factor REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS income (
ts_code TEXT NOT NULL,
end_date TEXT NOT NULL,
ann_date TEXT,
report_type TEXT NOT NULL,
comp_type TEXT,
basic_eps REAL,
diluted_eps REAL,
total_revenue REAL,
revenue REAL,
total_cogs REAL,
oper_cost REAL,
sell_exp REAL,
admin_exp REAL,
fin_exp REAL,
total_profit REAL,
income_tax REAL,
n_income REAL,
n_income_attr_p REAL,
minority_gain REAL,
oth_compr_income REAL,
t_compr_income REAL,
compr_inc_attr_p REAL,
ebit REAL,
ebitda REAL,
roe REAL,
roa REAL,
gross_margin REAL,
net_profit_margin REAL,
net_profit_yoy REAL,
revenue_yoy REAL,
equity_yoy REAL,
pcf REAL,
free_circ_mv REAL,
PRIMARY KEY (ts_code, report_type, end_date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS stock_limit (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
pre_close REAL,
up_limit REAL,
down_limit REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_limit_list (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
name TEXT,
limit_type TEXT,
limit_price REAL,
pct_chg REAL,
volume REAL,
amount REAL,
limit_streak INTEGER,
sector TEXT,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_bomb_list (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
name TEXT,
bomb_type TEXT,
limit_price REAL,
pct_chg REAL,
volume REAL,
amount REAL,
sector TEXT,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS sector_stock_map (
sector_code TEXT NOT NULL,
stock_code TEXT NOT NULL,
sector_name TEXT,
source TEXT,
PRIMARY KEY (sector_code, stock_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS top_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT,
ts_code TEXT,
name TEXT,
close REAL,
pct_change REAL,
turnover_rate REAL,
amount REAL,
l_sell REAL,
l_buy REAL,
l_amount REAL,
net_amount REAL,
net_rate REAL,
amount_rate REAL,
float_values REAL,
reason TEXT
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS top_inst (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT,
ts_code TEXT,
exalter TEXT,
side TEXT,
buy REAL,
buy_rate REAL,
sell REAL,
sell_rate REAL,
net_buy REAL,
reason TEXT
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS sector_flow_daily (
trade_date TEXT NOT NULL,
content_type TEXT,
ts_code TEXT NOT NULL,
name TEXT,
pct_change REAL,
close REAL,
net_amount REAL,
net_amount_rate REAL,
buy_elg_amount REAL,
buy_elg_amount_rate REAL,
buy_lg_amount REAL,
buy_lg_amount_rate REAL,
buy_md_amount REAL,
buy_md_amount_rate REAL,
buy_sm_amount REAL,
buy_sm_amount_rate REAL,
buy_sm_amount_stock TEXT,
rank INTEGER,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_basic (
ts_code TEXT NOT NULL,
name TEXT,
fullname TEXT,
market TEXT,
publisher TEXT,
index_type TEXT,
category TEXT,
base_date TEXT,
base_point REAL,
list_date TEXT,
weight_rule TEXT,
desc TEXT,
exp_date TEXT,
PRIMARY KEY (ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_daily (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
open REAL,
high REAL,
low REAL,
pre_close REAL,
change REAL,
pct_chg REAL,
vol REAL,
amount REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_weekly (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
open REAL,
high REAL,
low REAL,
pre_close REAL,
change REAL,
pct_chg REAL,
vol REAL,
amount REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_monthly (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
open REAL,
high REAL,
low REAL,
pre_close REAL,
change REAL,
pct_chg REAL,
vol REAL,
amount REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS table_patch (
patch TEXT NOT NULL
)
"""))
conn.commit()
# ============================================================
# 本地 SQLite 查询接口
# ============================================================
def query_stock_basic(
ts_code: Optional[str] = None,
industry: Optional[str] = None,
industry_keyword: Optional[str] = None,
area: Optional[str] = None,
market: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[StockBasic]:
"""
从本地 SQLite 数据库查询 stock_basic 表,返回 StockBasic 对象列表。
参数:
ts_code 按股票代码精确过滤
industry 按行业名称精确过滤
industry_keyword 按行业名称关键词模糊过滤(LIKE %keyword%),与 industry 互斥,
优先使用 industry_keyword
area 按地区精确过滤
market 按市场精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
返回:
List[StockBasic] 符合条件的股票基础信息对象列表
示例:
all_stocks = query_stock_basic()
bank_stocks = query_stock_basic(industry="银行")
chip_stocks = query_stock_basic(industry_keyword="半导体")
single_stock = query_stock_basic(ts_code="000001.SZ")
"""
conditions = []
params: dict = {}
if ts_code is not None:
conditions.append("ts_code = :ts_code")
params["ts_code"] = ts_code
if industry_keyword is not None:
conditions.append("industry LIKE :industry_keyword")
params["industry_keyword"] = f"%{industry_keyword}%"
elif industry is not None:
conditions.append("industry = :industry")
params["industry"] = industry
if area is not None:
conditions.append("area = :area")
params["area"] = area
if market is not None:
conditions.append("market = :market")
params["market"] = market
sql = "SELECT * FROM stock_basic"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [StockBasic.from_dict(dict(row._mapping)) for row in rows]
def query_daily_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC",
) -> List[DailyKline]:
"""
从本地 SQLite 数据库查询 daily_kline 表,返回 DailyKline 对象列表。
参数:
code 按股票代码精确过滤
date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC"
返回:
List[DailyKline] 符合条件的日线行情对象列表
示例:
# 查询某只股票全部历史行情(按日期升序)
klines = query_daily_kline(code=["sz.000001"])
# 查询某只股票某段时间行情,最新的 30 条
klines = query_daily_kline(code=["sz.000001"],
start_date="2024-01-01", end_date="2024-12-31",
limit=30, order_by="date DESC")
# 查询某天全市场行情
klines = query_daily_kline(date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM daily_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
# SQLite IN 子句上限 999,codes 过多时分批查询后合并
if len(codes) > 900:
result = []
for i in range(0, len(codes), 900):
result.extend(
query_daily_kline(
codes=codes[i: i + 900],
date=date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
)
return result
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyKline.from_dict(dict(row._mapping)) for row in rows]
def query_hour_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC, time ASC",
) -> List[HourKline]:
"""
从本地 SQLite 数据库查询 hour_kline 表,返回 HourKline 对象列表。
参数:
codes 按股票代码列表过滤
date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC, time ASC"
返回:
List[HourKline] 符合条件的小时级别 K 线对象列表
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM hour_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [HourKline.from_dict(dict(row._mapping)) for row in rows]
def query_weekly_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC",
) -> List[WeeklyKline]:
"""
从本地 SQLite 数据库查询 weekly_kline 表,返回 WeeklyKline 对象列表。
参数:
codes 按股票代码列表过滤
date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC"
返回:
List[WeeklyKline] 符合条件的周线 K 线对象列表
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM weekly_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [WeeklyKline.from_dict(dict(row._mapping)) for row in rows]
def query_monthly_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC",
) -> List[MonthlyKline]:
"""
从本地 SQLite 数据库查询 monthly_kline 表,返回 MonthlyKline 对象列表。
参数:
codes 按股票代码列表过滤
date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC"
返回:
List[MonthlyKline] 符合条件的月线 K 线对象列表
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM monthly_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [MonthlyKline.from_dict(dict(row._mapping)) for row in rows]
def query_daily_basic(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBasic]:
"""
从本地 SQLite 数据库查询 daily_basic 表,返回 DailyBasic 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBasic] 符合条件的每日基本面指标对象列表
示例:
# 查询某只股票全部历史基本面数据
basics = query_daily_basic(ts_codes=["000001.SZ"])
# 查询某天全市场基本面数据
basics = query_daily_basic(trade_date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM daily_basic"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyBasic.from_dict(dict(row._mapping)) for row in rows]
def query_income(
ts_codes: List[str] = [],
report_type: Optional[str] = None,
end_date: Optional[str] = None,
start_end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "end_date ASC",
) -> List[Income]:
"""
从本地 SQLite 数据库查询 income 表,返回 Income 对象列表。
参数:
ts_codes 按股票代码列表过滤
report_type 按报告类型精确过滤(如 "1" 表示合并报表)
end_date 按报告期结束日期精确过滤,格式 "YYYY-MM-DD"
start_end_date 按报告期结束日期范围过滤下限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "end_date ASC"
返回:
List[Income] 符合条件的利润表对象列表
示例:
# 查询某只股票全部利润表(合并报表)
records = query_income(ts_codes=["000001.SZ"], report_type="1")
# 查询某报告期全市场数据
records = query_income(end_date="20231231")
# 查询最新一期
records = query_income(ts_codes=["000001.SZ"], order_by="end_date DESC", limit=1)
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if report_type is not None:
conditions.append("report_type = :report_type")
params["report_type"] = report_type
if end_date is not None:
conditions.append("end_date = :end_date")
params["end_date"] = end_date
elif start_end_date is not None:
conditions.append("end_date >= :start_end_date")
params["start_end_date"] = start_end_date
sql = "SELECT * FROM income"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [Income.from_dict(dict(row._mapping)) for row in rows]
def query_stock_limit(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[StockLimit]:
"""
从本地 SQLite 数据库查询 stock_limit 表,返回 StockLimit 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[StockLimit] 符合条件的每日涨跌停价格对象列表
示例:
# 查询某只股票的涨跌停价格历史
limits = query_stock_limit(ts_codes=["000001.SZ"])
# 查询某天全市场涨跌停价格
limits = query_stock_limit(trade_date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM stock_limit"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [StockLimit.from_dict(dict(row._mapping)) for row in rows]
def query_daily_limit_list(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyLimitList]:
"""
从本地 SQLite 数据库查询 daily_limit_list 表,返回 DailyLimitList 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit_type 按榜单类型过滤(U=涨停, D=跌停)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyLimitList] 符合条件的每日涨跌停榜单对象列表
示例:
# 查询某天所有涨停股
records = query_daily_limit_list(trade_date="2024-06-03", limit_type="U")
# 查询某只股票历史上榜记录
records = query_daily_limit_list(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
if limit_type is not None:
conditions.append("limit_type = :limit_type")
params["limit_type"] = limit_type
sql = "SELECT * FROM daily_limit_list"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyLimitList.from_dict(dict(row._mapping)) for row in rows]
def query_daily_bomb_list(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
bomb_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBombList]:
"""
从本地 SQLite 数据库查询 daily_bomb_list 表,返回 DailyBombList 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
bomb_type 按炸板类型过滤(U=曾涨停, D=曾跌停/撬板)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBombList] 符合条件的每日炸板榜单对象列表
示例:
# 查询某天所有炸板(曾涨停)股票
records = query_daily_bomb_list(trade_date="2024-06-03", bomb_type="U")
# 查询某只股票历史炸板记录
records = query_daily_bomb_list(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
if bomb_type is not None:
conditions.append("bomb_type = :bomb_type")
params["bomb_type"] = bomb_type
sql = "SELECT * FROM daily_bomb_list"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyBombList.from_dict(dict(row._mapping)) for row in rows]
def query_sector_stock_map(
sector_codes: List[str] = [],
stock_codes: List[str] = [],
source: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[SectorStockMap]:
"""
从本地 SQLite 数据库查询 sector_stock_map 表,返回 SectorStockMap 对象列表。
参数:
sector_codes 按板块代码列表过滤
stock_codes 按股票代码列表过滤
source 按数据来源精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询某个板块下的所有股票
records = query_sector_stock_map(sector_codes=["BK0475"])
# 查询某只股票归属的所有板块
records = query_sector_stock_map(stock_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(sector_codes) != 0:
keys = [f"sector_code_{i}" for i in range(len(sector_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"sector_code IN ({placeholders})")
for k, v in zip(keys, sector_codes):
params[k] = v
if len(stock_codes) != 0:
keys = [f"stock_code_{i}" for i in range(len(stock_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"stock_code IN ({placeholders})")
for k, v in zip(keys, stock_codes):
params[k] = v
if source is not None:
conditions.append("source = :source")
params["source"] = source
sql = "SELECT * FROM sector_stock_map"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [SectorStockMap.from_dict(dict(row._mapping)) for row in rows]
def query_top_list(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopList]:
"""
从本地 SQLite 数据库查询 top_list 表,返回 TopList 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天龙虎榜数据
records = query_top_list(trade_date="2024-06-03")
# 查询某只股票历史上榜记录
records = query_top_list(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM top_list"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [TopList.from_dict(dict(row._mapping)) for row in rows]
def query_top_inst(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
side: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopInst]:
"""
从本地 SQLite 数据库查询 top_inst 表,返回 TopInst 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
side 按买卖类型过滤("0"=买入, "1"=卖出)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天机构交易明细
records = query_top_inst(trade_date="2024-06-03")
# 查询某只股票历史机构上榜记录
records = query_top_inst(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
if side is not None:
conditions.append("side = :side")
params["side"] = side
sql = "SELECT * FROM top_inst"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [TopInst.from_dict(dict(row._mapping)) for row in rows]
def query_sector_flow_daily(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[SectorFlowDaily]:
"""
从本地 SQLite 数据库查询 sector_flow_daily 表,返回 SectorFlowDaily 对象列表。
参数:
ts_codes 按板块代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天所有板块资金流向
records = query_sector_flow_daily(trade_date="2024-06-03")
# 查询某个板块历史资金流向
records = query_sector_flow_daily(ts_codes=["BK0475"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM sector_flow_daily"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [SectorFlowDaily.from_dict(dict(row._mapping)) for row in rows]
def query_index_basic(
ts_code: Optional[str] = None,
market: Optional[str] = None,
publisher: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[IndexBasic]:
"""
从本地 SQLite 数据库查询 index_basic 表,返回 IndexBasic 对象列表。
参数:
ts_code 按指数代码精确过滤
market 按市场精确过滤
publisher 按发布方精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询所有指数
records = query_index_basic()
# 查询上证指数信息
records = query_index_basic(ts_code="000001.SH")
"""
conditions = []
params: dict = {}
if ts_code is not None:
conditions.append("ts_code = :ts_code")
params["ts_code"] = ts_code
if market is not None:
conditions.append("market = :market")
params["market"] = market
if publisher is not None:
conditions.append("publisher = :publisher")
params["publisher"] = publisher
sql = "SELECT * FROM index_basic"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexBasic.from_dict(dict(row._mapping)) for row in rows]
def query_index_daily(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexDaily]:
"""
从本地 SQLite 数据库查询 index_daily 表,返回 IndexDaily 对象列表。
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数历史日线
records = query_index_daily(ts_codes=["000001.SH"])
# 查询某天所有指数行情
records = query_index_daily(trade_date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM index_daily"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexDaily.from_dict(dict(row._mapping)) for row in rows]
def query_index_weekly(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexWeekly]:
"""
从本地 SQLite 数据库查询 index_weekly 表,返回 IndexWeekly 对象列表。
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数周线
records = query_index_weekly(ts_codes=["000001.SH"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM index_weekly"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexWeekly.from_dict(dict(row._mapping)) for row in rows]
def query_index_monthly(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexMonthly]:
"""
从本地 SQLite 数据库查询 index_monthly 表,返回 IndexMonthly 对象列表。
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数月线
records = query_index_monthly(ts_codes=["000001.SH"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM index_monthly"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexMonthly.from_dict(dict(row._mapping)) for row in rows]
def get_local_patch_ver() -> int:
"""从 table_patch 表中读取当前本地 patch 版本号,无记录时返回 -1。"""
with getEngine().connect() as conn:
row = conn.execute(text("SELECT patch FROM table_patch LIMIT 1")).fetchone()
print(int(row[0]) if row else -1)
return int(row[0]) if row else -1
def download_data_file(file_name: str, output_path: str, max_retries: int = 3) -> bool:
"""
从服务器下载数据文件。
参数:
file_name: 要下载的文件名(如 data_1.0.bin)
output_path: 保存路径
max_retries: 最大重试次数
返回:
bool: 下载成功返回 True,否则返回 False
"""
token = config.get_token()
if not token:
log("错误:未设置 Token,无法下载数据文件")
return False
for retry in range(max_retries):
try:
download_url = remote_api.request_download_url(file_name, token)
if not download_url:
log(f"错误:获取 {file_name} 下载链接失败,请检查 Token 是否有效")
return False
log(f"开始下载 {file_name} ...")
log(f"下载地址: {download_url}")
with requests.get(download_url, stream=True, timeout=300) as response:
if response.status_code != 200:
log(f"下载失败,HTTP 状态码: {response.status_code}")
if retry < max_retries - 1:
log(f"重试 {retry + 1}/{max_retries} ...")
continue
return False
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
chunk_size = 1024 * 1024
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
pct = (downloaded / total_size) * 100
if downloaded % (10 * chunk_size) < chunk_size:
log(f"下载进度: {pct:.1f}%")
log(f"文件下载完成: {output_path}")
return True
except Exception as e:
log(f"下载出错: {str(e)}")
if retry < max_retries - 1:
log(f"重试 {retry + 1}/{max_retries} ...")
else:
log(f"下载失败,已达到最大重试次数")
return False
return False
def download_from_url(url: str, output_path: str, timeout: int = 300) -> bool:
"""
从指定 URL 下载文件到指定路径。
参数:
url: 下载链接
output_path: 保存路径
timeout: 超时时间(秒)
返回:
bool: 下载成功返回 True,否则返回 False
"""
try:
log(f"从 URL 下载文件: {url}")
log(f"保存路径: {output_path}")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with requests.get(url, stream=True, timeout=timeout) as response:
if response.status_code != 200:
log(f"下载失败,HTTP 状态码: {response.status_code}")
return False
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
chunk_size = 1024 * 1024
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
pct = downloaded / total_size * 100
if downloaded % (10 * chunk_size) < chunk_size:
log(f"下载进度: {pct:.1f}%")
log(f"文件下载完成: {output_path}")
return True
except Exception as e:
log(f"下载出错: {str(e)}")
return False
def syn_table_datas() -> List[str]:
"""
根据表名,获取需要下载的 patch 列表。
逻辑:
1. 调用 request_patch_list() 获取服务器上该表的全部可用 patch 列表
2. 查询本地 table_patch 表,找到该表当前已应用的 patch
3. 返回当前 patch 之后(不含)的所有 patch,即待下载的部分;
若本地无记录,则返回全部可用 patch
参数:
table_name 指定表的名称
返回:
List[str] 待下载的 patch 名称列表(按顺序)
"""
local_patch_ver = -1
with getEngine().connect() as conn:
cursor = conn.execute(
text("SELECT patch FROM table_patch")
)
row = cursor.fetchone()
if row:
local_patch_ver = int(row[0])
conn.commit()
log(f"本地数据patch ver:{local_patch_ver}")
if local_patch_ver < 0:
log(f"导入基础数据...")
assets_dir = utils.get_skill_assets_dir()
name = "data_1.0.bin"
base_patch_zip = os.path.join(assets_dir, name)
base_patch_decrypt_zip = os.path.join(utils.get_skill_work_dir(), "data_1.0_decrypt.zip")
decrypt_key = remote_api.request_decrypt_key(name, config.get_token())
if len(decrypt_key) == 0:
log("错误:没有数据读取权限,请先注册")
return
decrypt_patch.process_file(base_patch_zip, base_patch_decrypt_zip, decrypt_key, False)
base_patch_dir = os.path.join(utils.get_skill_work_dir(), "data_1.0")
if os.path.exists(base_patch_dir):
shutil.rmtree(base_patch_dir)
utils.unzip_file(base_patch_decrypt_zip, base_patch_dir)
import_datas_in_dir(base_patch_dir)
with getEngine().connect() as conn:
conn.execute(text("DELETE FROM table_patch"))
conn.execute(text("INSERT INTO table_patch (patch) VALUES (0)"))
conn.commit()
os.remove(base_patch_decrypt_zip)
shutil.rmtree(base_patch_dir)
remote_patchs: List[PatchItem] = remote_api.request_patch_list()
log(f"remote patch list:{','.join([str(r_patch.version) for r_patch in remote_patchs])}")
for r_patch in remote_patchs:
if r_patch.version > local_patch_ver:
request_and_import_remote_patch_by_name(r_patch.patch_name, r_patch.version)
def request_and_import_remote_patch_by_name(patch_name:str, patch_ver: int):
log(f"更新remote patch ver:{patch_ver}, name:{patch_name}......")
url = f"{BASE_URL}/api/download_file"
params = {
"file_name": patch_name,
"token_key": config.get_token()
}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
download_url = data.get("download_url")
tmp_patch_zip = os.path.join(utils.get_skill_work_dir(), patch_name)
tmp_patch_decrypt_zip = os.path.join(utils.get_skill_work_dir(), f"decrypt_{patch_name}")
tmp_patch_dir = os.path.join(utils.get_skill_work_dir(), "tmp_patch_unzip")
# 下载
utils.download_file(download_url, tmp_patch_zip)
# 解密
decrypt_key = remote_api.request_decrypt_key(patch_name, config.get_token())
if len(decrypt_key) == 0:
log("错误:没有数据读取权限,请先注册")
return
decrypt_patch.process_file(tmp_patch_zip, tmp_patch_decrypt_zip, decrypt_key, False)
# 解压
utils.unzip_file(tmp_patch_decrypt_zip, tmp_patch_dir)
import_datas_in_dir(tmp_patch_dir)
with getEngine().connect() as conn:
conn.execute(text("DELETE FROM table_patch"))
conn.execute(text(f"INSERT INTO table_patch (patch) VALUES ({patch_ver})"))
conn.commit()
shutil.rmtree(tmp_patch_dir)
os.remove(tmp_patch_zip)
os.remove(tmp_patch_decrypt_zip)
log(f"更新本地数据patch ver:{patch_ver}")
def import_datas_in_dir(dir: str):
files = utils.scan_files_in_dir(dir)
for file in files:
basename = os.path.basename(file)
for table_name in g_table_name_to_pk.keys():
if basename.startswith(table_name):
import_data_to_table(file, table_name)
def import_data_to_table(input_file:str, table_name:str):
log(f"导入表{table_name} from {input_file}")
if input_file.endswith('.csv.gz'):
df = pd.read_csv(input_file, compression='gzip')
elif input_file.endswith('.csv'):
df = pd.read_csv(input_file)
elif input_file.endswith('.pkl'):
df = pd.read_pickle(input_file)
else:
assert False
engine = getEngine()
raw_conn = engine.raw_connection()
try:
df.to_sql("tmp_import", raw_conn, if_exists="replace", index=False)
cursor = raw_conn.cursor()
cursor.execute(f"INSERT OR REPLACE INTO {table_name} SELECT * FROM tmp_import")
raw_conn.commit()
cursor.close()
finally:
raw_conn.close()
def syn_vip_basic_data():
"""
更新vip基础数据包
"""
url = f"{BASE_URL}/api/history_data"
response = requests.get(url, params={"token": config.get_token()})
if response.status_code == 200:
file_url = response.json().get("download_url")
filename = os.path.basename(urlparse(file_url).path)
print("下载中...")
tmp_patch_zip = os.path.join(utils.get_skill_work_dir(), filename)
tmp_patch_decrypt_zip = os.path.join(utils.get_skill_work_dir(), f"decrypt_{filename}")
tmp_patch_dir = os.path.join(utils.get_skill_work_dir(), "tmp_history_unzip")
decrypt_key = remote_api.request_decrypt_key(filename, config.get_token())
# 解密
if len(decrypt_key) == 0:
log("错误:没有数据读取权限,请先注册")
return
# 下载
utils.download_file(file_url, tmp_patch_zip)
decrypt_patch.process_file(tmp_patch_zip, tmp_patch_decrypt_zip, decrypt_key, False)
# 解压
utils.unzip_file(tmp_patch_decrypt_zip, tmp_patch_dir)
import_datas_in_dir(tmp_patch_dir)
if __name__ == "__main__":
log(f"数据库路径:{DB_PATH}")
init_db()
syn_table_datas()
# syn_vip_basic_data()
# testfunc()
FILE:scripts/decrypt_patch.py
import os
import hashlib
import random
import struct
import argparse
def derive_seed(key, iv):
"""Derive a seed for the random number generator from Key and IV."""
h = hashlib.sha256()
h.update(key.encode('utf-8'))
h.update(iv)
return int.from_bytes(h.digest(), 'big')
def process_file(input_path, output_path, key, is_encrypt=True):
"""
Encrypt or Decrypt a file using a stream cipher based on Python's random (Mersenne Twister).
Uses large integer XOR for performance.
"""
chunk_size = 1024 * 1024 # 1MB chunks
with open(input_path, 'rb') as fin, open(output_path, 'wb') as fout:
if is_encrypt:
# Generate IV
iv = os.urandom(16)
fout.write(iv)
else:
# Read IV
iv = fin.read(16)
if len(iv) < 16:
raise ValueError("File too short or corrupted.")
# Initialize PRNG
seed = derive_seed(key, iv)
rng = random.Random(seed)
while True:
chunk = fin.read(chunk_size)
if not chunk:
break
# Generate mask (compatible with Python < 3.9, same as encrypt)
mask = rng.getrandbits(len(chunk) * 8).to_bytes(len(chunk), 'little')
# Fast XOR using integers
chunk_int = int.from_bytes(chunk, 'little')
mask_int = int.from_bytes(mask, 'little')
encrypted_int = chunk_int ^ mask_int
# Convert back to bytes
encrypted_chunk = encrypted_int.to_bytes(len(chunk), 'little')
fout.write(encrypted_chunk)
FILE:scripts/define.py
import os
import json
from typing import Optional
import utils
import config
def _load_config():
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "config.json")
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {"base_url": "", "http_timeout": 30}
_config = _load_config()
# ============================================================
# 常量
# ============================================================
BASE_URL = _config.get("base_url", "")
HTTP_TIMEOUT = _config.get("http_timeout", 30)
DB_PATH = os.path.join(config.get_cache_dir(), "data.db")
def get_cache_dir() -> str:
return config.get_cache_dir()
# ============================================================
# 数据模型
# ============================================================
class RealtimeStockQuote:
"""
实时股票报价信息
字段说明:
ts_code 股票代码(如 000001.SZ)
name 股票名称
open 今日开盘价
pre_close 昨日收盘价
price 当前最新价
high 今日最高价
low 今日最低价
bid 买一价
ask 卖一价
volume 成交量(股)
amount 成交额(元)
date 交易日期(YYYY-MM-DD)
time 最新报价时间(HH:MM:SS)
amplitude 振幅(%)
turnover_rate 换手率(%),可能为空
total_cap 总市值(元),可能为空
circ_cap 流通市值(元),可能为空
pb 市净率,可能为空
pe_ttm 市盈率(TTM),可能为空
total_shares 总股本(股),可能为空
circ_shares 流通股本(股),可能为空
status 请求状态(success / error)
"""
__slots__ = ("ts_code", "name", "open", "pre_close", "price",
"high", "low", "bid", "ask", "volume", "amount",
"date", "time", "amplitude", "turnover_rate",
"total_cap", "circ_cap", "pb", "pe_ttm",
"total_shares", "circ_shares", "status")
def __init__(self, ts_code: str, name: str, open: float, pre_close: float,
price: float, high: float, low: float, bid: float, ask: float,
volume: int, amount: float, date: str, time: str,
amplitude: float, turnover_rate: Optional[float],
total_cap: Optional[float], circ_cap: Optional[float],
pb: Optional[float], pe_ttm: Optional[float],
total_shares: Optional[float], circ_shares: Optional[float],
status: str):
self.ts_code = ts_code
self.name = name
self.open = open
self.pre_close = pre_close
self.price = price
self.high = high
self.low = low
self.bid = bid
self.ask = ask
self.volume = volume
self.amount = amount
self.date = date
self.time = time
self.amplitude = amplitude
self.turnover_rate = turnover_rate
self.total_cap = total_cap
self.circ_cap = circ_cap
self.pb = pb
self.pe_ttm = pe_ttm
self.total_shares = total_shares
self.circ_shares = circ_shares
self.status = status
@classmethod
def from_dict(cls, d: dict) -> "RealtimeStockQuote":
def _f(v):
try:
return float(v) if v is not None and v != "" else None
except (TypeError, ValueError):
return None
return cls(
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
open=float(d.get("open") or 0.0),
pre_close=float(d.get("pre_close") or 0.0),
price=float(d.get("price") or 0.0),
high=float(d.get("high") or 0.0),
low=float(d.get("low") or 0.0),
bid=float(d.get("bid") or 0.0),
ask=float(d.get("ask") or 0.0),
volume=int(d.get("volume") or 0),
amount=float(d.get("amount") or 0.0),
date=d.get("date") or "",
time=d.get("time") or "",
amplitude=float(d.get("amplitude") or 0.0),
turnover_rate=_f(d.get("turnover_rate")),
total_cap=_f(d.get("total_cap")),
circ_cap=_f(d.get("circ_cap")),
pb=_f(d.get("pb")),
pe_ttm=_f(d.get("pe_ttm")),
total_shares=_f(d.get("total_shares")),
circ_shares=_f(d.get("circ_shares")),
status=d.get("status") or "",
)
def __repr__(self) -> str:
return (f"RealtimeStockQuote(ts_code={self.ts_code!r}, name={self.name!r}, "
f"price={self.price}, date={self.date!r})")
class StockBasic:
"""
股票基础信息,对应远程 stock_basic 表及本地同名表。
字段说明:
ts_code 股票代码,如 000001.SZ
symbol 股票符号,如 000001
name 股票名称,如 平安银行
area 所在地区
industry 所属行业
fullname 股票全称
enname 英文名称
cnspell 拼音
market 市场类型(主板/创业板/科创板等)
exchange 交易所代码
curr_type 交易货币
list_date 上市日期,格式 YYYY-MM-DD
list_status 上市状态 (L=上市, D=退市, G=过会未交易, P=暂停上市)
delist_date 退市日期(未退市则为空),格式 YYYY-MM-DD
is_hs 是否沪深港通标的(N=否, H=沪股通, S=深股通)
"""
__slots__ = ("ts_code", "symbol", "name", "area", "industry",
"fullname", "enname", "cnspell", "market", "exchange",
"curr_type", "list_date", "list_status", "delist_date", "is_hs")
def __init__(self, ts_code: str, symbol: str, name: str,
area: str, industry: str, fullname: str, enname: str,
cnspell: str, market: str, exchange: str, curr_type: str,
list_date: str, list_status:str, delist_date: str, is_hs: str):
self.ts_code = ts_code
self.symbol = symbol
self.name = name
self.area = area
self.industry = industry
self.fullname = fullname
self.enname = enname
self.cnspell = cnspell
self.market = market
self.exchange = exchange
self.curr_type = curr_type
self.list_date = list_date
self.list_status = list_status
self.delist_date = delist_date
self.is_hs = is_hs
@classmethod
def from_dict(cls, d: dict) -> "StockBasic":
"""从字典(API 响应或数据库行)构造 StockBasic 对象。"""
return cls(
ts_code=d.get("ts_code") or "",
symbol=d.get("symbol") or "",
name=d.get("name") or "",
area=d.get("area") or "",
industry=d.get("industry") or "",
fullname=d.get("fullname") or "",
enname=d.get("enname") or "",
cnspell=d.get("cnspell") or "",
market=d.get("market") or "",
exchange=d.get("exchange") or "",
curr_type=d.get("curr_type") or "",
list_date=d.get("list_date") or "",
list_status=d.get("list_status") or "",
delist_date=d.get("delist_date") or "",
is_hs=d.get("is_hs") or "",
)
def __repr__(self) -> str:
return f"StockBasic(ts_code={self.ts_code!r}, name={self.name!r}, market={self.market!r})"
class DailyKline:
"""
日线行情数据,对应远程 daily_kline 表及本地同名表。
字段说明:
date 交易日期,格式 YYYY-MM-DD
code 股票代码,如 sz.000001
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量(股)
amount 成交额(元)
adjustflag 复权状态
turn 换手率
pctChg 涨跌幅(%)
pre_close 前收盘价
change 涨跌额
"""
__slots__ = ("date", "code", "open", "high", "low", "close",
"volume", "amount", "adjustflag", "turn", "pctChg",
"pre_close", "change")
def __init__(self, date: str, code: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
adjustflag: str, turn: float, pctChg: float,
pre_close: float, change: float):
self.date = date
self.code = code
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.adjustflag = adjustflag
self.turn = turn
self.pctChg = pctChg
self.pre_close = pre_close
self.change = change
@classmethod
def from_dict(cls, d: dict) -> "DailyKline":
"""从字典(API 响应或数据库行)构造 DailyKline 对象。"""
def _f(v):
"""将值安全转换为 float,None/空字符串返回 0.0。"""
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
code=d.get("code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
adjustflag=d.get("adjustflag") or "",
turn=_f(d.get("turn")),
pctChg=_f(d.get("pctChg")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
)
def __repr__(self) -> str:
return (f"DailyKline(date={self.date!r}, code={self.code!r}, "
f"close={self.close}, pctChg={self.pctChg})")
class HourKline:
"""
小时级别 K 线行情数据,对应本地 hour_kline 表。
字段说明:
date 交易日期,格式 "YYYY-MM-DD"
time 交易时间
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量
amount 成交额
code 股票代码
"""
__slots__ = ("date", "time", "open", "high", "low", "close",
"volume", "amount", "code")
def __init__(self, date: str, time: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
code: str):
self.date = date
self.time = time
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.code = code
@classmethod
def from_dict(cls, d: dict) -> "HourKline":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
time=d.get("time") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
code=d.get("code") or "",
)
def __repr__(self) -> str:
return (f"HourKline(date={self.date!r}, time={self.time!r}, "
f"code={self.code!r}, close={self.close})")
class WeeklyKline:
"""
周线行情数据,对应本地 weekly_kline 表。
字段说明:
date 交易日期(周五日期),格式 YYYY-MM-DD
code 股票代码
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量
amount 成交额
pctChg 涨跌幅(%)
"""
__slots__ = ("date", "code", "open", "high", "low", "close",
"volume", "amount", "pctChg")
def __init__(self, date: str, code: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
pctChg: float):
self.date = date
self.code = code
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.pctChg = pctChg
@classmethod
def from_dict(cls, d: dict) -> "WeeklyKline":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
code=d.get("code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
pctChg=_f(d.get("pctChg")),
)
def __repr__(self) -> str:
return (f"WeeklyKline(date={self.date!r}, code={self.code!r}, "
f"close={self.close}, pctChg={self.pctChg})")
class MonthlyKline:
"""
月线行情数据,对应本地 monthly_kline 表。
字段说明:
date 交易日期(月末日期),格式 YYYY-MM-DD
code 股票代码
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量
amount 成交额
pctChg 涨跌幅(%)
"""
__slots__ = ("date", "code", "open", "high", "low", "close",
"volume", "amount", "pctChg")
def __init__(self, date: str, code: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
pctChg: float):
self.date = date
self.code = code
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.pctChg = pctChg
@classmethod
def from_dict(cls, d: dict) -> "MonthlyKline":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
code=d.get("code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
pctChg=_f(d.get("pctChg")),
)
def __repr__(self) -> str:
return (f"MonthlyKline(date={self.date!r}, code={self.code!r}, "
f"close={self.close}, pctChg={self.pctChg})")
class DailyBasic:
"""
每日基本面指标数据,对应本地 daily_basic 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
close 当日收盘价
turnover_rate 换手率(%)
turnover_rate_f 换手率(自由流通股)
volume_ratio 量比
pe 市盈率(总市值/净利润)
pe_ttm 市盈率(TTM)
pb 市净率(总市值/净资产)
ps 市销率
ps_ttm 市销率(TTM)
dv_ratio 股息率(%)
dv_ttm 股息率(TTM)(%)
total_share 总股本(万股)
float_share 流通股本(万股)
free_share 自由流通股本(万)
total_mv 总市值(万元)
circ_mv 流通市值(万元)
adj_factor 复权因子
"""
__slots__ = ("trade_date", "ts_code", "close", "turnover_rate",
"turnover_rate_f", "volume_ratio", "pe", "pe_ttm",
"pb", "ps", "ps_ttm", "dv_ratio", "dv_ttm",
"total_share", "float_share", "free_share",
"total_mv", "circ_mv", "adj_factor")
def __init__(self, trade_date: str, ts_code: str, close: float,
turnover_rate: float, turnover_rate_f: float, volume_ratio: float,
pe: float, pe_ttm: float, pb: float, ps: float, ps_ttm: float,
dv_ratio: float, dv_ttm: float, total_share: float,
float_share: float, free_share: float, total_mv: float,
circ_mv: float, adj_factor: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.close = close
self.turnover_rate = turnover_rate
self.turnover_rate_f = turnover_rate_f
self.volume_ratio = volume_ratio
self.pe = pe
self.pe_ttm = pe_ttm
self.pb = pb
self.ps = ps
self.ps_ttm = ps_ttm
self.dv_ratio = dv_ratio
self.dv_ttm = dv_ttm
self.total_share = total_share
self.float_share = float_share
self.free_share = free_share
self.total_mv = total_mv
self.circ_mv = circ_mv
self.adj_factor = adj_factor
@classmethod
def from_dict(cls, d: dict) -> "DailyBasic":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
close=_f(d.get("close")),
turnover_rate=_f(d.get("turnover_rate")),
turnover_rate_f=_f(d.get("turnover_rate_f")),
volume_ratio=_f(d.get("volume_ratio")),
pe=_f(d.get("pe")),
pe_ttm=_f(d.get("pe_ttm")),
pb=_f(d.get("pb")),
ps=_f(d.get("ps")),
ps_ttm=_f(d.get("ps_ttm")),
dv_ratio=_f(d.get("dv_ratio")),
dv_ttm=_f(d.get("dv_ttm")),
total_share=_f(d.get("total_share")),
float_share=_f(d.get("float_share")),
free_share=_f(d.get("free_share")),
total_mv=_f(d.get("total_mv")),
circ_mv=_f(d.get("circ_mv")),
adj_factor=_f(d.get("adj_factor")),
)
def __repr__(self) -> str:
return (f"DailyBasic(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pe={self.pe}, pb={self.pb})")
class Income:
"""
利润表数据,对应本地 income 表。
字段说明:
ts_code 股票代码(PK)
end_date 报告期结束日期(PK),格式 YYYY-MM-DD
ann_date 公告日期,格式 YYYY-MM-DD
report_type 报告类型(PK,1=合并报表)
comp_type 公司类型
basic_eps 基本每股收益
diluted_eps 稀释每股收益
total_revenue 营业总收入
revenue 营业收入
total_cogs 营业总成本
oper_cost 营业成本
sell_exp 销售费用
admin_exp 管理费用
fin_exp 财务费用
total_profit 利润总额
income_tax 所得税费用
n_income 净利润
n_income_attr_p 归属于母公司所有者的净利润
minority_gain 少数股东损益
oth_compr_income 其他综合收益
t_compr_income 综合收益总额
compr_inc_attr_p 归属于母公司所有者的综合收益总额
ebit 息税前利润
ebitda 息税折旧摊销前利润
roe 净资产收益率(%)
roa 总资产收益率(%)
gross_margin 毛利率(%)
net_profit_margin 净利率(%)
net_profit_yoy 净利润增长率(%)
revenue_yoy 营业收入增长率(%)
equity_yoy 净资产增长率(%)
pcf 市现率
free_circ_mv 自由流通市值
"""
__slots__ = (
"ts_code", "end_date", "ann_date", "report_type", "comp_type",
"basic_eps", "diluted_eps", "total_revenue", "revenue",
"total_cogs", "oper_cost", "sell_exp", "admin_exp", "fin_exp",
"total_profit", "income_tax", "n_income", "n_income_attr_p",
"minority_gain", "oth_compr_income", "t_compr_income", "compr_inc_attr_p",
"ebit", "ebitda", "roe", "roa", "gross_margin", "net_profit_margin",
"net_profit_yoy", "revenue_yoy", "equity_yoy", "pcf", "free_circ_mv",
)
def __init__(
self,
ts_code: str, end_date: str, report_type: str, ann_date: str, comp_type: str,
basic_eps: float, diluted_eps: float, total_revenue: float, revenue: float,
total_cogs: float, oper_cost: float, sell_exp: float, admin_exp: float, fin_exp: float,
total_profit: float, income_tax: float, n_income: float, n_income_attr_p: float,
minority_gain: float, oth_compr_income: float, t_compr_income: float, compr_inc_attr_p: float,
ebit: float, ebitda: float, roe: float, roa: float, gross_margin: float,
net_profit_margin: float, net_profit_yoy: float, revenue_yoy: float,
equity_yoy: float, pcf: float, free_circ_mv: float,
):
self.ts_code = ts_code
self.end_date = end_date
self.report_type = report_type
self.ann_date = ann_date
self.comp_type = comp_type
self.basic_eps = basic_eps
self.diluted_eps = diluted_eps
self.total_revenue = total_revenue
self.revenue = revenue
self.total_cogs = total_cogs
self.oper_cost = oper_cost
self.sell_exp = sell_exp
self.admin_exp = admin_exp
self.fin_exp = fin_exp
self.total_profit = total_profit
self.income_tax = income_tax
self.n_income = n_income
self.n_income_attr_p = n_income_attr_p
self.minority_gain = minority_gain
self.oth_compr_income = oth_compr_income
self.t_compr_income = t_compr_income
self.compr_inc_attr_p = compr_inc_attr_p
self.ebit = ebit
self.ebitda = ebitda
self.roe = roe
self.roa = roa
self.gross_margin = gross_margin
self.net_profit_margin = net_profit_margin
self.net_profit_yoy = net_profit_yoy
self.revenue_yoy = revenue_yoy
self.equity_yoy = equity_yoy
self.pcf = pcf
self.free_circ_mv = free_circ_mv
@classmethod
def from_dict(cls, d: dict) -> "Income":
"""从字典(数据库行)构造 Income 对象。"""
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
ts_code=d.get("ts_code") or "",
end_date=d.get("end_date") or "",
report_type=d.get("report_type") or "",
ann_date=d.get("ann_date") or "",
comp_type=d.get("comp_type") or "",
basic_eps=_f(d.get("basic_eps")),
diluted_eps=_f(d.get("diluted_eps")),
total_revenue=_f(d.get("total_revenue")),
revenue=_f(d.get("revenue")),
total_cogs=_f(d.get("total_cogs")),
oper_cost=_f(d.get("oper_cost")),
sell_exp=_f(d.get("sell_exp")),
admin_exp=_f(d.get("admin_exp")),
fin_exp=_f(d.get("fin_exp")),
total_profit=_f(d.get("total_profit")),
income_tax=_f(d.get("income_tax")),
n_income=_f(d.get("n_income")),
n_income_attr_p=_f(d.get("n_income_attr_p")),
minority_gain=_f(d.get("minority_gain")),
oth_compr_income=_f(d.get("oth_compr_income")),
t_compr_income=_f(d.get("t_compr_income")),
compr_inc_attr_p=_f(d.get("compr_inc_attr_p")),
ebit=_f(d.get("ebit")),
ebitda=_f(d.get("ebitda")),
roe=_f(d.get("roe")),
roa=_f(d.get("roa")),
gross_margin=_f(d.get("gross_margin")),
net_profit_margin=_f(d.get("net_profit_margin")),
net_profit_yoy=_f(d.get("net_profit_yoy")),
revenue_yoy=_f(d.get("revenue_yoy")),
equity_yoy=_f(d.get("equity_yoy")),
pcf=_f(d.get("pcf")),
free_circ_mv=_f(d.get("free_circ_mv")),
)
def __repr__(self) -> str:
return (f"Income(ts_code={self.ts_code!r}, end_date={self.end_date!r}, "
f"report_type={self.report_type!r}, n_income={self.n_income})")
class StockLimit:
"""
每日涨跌停价格数据,对应本地 stock_limit 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
pre_close 昨日收盘价
up_limit 涨停价
down_limit 跌停价
"""
__slots__ = ("trade_date", "ts_code", "pre_close", "up_limit", "down_limit")
def __init__(self, trade_date: str, ts_code: str,
pre_close: float, up_limit: float, down_limit: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.pre_close = pre_close
self.up_limit = up_limit
self.down_limit = down_limit
@classmethod
def from_dict(cls, d: dict) -> "StockLimit":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
pre_close=_f(d.get("pre_close")),
up_limit=_f(d.get("up_limit")),
down_limit=_f(d.get("down_limit")),
)
def __repr__(self) -> str:
return (f"StockLimit(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"up_limit={self.up_limit}, down_limit={self.down_limit})")
class DailyLimitList:
"""
每日涨跌停榜单数据,对应本地 daily_limit_list 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
name 股票名称
limit_type 榜单类型(U=涨停, D=跌停)
limit_price 涨跌停价格
pct_chg 涨跌幅(%)
volume 成交量
amount 成交额
limit_streak 连板数(涨停板)
sector 所属板块
"""
__slots__ = ("trade_date", "ts_code", "name", "limit_type", "limit_price",
"pct_chg", "volume", "amount", "limit_streak", "sector")
def __init__(self, trade_date: str, ts_code: str, name: str, limit_type: str,
limit_price: float, pct_chg: float, volume: float, amount: float,
limit_streak: int, sector: str):
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.limit_type = limit_type
self.limit_price = limit_price
self.pct_chg = pct_chg
self.volume = volume
self.amount = amount
self.limit_streak = limit_streak
self.sector = sector
@classmethod
def from_dict(cls, d: dict) -> "DailyLimitList":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
limit_type=d.get("limit_type") or "",
limit_price=_f(d.get("limit_price")),
pct_chg=_f(d.get("pct_chg")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
limit_streak=_i(d.get("limit_streak")),
sector=d.get("sector") or "",
)
def __repr__(self) -> str:
return (f"DailyLimitList(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"name={self.name!r}, limit_type={self.limit_type!r}, limit_streak={self.limit_streak})")
class SectorStockMap:
"""
板块成分股映射表,对应本地 sector_stock_map 表。
字段说明:
sector_code 板块代码(PK)
stock_code 股票代码(PK)
sector_name 板块名称(冗余字段)
source 数据来源
"""
__slots__ = ("sector_code", "stock_code", "sector_name", "source")
def __init__(self, sector_code: str, stock_code: str, sector_name: str, source: str):
self.sector_code = sector_code
self.stock_code = stock_code
self.sector_name = sector_name
self.source = source
@classmethod
def from_dict(cls, d: dict) -> "SectorStockMap":
return cls(
sector_code=d.get("sector_code") or "",
stock_code=d.get("stock_code") or "",
sector_name=d.get("sector_name") or "",
source=d.get("source") or "",
)
def __repr__(self) -> str:
return (f"SectorStockMap(sector_code={self.sector_code!r}, "
f"stock_code={self.stock_code!r}, sector_name={self.sector_name!r})")
class TopList:
"""
龙虎榜每日明细数据,对应本地 top_list 表。
字段说明:
id 自增ID(PK)
trade_date 交易日期,格式 YYYY-MM-DD
ts_code 股票代码
name 股票名称
close 收盘价
pct_change 涨跌幅(%)
turnover_rate 换手率(%)
amount 总成交额
l_sell 龙虎榜卖出额
l_buy 龙虎榜买入额
l_amount 龙虎榜成交额
net_amount 龙虎榜净买入额
net_rate 龙虎榜净买额占比(%)
amount_rate 龙虎榜成交额占比(%)
float_values 当日流通市值
reason 上榜理由
"""
__slots__ = ("id", "trade_date", "ts_code", "name", "close", "pct_change",
"turnover_rate", "amount", "l_sell", "l_buy", "l_amount",
"net_amount", "net_rate", "amount_rate", "float_values", "reason")
def __init__(self, id: int, trade_date: str, ts_code: str, name: str,
close: float, pct_change: float, turnover_rate: float, amount: float,
l_sell: float, l_buy: float, l_amount: float, net_amount: float,
net_rate: float, amount_rate: float, float_values: float, reason: str):
self.id = id
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.close = close
self.pct_change = pct_change
self.turnover_rate = turnover_rate
self.amount = amount
self.l_sell = l_sell
self.l_buy = l_buy
self.l_amount = l_amount
self.net_amount = net_amount
self.net_rate = net_rate
self.amount_rate = amount_rate
self.float_values = float_values
self.reason = reason
@classmethod
def from_dict(cls, d: dict) -> "TopList":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
id=_i(d.get("id")),
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
close=_f(d.get("close")),
pct_change=_f(d.get("pct_change")),
turnover_rate=_f(d.get("turnover_rate")),
amount=_f(d.get("amount")),
l_sell=_f(d.get("l_sell")),
l_buy=_f(d.get("l_buy")),
l_amount=_f(d.get("l_amount")),
net_amount=_f(d.get("net_amount")),
net_rate=_f(d.get("net_rate")),
amount_rate=_f(d.get("amount_rate")),
float_values=_f(d.get("float_values")),
reason=d.get("reason") or "",
)
def __repr__(self) -> str:
return (f"TopList(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"name={self.name!r}, pct_change={self.pct_change})")
class TopInst:
"""
龙虎榜机构交易明细数据,对应本地 top_inst 表。
字段说明:
id 自增ID(PK)
trade_date 交易日期,格式 YYYY-MM-DD
ts_code 股票代码
exalter 营业部名称/机构名称
side 买卖类型(0:买入最大的前5名, 1:卖出最大的前5名)
buy 买入额(元)
buy_rate 买入占总成交比例(%)
sell 卖出额(元)
sell_rate 卖出占总成交比例(%)
net_buy 净成交额(元)
reason 上榜理由
"""
__slots__ = ("id", "trade_date", "ts_code", "exalter", "side",
"buy", "buy_rate", "sell", "sell_rate", "net_buy", "reason")
def __init__(self, id: int, trade_date: str, ts_code: str, exalter: str,
side: str, buy: float, buy_rate: float, sell: float,
sell_rate: float, net_buy: float, reason: str):
self.id = id
self.trade_date = trade_date
self.ts_code = ts_code
self.exalter = exalter
self.side = side
self.buy = buy
self.buy_rate = buy_rate
self.sell = sell
self.sell_rate = sell_rate
self.net_buy = net_buy
self.reason = reason
@classmethod
def from_dict(cls, d: dict) -> "TopInst":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
id=_i(d.get("id")),
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
exalter=d.get("exalter") or "",
side=d.get("side") or "",
buy=_f(d.get("buy")),
buy_rate=_f(d.get("buy_rate")),
sell=_f(d.get("sell")),
sell_rate=_f(d.get("sell_rate")),
net_buy=_f(d.get("net_buy")),
reason=d.get("reason") or "",
)
def __repr__(self) -> str:
return (f"TopInst(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"exalter={self.exalter!r}, side={self.side!r}, net_buy={self.net_buy})")
class SectorFlowDaily:
"""
板块资金流向数据,对应本地 sector_flow_daily 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
content_type 板块类型(行业/概念/地域)
ts_code 板块代码(PK)
name 板块名称
pct_change 涨跌幅(%)
close 收盘价
net_amount 净流入金额(元)
net_amount_rate 净流入占比(%)
buy_elg_amount 超大单净流入(元)
buy_elg_amount_rate 超大单净流入占比(%)
buy_lg_amount 大单净流入(元)
buy_lg_amount_rate 大单净流入占比(%)
buy_md_amount 中单净流入(元)
buy_md_amount_rate 中单净流入占比(%)
buy_sm_amount 小单净流入(元)
buy_sm_amount_rate 小单净流入占比(%)
buy_sm_amount_stock 小单净流入最大股
rank 排名
"""
__slots__ = ("trade_date", "ts_code", "name", "content_type",
"pct_change", "close", "net_amount", "net_amount_rate",
"buy_elg_amount", "buy_elg_amount_rate",
"buy_lg_amount", "buy_lg_amount_rate",
"buy_md_amount", "buy_md_amount_rate",
"buy_sm_amount", "buy_sm_amount_rate",
"buy_sm_amount_stock", "rank")
def __init__(self, trade_date: str, ts_code: str, name: str, content_type: str,
pct_change: float, close: float,
net_amount: float, net_amount_rate: float,
buy_elg_amount: float, buy_elg_amount_rate: float,
buy_lg_amount: float, buy_lg_amount_rate: float,
buy_md_amount: float, buy_md_amount_rate: float,
buy_sm_amount: float, buy_sm_amount_rate: float,
buy_sm_amount_stock: str, rank: int):
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.content_type = content_type
self.pct_change = pct_change
self.close = close
self.net_amount = net_amount
self.net_amount_rate = net_amount_rate
self.buy_elg_amount = buy_elg_amount
self.buy_elg_amount_rate = buy_elg_amount_rate
self.buy_lg_amount = buy_lg_amount
self.buy_lg_amount_rate = buy_lg_amount_rate
self.buy_md_amount = buy_md_amount
self.buy_md_amount_rate = buy_md_amount_rate
self.buy_sm_amount = buy_sm_amount
self.buy_sm_amount_rate = buy_sm_amount_rate
self.buy_sm_amount_stock = buy_sm_amount_stock
self.rank = rank
@classmethod
def from_dict(cls, d: dict) -> "SectorFlowDaily":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
content_type=d.get("content_type") or "",
pct_change=_f(d.get("pct_change")),
close=_f(d.get("close")),
net_amount=_f(d.get("net_amount")),
net_amount_rate=_f(d.get("net_amount_rate")),
buy_elg_amount=_f(d.get("buy_elg_amount")),
buy_elg_amount_rate=_f(d.get("buy_elg_amount_rate")),
buy_lg_amount=_f(d.get("buy_lg_amount")),
buy_lg_amount_rate=_f(d.get("buy_lg_amount_rate")),
buy_md_amount=_f(d.get("buy_md_amount")),
buy_md_amount_rate=_f(d.get("buy_md_amount_rate")),
buy_sm_amount=_f(d.get("buy_sm_amount")),
buy_sm_amount_rate=_f(d.get("buy_sm_amount_rate")),
buy_sm_amount_stock=d.get("buy_sm_amount_stock") or "",
rank=_i(d.get("rank")),
)
def __repr__(self) -> str:
return (f"SectorFlowDaily(trade_date={self.trade_date!r}, "
f"ts_code={self.ts_code!r}, name={self.name!r}, "
f"net_amount={self.net_amount})")
class IndexBasic:
"""
指数基础信息,对应本地 index_basic 表。
字段说明:
ts_code 指数代码(PK)
name 指数名称
fullname 指数全称
market 市场
publisher 发布方
index_type 指数类型
category 分类
base_date 基准日期,格式 YYYY-MM-DD
base_point 基点
list_date 发布日期,格式 YYYY-MM-DD
weight_rule 加权方式
desc 描述
exp_date 终止日期,格式 YYYY-MM-DD
"""
__slots__ = ("ts_code", "name", "fullname", "market", "publisher",
"index_type", "category", "base_date", "base_point",
"list_date", "weight_rule", "desc", "exp_date")
def __init__(self, ts_code: str, name: str, fullname: str, market: str,
publisher: str, index_type: str, category: str, base_date: str,
base_point: float, list_date: str, weight_rule: str,
desc: str, exp_date: str):
self.ts_code = ts_code
self.name = name
self.fullname = fullname
self.market = market
self.publisher = publisher
self.index_type = index_type
self.category = category
self.base_date = base_date
self.base_point = base_point
self.list_date = list_date
self.weight_rule = weight_rule
self.desc = desc
self.exp_date = exp_date
@classmethod
def from_dict(cls, d: dict) -> "IndexBasic":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
fullname=d.get("fullname") or "",
market=d.get("market") or "",
publisher=d.get("publisher") or "",
index_type=d.get("index_type") or "",
category=d.get("category") or "",
base_date=d.get("base_date") or "",
base_point=_f(d.get("base_point")),
list_date=d.get("list_date") or "",
weight_rule=d.get("weight_rule") or "",
desc=d.get("desc") or "",
exp_date=d.get("exp_date") or "",
)
def __repr__(self) -> str:
return (f"IndexBasic(ts_code={self.ts_code!r}, name={self.name!r}, "
f"market={self.market!r})")
class IndexDaily:
"""
指数日线行情数据,对应本地 index_daily 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 指数代码(PK)
close 收盘指数
open 开盘指数
high 最高指数
low 最低指数
pre_close 前收盘指数
change 涨跌点数
pct_chg 涨跌幅(%)
vol 成交量
amount 成交额
"""
__slots__ = ("trade_date", "ts_code", "open", "high", "low", "close",
"pre_close", "change", "pct_chg", "vol", "amount")
def __init__(self, trade_date: str, ts_code: str, open: float, high: float,
low: float, close: float, pre_close: float, change: float,
pct_chg: float, vol: float, amount: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.open = open
self.high = high
self.low = low
self.close = close
self.pre_close = pre_close
self.change = change
self.pct_chg = pct_chg
self.vol = vol
self.amount = amount
@classmethod
def from_dict(cls, d: dict) -> "IndexDaily":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
pct_chg=_f(d.get("pct_chg")),
vol=_f(d.get("vol")),
amount=_f(d.get("amount")),
)
def __repr__(self) -> str:
return (f"IndexDaily(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pct_chg={self.pct_chg})")
class IndexWeekly:
"""
指数周线行情数据,对应本地 index_weekly 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 指数代码(PK)
close 收盘指数
open 开盘指数
high 最高指数
low 最低指数
pre_close 前收盘指数
change 涨跌点数
pct_chg 涨跌幅(%)
vol 成交量
amount 成交额
"""
__slots__ = ("trade_date", "ts_code", "open", "high", "low", "close",
"pre_close", "change", "pct_chg", "vol", "amount")
def __init__(self, trade_date: str, ts_code: str, open: float, high: float,
low: float, close: float, pre_close: float, change: float,
pct_chg: float, vol: float, amount: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.open = open
self.high = high
self.low = low
self.close = close
self.pre_close = pre_close
self.change = change
self.pct_chg = pct_chg
self.vol = vol
self.amount = amount
@classmethod
def from_dict(cls, d: dict) -> "IndexWeekly":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
pct_chg=_f(d.get("pct_chg")),
vol=_f(d.get("vol")),
amount=_f(d.get("amount")),
)
def __repr__(self) -> str:
return (f"IndexWeekly(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pct_chg={self.pct_chg})")
class IndexMonthly:
"""
指数月线行情数据,对应本地 index_monthly 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 指数代码(PK)
close 收盘指数
open 开盘指数
high 最高指数
low 最低指数
pre_close 前收盘指数
change 涨跌点数
pct_chg 涨跌幅(%)
vol 成交量
amount 成交额
"""
__slots__ = ("trade_date", "ts_code", "open", "high", "low", "close",
"pre_close", "change", "pct_chg", "vol", "amount")
def __init__(self, trade_date: str, ts_code: str, open: float, high: float,
low: float, close: float, pre_close: float, change: float,
pct_chg: float, vol: float, amount: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.open = open
self.high = high
self.low = low
self.close = close
self.pre_close = pre_close
self.change = change
self.pct_chg = pct_chg
self.vol = vol
self.amount = amount
@classmethod
def from_dict(cls, d: dict) -> "IndexMonthly":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
pct_chg=_f(d.get("pct_chg")),
vol=_f(d.get("vol")),
amount=_f(d.get("amount")),
)
def __repr__(self) -> str:
return (f"IndexMonthly(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pct_chg={self.pct_chg})")
class DailyBombList:
"""
每日炸板榜单数据,对应本地 daily_bomb_list 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
name 股票名称
bomb_type 炸板类型(U=曾涨停, D=曾跌停/撬板)
limit_price 触及的涨跌停价格
pct_chg 涨跌幅(%)
volume 成交量
amount 成交额
sector 所属板块
"""
__slots__ = ("trade_date", "ts_code", "name", "bomb_type", "limit_price",
"pct_chg", "volume", "amount", "sector")
def __init__(self, trade_date: str, ts_code: str, name: str, bomb_type: str,
limit_price: float, pct_chg: float, volume: float, amount: float,
sector: str):
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.bomb_type = bomb_type
self.limit_price = limit_price
self.pct_chg = pct_chg
self.volume = volume
self.amount = amount
self.sector = sector
@classmethod
def from_dict(cls, d: dict) -> "DailyBombList":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
bomb_type=d.get("bomb_type") or "",
limit_price=_f(d.get("limit_price")),
pct_chg=_f(d.get("pct_chg")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
sector=d.get("sector") or "",
)
def __repr__(self) -> str:
return (f"DailyBombList(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"name={self.name!r}, bomb_type={self.bomb_type!r})")
class AppVersion:
"""
应用版本信息。
字段说明:
version 版本号(如 1.0、1.1)
release_date 发布日期,格式 YYYY-MM-DD
file_name 安装包文件名
download_url 安装包下载地址
"""
__slots__ = ("version", "release_date", "file_name", "download_url")
def __init__(self, version: str, release_date: str, file_name: str, download_url: str):
self.version = version
self.release_date = release_date
self.file_name = file_name
self.download_url = download_url
@classmethod
def from_dict(cls, d: dict) -> "AppVersion":
return cls(
version=d["version"],
release_date=d["release_date"],
file_name=d["file_name"],
download_url=d["download_url"],
)
def __repr__(self) -> str:
return (f"AppVersion(version={self.version!r}, release_date={self.release_date!r}, "
f"file_name={self.file_name!r}, download_url={self.download_url!r})")
class TokenCheckResult:
"""
Token 校验结果。
字段说明:
status 校验状态(success=通过, failure=失败)
message 状态描述信息
"""
__slots__ = ("status", "message")
def __init__(self, status: str, message: str):
self.status = status
self.message = message
@classmethod
def from_dict(cls, d: dict) -> "TokenCheckResult":
return cls(
status=d.get("status") or "",
message=d.get("message") or "",
)
def is_success(self) -> bool:
return self.status == "success"
def __repr__(self) -> str:
return f"TokenCheckResult(status={self.status!r}, message={self.message!r})"
FILE:scripts/factor_mining.py
"""
factor_mining.py — 因子挖矿实现模块
包含 random_alpha_backtest 的完整实现逻辑。
stock_api.StockApi.random_alpha_backtest() 是对外接口,内部委托此模块执行。
外部勿直接调用本模块,请统一通过 StockApi.random_alpha_backtest() 调用。
"""
import random
import statistics as _stat
from datetime import datetime, timedelta
from typing import Dict, List, Optional, TYPE_CHECKING
import numpy as np
import pandas as pd
from formulaicAlphas import AlphaDataLoader, Alpha101, ALPHA_DESCRIPTIONS
if TYPE_CHECKING:
from stock_api import StockApi
def run_random_alpha_backtest(
api: "StockApi",
codes: Optional[List[str]] = None,
max_screen_factors: int = 5,
max_signal_factors: int = 7,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
initial_cash: float = 1_000_000.0,
warmup_days: int = 90,
random_seed: Optional[int] = None,
top_n_stocks: int = 5,
max_pool_size: int = 30,
max_holdings: int = 5,
) -> Dict:
"""
因子挖矿核心逻辑(两阶段:选股 + 交易信号)。
流程:
1. 随机抽取 k_screen 个选股因子 + k_signal 个信号因子
2. 选股阶段:以 start_date 为截面日,每个选股因子随机保留 5%~20% 的股票
3. 若过滤后股票数仍超过 max_pool_size,按各选股因子综合得分再截取前 max_pool_size 只
4. 信号阶段:逐日计算信号因子横截面分位排名,综合排名 >= buy_thresh 时买入,
<= sell_thresh 时卖出(阈值在合理范围内随机生成)
5. 每日最多同时持仓 max_holdings 只,优先买入综合排名最高的股票
6. 输出 Top N 个股的每笔交易时的具体因子值与排名
Args:
api: StockApi 实例(用于调用 get_all_symbols、load_alpha_data 等方法)
codes: 股票池;None 时取全市场
max_screen_factors: 选股因子最大数量(默认 5)
max_signal_factors: 信号因子最大数量(默认 7)
start_date: 回测起始日,None 取 end_date 前 90 天
end_date: 回测截止日,None 取今日
initial_cash: 初始资金(默认 100 万)
warmup_days: 因子预热天数(默认 90)
random_seed: 随机种子,None 不固定
top_n_stocks: 输出详细交易记录的个股数量(默认 5)
max_pool_size: 最终候选池上限,超过时按综合得分截取(默认 30)
max_holdings: 最大同时持仓数,优先持有综合排名最高的股票(默认 5)
"""
# ── 1. 日期默认值 ──────────────────────────────────────────────────────
if end_date is None:
end_date = datetime.today().strftime('%Y-%m-%d')
if start_date is None:
start_date = (
datetime.strptime(end_date, '%Y-%m-%d') - timedelta(days=90)
).strftime('%Y-%m-%d')
# ── 2. 股票池 ──────────────────────────────────────────────────────────
if codes is None:
codes = api.get_all_symbols()
if not codes:
return {'error': '股票池为空'}
# ── 3. 随机抽取因子(选股 / 信号各自独立不重复,两组间允许重叠)────────
rng = random.Random(random_seed)
k_screen = rng.randint(1, max(1, max_screen_factors))
k_signal = rng.randint(1, max(1, max_signal_factors))
screen_nums = rng.sample(range(1, 102), k_screen)
signal_nums = rng.sample(range(1, 102), k_signal)
screen_names = [f'alpha{n:03d}' for n in screen_nums]
signal_names = [f'alpha{n:03d}' for n in signal_nums]
all_nums = list(dict.fromkeys(screen_nums + signal_nums))
all_names = list(dict.fromkeys(screen_names + signal_names))
all_descs = {name: ALPHA_DESCRIPTIONS.get(name, '') for name in all_names}
# 选股因子:每个因子随机保留比例 [0.05, 0.20]
screen_top_pcts = {name: round(rng.uniform(0.05, 0.20), 2) for name in screen_names}
# 信号因子阈值
signal_buy_thresh = round(rng.uniform(0.55, 0.82), 2)
signal_sell_thresh = round(rng.uniform(0.30, 0.55), 2)
# ── 4. 加载面板数据(含预热段)────────────────────────────────────────
warmup_start = (
datetime.strptime(start_date, '%Y-%m-%d') - timedelta(days=warmup_days)
).strftime('%Y-%m-%d')
panel = api.load_alpha_data(codes, warmup_start, end_date)
if not panel:
return {'error': '无法加载面板数据',
'screen_factors': screen_names, 'signal_factors': signal_names}
close_panel = panel['close']
start_ts = pd.Timestamp(start_date)
end_ts = pd.Timestamp(end_date)
valid_idx = close_panel.index[close_panel.index <= start_ts]
ref_date = valid_idx[-1] if len(valid_idx) > 0 else close_panel.index[0]
# ── 5. 计算所有因子 ────────────────────────────────────────────────────
alpha_obj = Alpha101(panel)
factor_panels: Dict[str, object] = {}
for num in all_nums:
name = f'alpha{num:03d}'
method = getattr(alpha_obj, name, None)
if method is None:
continue
try:
factor_panels[name] = method()
except Exception:
pass
# ── 6. 选股阶段:逐因子顺序过滤 ───────────────────────────────────────
current_pool = set(close_panel.columns.tolist())
filter_log: List[Dict] = []
for name in screen_names:
before = len(current_pool)
top_pct = screen_top_pcts[name]
if not current_pool or name not in factor_panels:
filter_log.append({'factor': name, 'status': 'skipped',
'before': before, 'after': before, 'top_pct': top_pct})
continue
fp = factor_panels[name]
if ref_date not in fp.index:
filter_log.append({'factor': name, 'status': 'no_ref_date',
'before': before, 'after': before, 'top_pct': top_pct})
continue
pool_cols = [c for c in current_pool if c in fp.columns]
snapshot = fp.loc[ref_date, pool_cols].dropna()
if snapshot.empty:
filter_log.append({'factor': name, 'status': 'all_nan',
'before': before, 'after': before, 'top_pct': top_pct})
continue
n_keep = max(1, int(len(snapshot) * top_pct))
top_codes = set(snapshot.nlargest(n_keep).index)
current_pool &= top_codes
filter_log.append({'factor': name, 'status': 'ok',
'before': before, 'after': len(current_pool),
'ref_date': str(ref_date.date()),
'snapshot_size': len(snapshot),
'kept': n_keep, 'top_pct': top_pct})
final_codes = sorted(current_pool)
if not final_codes:
return {'screen_k': k_screen, 'signal_k': k_signal,
'screen_factors': screen_names, 'signal_factors': signal_names,
'factor_descriptions': all_descs,
'initial_pool': len(codes), 'filter_log': filter_log,
'final_pool': [], 'final_pool_count': 0,
'error': '过滤后股票池为空,无法回测'}
# ── 二次裁剪:候选池超过 max_pool_size 时按综合得分取 Top N ──────────
if max_pool_size > 0 and len(final_codes) > max_pool_size:
score_map: Dict[str, float] = {c: 0.0 for c in final_codes}
for name in screen_names:
if name not in factor_panels:
continue
fp = factor_panels[name]
if ref_date not in fp.index:
continue
pool_cols = [c for c in final_codes if c in fp.columns]
snap = fp.loc[ref_date, pool_cols].dropna()
if snap.empty:
continue
ranked = snap.rank(pct=True)
for c, v in ranked.items():
score_map[c] = score_map.get(c, 0.0) + float(v)
sorted_by_score = sorted(final_codes, key=lambda c: score_map.get(c, 0.0), reverse=True)
final_codes = sorted(sorted_by_score[:max_pool_size])
# ── 7. 信号阶段:逐日模拟买卖 ─────────────────────────────────────────
bt_mask = (close_panel.index >= start_ts) & (close_panel.index <= end_ts)
avail_codes = [c for c in final_codes if c in close_panel.columns]
bt_close = close_panel.loc[bt_mask, avail_codes]
if bt_close.empty or len(bt_close) < 2:
return {'screen_k': k_screen, 'signal_k': k_signal,
'screen_factors': screen_names, 'signal_factors': signal_names,
'factor_descriptions': all_descs,
'initial_pool': len(codes), 'filter_log': filter_log,
'final_pool': final_codes, 'final_pool_count': len(final_codes),
'error': '回测期内收盘数据不足(< 2 个交易日)'}
trading_dates = bt_close.index.tolist()
# 预提取信号因子回测期面板(在筛选后的股票池内计算横截面排名)
sig_panels: Dict[str, object] = {}
for name in signal_names:
if name in factor_panels:
fp = factor_panels[name]
sig_cols = [c for c in avail_codes if c in fp.columns]
if sig_cols:
sig_panels[name] = fp.loc[bt_mask, sig_cols]
fee_rate = 0.001
n_slots = max(1, min(max_holdings, len(avail_codes)))
per_stock_budget = initial_cash / n_slots
cash = initial_cash
positions: Dict[str, Dict] = {}
equity_curve: List[float] = [initial_cash]
trade_log: List[Dict] = []
for date in trading_dates:
date_str = str(date.date())
# 计算各信号因子当日横截面分位排名
factor_day_ranks: Dict[str, Dict] = {}
composite_ranks: Dict[str, float] = {}
valid_sig = [n for n in signal_names if n in sig_panels]
for code in avail_codes:
per_factor = {}
rank_vals = []
for sname in valid_sig:
sp = sig_panels[sname]
if code not in sp.columns or date not in sp.index:
continue
day_series = sp.loc[date].dropna()
if day_series.empty or code not in day_series.index:
continue
raw = float(day_series[code])
rank = float(day_series.rank(pct=True)[code])
per_factor[sname] = {'value': round(raw, 6), 'rank': round(rank, 4)}
rank_vals.append(rank)
factor_day_ranks[code] = per_factor
composite_ranks[code] = round(float(np.mean(rank_vals)), 4) if rank_vals else 0.5
# ── 先执行卖出信号 ──────────────────────────────────────────────────
for code in list(positions.keys()):
comp = composite_ranks.get(code, 0.5)
if code not in bt_close.columns:
continue
price_raw = bt_close.loc[date, code]
if pd.isna(price_raw) or float(price_raw) <= 0:
continue
price = float(price_raw)
if comp <= signal_sell_thresh:
pos = positions[code]
proceeds = pos['shares'] * price * (1 - fee_rate)
pnl = proceeds - pos['entry_value']
pnl_pct = (proceeds / pos['entry_value'] - 1) * 100
hold_days = (date - pd.Timestamp(pos['entry_date'])).days
cash += proceeds
trade_log.append({
'date': date_str, 'code': code, 'action': 'SELL',
'price': round(price, 3), 'shares': pos['shares'],
'amount': round(proceeds, 2),
'composite_rank': comp,
'factor_values': factor_day_ranks.get(code, {}),
'signal_buy_thresh': signal_buy_thresh,
'signal_sell_thresh': signal_sell_thresh,
'hold_days': hold_days,
'pnl': round(pnl, 2),
'pnl_pct': round(pnl_pct, 4),
})
del positions[code]
# ── 再执行买入信号:候选排名最高、持仓数未满 max_holdings ────────────
slots_free = max_holdings - len(positions)
if slots_free > 0:
buy_candidates = []
for code in avail_codes:
if code in positions:
continue
comp = composite_ranks.get(code, 0.5)
if comp < signal_buy_thresh:
continue
if code not in bt_close.columns:
continue
price_raw = bt_close.loc[date, code]
if pd.isna(price_raw) or float(price_raw) <= 0:
continue
buy_candidates.append((comp, code, float(price_raw)))
buy_candidates.sort(key=lambda x: x[0], reverse=True)
for comp, code, price in buy_candidates[:slots_free]:
budget = min(per_stock_budget, cash * 0.99)
if budget < price * 100:
continue
shares = int(budget / price / 100) * 100
if shares <= 0:
continue
cost = shares * price * (1 + fee_rate)
if cost > cash:
continue
cash -= cost
positions[code] = {'shares': shares, 'entry_price': price,
'entry_date': date_str, 'entry_value': cost}
trade_log.append({
'date': date_str, 'code': code, 'action': 'BUY',
'price': round(price, 3), 'shares': shares, 'amount': round(cost, 2),
'composite_rank': comp,
'factor_values': factor_day_ranks.get(code, {}),
'signal_buy_thresh': signal_buy_thresh,
'signal_sell_thresh': signal_sell_thresh,
})
# 当日组合净值
pos_val = sum(
positions[c]['shares'] * float(bt_close.loc[date, c])
for c in positions
if c in bt_close.columns and not pd.isna(bt_close.loc[date, c])
)
equity_curve.append(cash + pos_val)
# 末日强制清仓
last_date = trading_dates[-1]
last_date_str = str(last_date.date())
for code in list(positions.keys()):
pos = positions[code]
if code in bt_close.columns and not pd.isna(bt_close.loc[last_date, code]):
price = float(bt_close.loc[last_date, code])
proceeds = pos['shares'] * price * (1 - fee_rate)
pnl = proceeds - pos['entry_value']
pnl_pct = (proceeds / pos['entry_value'] - 1) * 100
hold_days = (last_date - pd.Timestamp(pos['entry_date'])).days
cash += proceeds
trade_log.append({
'date': last_date_str, 'code': code, 'action': 'SELL(强平)',
'price': round(price, 3), 'shares': pos['shares'],
'amount': round(proceeds, 2),
'composite_rank': None, 'factor_values': {},
'signal_buy_thresh': signal_buy_thresh,
'signal_sell_thresh': signal_sell_thresh,
'hold_days': hold_days,
'pnl': round(pnl, 2),
'pnl_pct': round(pnl_pct, 4),
})
equity_curve[-1] = cash
# ── 8. 计算业绩指标 ────────────────────────────────────────────────────
total_ret_dec = (equity_curve[-1] / initial_cash) - 1.0
trading_days = len(trading_dates)
bt_result = {
'start_date': start_date,
'end_date': end_date,
'trading_days': trading_days,
'initial_cash': initial_cash,
'final_value': round(equity_curve[-1], 2),
'total_return_pct': round(total_ret_dec * 100, 4),
'annualized_return_pct': round(
api.get_annualized_return(total_ret_dec, trading_days) * 100, 4),
'max_drawdown_pct': round(
api.get_max_drawdown_pct(equity_curve) * 100, 4),
'sharpe_ratio': round(api.get_sharpe_ratio(equity_curve), 4),
'equity_curve': [round(v, 2) for v in equity_curve],
}
# ── 8.5. 基准线对比 ────────────────────────────────────────────────────
_BENCHMARKS = [
('000001.SH', '上证指数'),
('000300.SH', '沪深300'),
('000905.SH', '中证500'),
('399006.SZ', '创业板指'),
]
benchmarks_result: List[Dict] = []
def _fetch_bench_close(ts_code: str, s_date: str, e_date: str) -> Dict[str, float]:
"""优先查本地 index_daily 表;若表不存在则 fallback 到东方财富爬虫。"""
try:
rows = api.get_index_daily(ts_codes=[ts_code], start_date=s_date, end_date=e_date)
if rows:
return {r.trade_date: r.close for r in rows}
except Exception:
pass
import requests as _req
code_part, mkt = ts_code.split('.')
secid = f"1.{code_part}" if mkt == 'SH' else f"0.{code_part}"
url = 'https://push2his.eastmoney.com/api/qt/stock/kline/get'
params = {
'secid': secid,
'fields1': 'f1,f2,f3,f4,f5,f6',
'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
'klt': '101', 'fqt': '0',
'beg': s_date.replace('-', ''), 'end': e_date.replace('-', ''),
'lmt': '2000',
}
headers = {'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.eastmoney.com/'}
resp = _req.get(url, params=params, headers=headers, timeout=10)
klines = resp.json().get('data', {}).get('klines', [])
result: Dict[str, float] = {}
for kl in klines:
parts = kl.split(',')
if len(parts) >= 3:
result[parts[0]] = float(parts[2])
return result
try:
bench_map: Dict[str, Dict[str, float]] = {}
for bcode, _ in _BENCHMARKS:
try:
bench_map[bcode] = _fetch_bench_close(bcode, start_date, end_date)
except Exception:
bench_map[bcode] = {}
strat_daily_rets = []
for i in range(1, len(equity_curve)):
prev = equity_curve[i - 1]
strat_daily_rets.append((equity_curve[i] / prev - 1.0) if prev else 0.0)
for bcode, bname in _BENCHMARKS:
date_close = bench_map.get(bcode, {})
aligned: List[float] = []
last_val: Optional[float] = None
for d in trading_dates:
v = date_close.get(str(d.date()))
if v is not None:
last_val = v
if last_val is not None:
aligned.append(last_val)
elif aligned:
aligned.append(aligned[-1])
if len(aligned) < 2:
benchmarks_result.append({'code': bcode, 'name': bname, 'error': '数据不足'})
continue
base0 = aligned[0]
bench_curve = [initial_cash] + [
round(initial_cash * v / base0, 2) for v in aligned]
b_total_ret_dec = (bench_curve[-1] / initial_cash) - 1.0
b_td = len(bench_curve) - 1
b_ann = api.get_annualized_return(b_total_ret_dec, b_td) if b_td > 0 else 0.0
b_dd = api.get_max_drawdown_pct(bench_curve)
excess_ret_pct = round(bt_result['total_return_pct'] - b_total_ret_dec * 100, 4)
bench_daily_rets = []
for i in range(1, len(bench_curve)):
prev = bench_curve[i - 1]
bench_daily_rets.append((bench_curve[i] / prev - 1.0) if prev else 0.0)
n_common = min(len(strat_daily_rets), len(bench_daily_rets))
if n_common > 1:
excess_daily = [strat_daily_rets[i] - bench_daily_rets[i] for i in range(n_common)]
mu = sum(excess_daily) / n_common
std = _stat.stdev(excess_daily) if n_common > 1 else 0.0
ir = round((mu / std) * (252 ** 0.5), 4) if std > 1e-10 else 0.0
else:
ir = 0.0
benchmarks_result.append({
'code': bcode,
'name': bname,
'total_return_pct': round(b_total_ret_dec * 100, 4),
'annualized_return_pct': round(b_ann * 100, 4),
'max_drawdown_pct': round(b_dd * 100, 4),
'excess_return_pct': excess_ret_pct,
'information_ratio': ir,
'equity_curve': bench_curve,
})
except Exception as _e:
benchmarks_result = [{'error': str(_e)}]
# ── 8.6. 因子 IC 计算(Rank IC,斯皮尔曼相关系数,纯 numpy 实现)──────
def _spearman_corr(x: np.ndarray, y: np.ndarray) -> float:
rx = np.argsort(np.argsort(x)).astype(float)
ry = np.argsort(np.argsort(y)).astype(float)
mx, my = rx.mean(), ry.mean()
num = ((rx - mx) * (ry - my)).sum()
den = np.sqrt(((rx - mx) ** 2).sum() * ((ry - my) ** 2).sum())
return float(num / den) if den > 1e-10 else 0.0
ic_stats: Dict[str, Dict] = {}
for _fname in list(dict.fromkeys(screen_names + signal_names)):
if _fname not in factor_panels:
continue
_fp = factor_panels[_fname]
_daily_ic: List[float] = []
for _i in range(len(trading_dates) - 1):
_d_cur = trading_dates[_i]
_d_next = trading_dates[_i + 1]
if _d_cur not in _fp.index or _d_next not in bt_close.index:
continue
_factor_day = _fp.loc[_d_cur, avail_codes].dropna()
_codes_c = [c for c in _factor_day.index if c in bt_close.columns]
if len(_codes_c) < 5:
continue
_ret_next = (bt_close.loc[_d_next, _codes_c] /
bt_close.loc[_d_cur, _codes_c] - 1).dropna()
_codes_v = [c for c in _codes_c if c in _ret_next.index]
if len(_codes_v) < 5:
continue
_rho = _spearman_corr(
_factor_day[_codes_v].values,
_ret_next[_codes_v].values,
)
if not np.isnan(_rho):
_daily_ic.append(_rho)
if len(_daily_ic) < 2:
ic_stats[_fname] = {'error': '数据不足'}
continue
_ic_arr = np.array(_daily_ic)
_ic_mean = float(np.mean(_ic_arr))
_ic_std = float(np.std(_ic_arr, ddof=1))
_ic_ir = round(_ic_mean / _ic_std * (252 ** 0.5), 4) if _ic_std > 1e-10 else 0.0
ic_stats[_fname] = {
'ic_mean': round(_ic_mean, 4),
'ic_std': round(_ic_std, 4),
'ic_ir': round(_ic_ir, 4),
'ic_win_rate': round(float(np.mean(_ic_arr > 0)), 4),
'ic_abs_mean': round(float(np.mean(np.abs(_ic_arr))), 4),
'ic_series': [round(v, 4) for v in _daily_ic],
'n_days': len(_daily_ic),
}
# ── 9. 统计各股表现,取 Top N ──────────────────────────────────────────
stock_pnl: Dict[str, float] = {}
for tr in trade_log:
if tr['action'] in ('SELL', 'SELL(强平)'):
code = tr['code']
stock_pnl[code] = stock_pnl.get(code, 0.0) + tr.get('pnl', 0.0)
top_codes = sorted(stock_pnl, key=lambda c: stock_pnl[c], reverse=True)[:top_n_stocks]
top_stocks_detail = []
for code in top_codes:
code_trades = [tr for tr in trade_log if tr['code'] == code]
top_stocks_detail.append({
'code': code,
'total_pnl': round(stock_pnl[code], 2),
'trades': code_trades,
})
result = {
'screen_k': k_screen,
'signal_k': k_signal,
'screen_factors': screen_names,
'signal_factors': signal_names,
'factor_descriptions': all_descs,
'signal_config': {
'buy_thresh': signal_buy_thresh,
'sell_thresh': signal_sell_thresh,
},
'screen_top_pcts': screen_top_pcts,
'initial_pool': len(codes),
'filter_log': filter_log,
'final_pool': final_codes,
'final_pool_count': len(final_codes),
'trade_log': trade_log,
'backtest': bt_result,
'benchmarks': benchmarks_result,
'ic_stats': ic_stats,
'top_stocks': top_stocks_detail,
}
import io
_buf = io.StringIO()
import sys as _sys
_old_stdout = _sys.stdout
_sys.stdout = _buf
try:
_print_mining_result(result)
finally:
_sys.stdout = _old_stdout
result['summary_text'] = _buf.getvalue()
return result
def _split_desc(desc: str):
"""从描述字符串提取 (定义, 方向标签, 高值解读)"""
import re
parts = desc.split('。', 1)
definition = parts[0] if parts else desc
rest = parts[1] if len(parts) > 1 else ''
if '正向因子' in rest:
dir_tag = '↑正向'
elif '反向因子' in rest:
dir_tag = '↓反向'
elif '反转因子' in rest:
dir_tag = '↺反转'
elif '条件正向' in rest:
dir_tag = '◈条件'
else:
dir_tag = ' ─ '
m = re.search(r'高值(?:\(\+1\))?表示(.+?)(?:,|$)', rest)
high_interp = m.group(1).strip() if m else ''
return definition, dir_tag, high_interp
def _ic_line(ic: dict) -> str:
if not ic or 'error' in ic:
return '(数据不足)'
icir = ic['ic_ir']
grade = '★★★优秀' if icir > 1 else ('★★良好' if icir > 0.5 else ('★一般' if icir > 0 else '✗反向'))
return (f'ICIR={icir:.2f} {grade} '
f'IC均值={ic["ic_mean"]:+.4f} '
f'胜率={ic["ic_win_rate"]*100:.1f}% '
f'|IC|均值={ic["ic_abs_mean"]:.4f}')
def _print_mining_result(result: dict) -> None:
"""格式化打印因子挖矿结果(从 run_factor_mining.py 迁移)。"""
import sys
# 确保中文正常输出
if hasattr(sys.stdout, 'reconfigure'):
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
sc = result['signal_config']
_scr_names = result['screen_factors']
_sig_names = result['signal_factors']
_descs = result['factor_descriptions']
_top_pcts = result['screen_top_pcts']
_ic_stats = result.get('ic_stats', {})
_flog = result.get('filter_log', [])
_buy_thr = sc['buy_thresh']
_sell_thr = sc['sell_thresh']
sep = '='*60
# ── 本次挖矿战绩 ───────────────────────────────────────────────────────
print(f'\n{sep}')
print('【本次挖矿战绩】')
print(sep)
if result.get('backtest'):
bt = result['backtest']
tl = result.get('trade_log', [])
n_buy = sum(1 for t in tl if t['action'] == 'BUY')
n_sell = sum(1 for t in tl if 'SELL' in t['action'])
ret = bt['total_return_pct']
ann = bt['annualized_return_pct']
dd = bt['max_drawdown_pct']
ret_flag = '▲' if ret >= 0 else '▼'
ann_flag = '▲' if ann >= 0 else '▼'
print(f' 挖矿日期: {datetime.today().strftime("%Y-%m-%d")}')
print(f' 回测区间: {bt["start_date"]} → {bt["end_date"]}({bt["trading_days"]} 交易日)')
print(f' 初始资金: {bt["initial_cash"]:>14,.0f} 元')
print(f' 期末资金: {bt["final_value"]:>14,.2f} 元')
print(f' 总收益率: {ret_flag} {abs(ret):>10.4f} %')
print(f' 年化收益率: {ann_flag} {abs(ann):>10.4f} %')
print(f' 最大回撤: ▼ {dd:>10.4f} %')
print(f' 夏普比率: {bt["sharpe_ratio"]:>10.4f}')
print(f' 交易笔数: 买入 {n_buy} 笔 / 卖出 {n_sell} 笔(共 {n_buy+n_sell} 笔)')
# ── 基准对比表 ─────────────────────────────────────────────────────────
benchmarks = result.get('benchmarks', [])
if benchmarks and result.get('backtest') and not any('error' in b and len(b) == 1 for b in benchmarks):
bt = result['backtest']
strat_ret = bt['total_return_pct']
strat_ann = bt['annualized_return_pct']
strat_dd = bt['max_drawdown_pct']
print(f' {"─"*54}')
print(f' {"基准对比(同期)":<10} {"总收益":>8} {"年化收益":>8} {"最大回撤":>8} {"超额收益":>9} {"信息比率":>8}')
print(f' {"─"*54}')
ret_sym = '▲' if strat_ret >= 0 else '▼'
ann_sym = '▲' if strat_ann >= 0 else '▼'
print(f' {"策略本身":<10} {ret_sym}{abs(strat_ret):>7.2f}% {ann_sym}{abs(strat_ann):>7.2f}% ▼{strat_dd:>7.2f}% {"—":>9} {"—":>8}')
for b in benchmarks:
if 'error' in b:
print(f' {b.get("name", b.get("code", "?")):<10} {"数据不足":>38}')
continue
b_ret = b['total_return_pct']
b_ann = b['annualized_return_pct']
b_dd = b['max_drawdown_pct']
exc = b['excess_return_pct']
ir = b['information_ratio']
rs = '▲' if b_ret >= 0 else '▼'
as_ = '▲' if b_ann >= 0 else '▼'
es = '▲' if exc >= 0 else '▼'
print(f' {b["name"]:<10} {rs}{abs(b_ret):>7.2f}% {as_}{abs(b_ann):>7.2f}% ▼{abs(b_dd):>7.2f}% {es}{abs(exc):>8.2f}% {ir:>8.2f}')
print(f' {"─"*54}')
print(sep)
# ── 因子IC汇总表 ───────────────────────────────────────────────────────
ic_stats = result.get('ic_stats', {})
if ic_stats:
_scr_set = set(_scr_names)
_sig_set = set(_sig_names)
print(f'\n{sep}')
print('【因子IC评估(Rank IC,预测能力分析)】')
print(f' 说明:IC为当日因子截面排名与次日收益率排名的斯皮尔曼相关系数')
print(f' {"─"*58}')
print(f' {"因子":<10} {"类型":>5} {"IC均值":>8} {"ICIR":>7} {"胜率":>7} {"|IC|均值":>9} {"评级":<10}')
print(f' {"─"*58}')
_all_ic_names = list(dict.fromkeys(_scr_names + _sig_names))
for _fn in _all_ic_names:
_ic = ic_stats.get(_fn, {})
_tag = '[选+信]' if (_fn in _scr_set and _fn in _sig_set) else \
('[选股]' if _fn in _scr_set else '[信号]')
if 'error' in _ic:
print(f' {_fn:<10} {_tag:>5} {"—":>8} {"—":>7} {"—":>7} {"—":>9} {"数据不足":<10}')
continue
_icm = _ic['ic_mean']
_icir = _ic['ic_ir']
_win = _ic['ic_win_rate'] * 100
_abs = _ic['ic_abs_mean']
_grade = '★★★ 优秀' if _icir > 1 else ('★★ 良好' if _icir > 0.5 else
('★ 一般' if _icir > 0 else '✗ 反向'))
_icm_s = f'+{_icm:.4f}' if _icm >= 0 else f'{_icm:.4f}'
print(f' {_fn:<10} {_tag:>5} {_icm_s:>8} {_icir:>7.2f} {_win:>6.1f}% {_abs:>9.4f} {_grade:<10}')
print(f' {"─"*58}')
print(f' 评级标准:ICIR>1优秀 / >0.5良好 / >0一般 / ≤0反向(负IC因子通常反向使用)')
# ── 因子深度解析 ───────────────────────────────────────────────────────
print(f'\n{sep}')
print('【因子深度解析】')
print(sep)
print(f'\n▌ 选股因子({len(_scr_names)} 个,串联过滤压缩股票池至候选股)')
for _i, _name in enumerate(_scr_names, 1):
_pct = _top_pcts.get(_name, 0)
_ic = _ic_stats.get(_name, {})
_step = next((s for s in _flog if s['factor'] == _name and s['status'] == 'ok'), {})
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
_before = _step.get('before', '?')
_after = _step.get('after', '?')
print(f'\n ▶ 第{_i}层 {_name} [{_dtag}] 保留前{int(_pct*100)}% ({_before} → {_after} 只)')
print(f' 预测能力: {_ic_line(_ic)}')
print(f' 因子定义: {_defn}')
if _hi:
print(f' 高值含义: {_hi}')
print(f'\n▌ 信号因子({len(_sig_names)} 个,逐日横截面排名驱动买卖)')
print(f' 买入阈值: {_buy_thr} → 综合排名前 {int((1-_buy_thr)*100)}% 触发买入')
print(f' 卖出阈值: {_sell_thr} → 综合排名后 {int(_sell_thr*100)}% 触发卖出')
for _name in _sig_names:
_ic = _ic_stats.get(_name, {})
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
print(f'\n ▶ {_name} [{_dtag}]')
print(f' 预测能力: {_ic_line(_ic)}')
print(f' 因子定义: {_defn}')
if _hi:
print(f' 高值含义: {_hi}')
# ── 选股过滤过程 ───────────────────────────────────────────────────────
print()
if result.get('filter_log'):
print('【选股过滤过程】')
for step in result['filter_log']:
status = step['status']
pct_str = f'保留前{int(step.get("top_pct", 0)*100)}%'
if status == 'ok':
print(f' {step["factor"]} {pct_str} 截面日={step["ref_date"]} '
f'{step["before"]} → {step["after"]} 只 '
f'(实留 {step["kept"]}/{step["snapshot_size"]})')
else:
print(f' {step["factor"]} {pct_str} 跳过({status}) {step["before"]} 只不变')
print(f'\n【最终入选】 {result["final_pool_count"]} 只')
if result['final_pool']:
print(f' {result["final_pool"]}')
# ── 回测结果 ───────────────────────────────────────────────────────────
if 'error' in result:
print(f'\n【错误】 {result["error"]}')
elif 'backtest' in result:
bt = result['backtest']
print()
print('【回测结果】(信号驱动买卖 / 等额资金分配)')
print(f' 回测区间: {bt["start_date"]} → {bt["end_date"]}')
print(f' 交易天数: {bt["trading_days"]} 日')
print(f' 初始资金: {bt["initial_cash"]:,.0f} 元')
print(f' 期末资金: {bt["final_value"]:,.2f} 元')
print(f' 总收益率: {bt["total_return_pct"]:+.4f} %')
print(f' 年化收益率: {bt["annualized_return_pct"]:+.4f} %')
print(f' 最大回撤: {bt["max_drawdown_pct"]:.4f} %')
print(f' 夏普比率: {bt["sharpe_ratio"]:.4f}')
ec = bt['equity_curve']
mid = len(ec) // 2
print(f' 权益曲线(首/中/尾): [{ec[0]:,.0f} ... {ec[mid]:,.0f} ... {ec[-1]:,.0f}]')
tl = result.get('trade_log', [])
print(f' 交易笔数: 买入 {sum(1 for t in tl if t["action"]=="BUY")} 笔 / '
f'卖出 {sum(1 for t in tl if "SELL" in t["action"])} 笔')
# ── Top N 个股详情 ─────────────────────────────────────────────────────
top_stocks = result.get('top_stocks', [])
buy_thr = sc['buy_thresh']
if top_stocks:
print(f'\n{sep}')
print(f'【Top {len(top_stocks)} 盈利个股详情】')
for rank_i, stock in enumerate(top_stocks, 1):
code = stock['code']
total_pnl = stock['total_pnl']
trades = stock['trades']
print(f'\n #{rank_i} {code} 累计盈亏: {total_pnl:+,.2f} 元')
print(f' {"─"*54}')
for tr in trades:
action = tr['action']
comp_rank = tr.get('composite_rank')
comp_str = f'{comp_rank:.4f}' if comp_rank is not None else 'N/A'
if action == 'BUY':
flag = f'>={buy_thr} ✓' if (comp_rank is not None and comp_rank >= buy_thr) else ''
print(f' [买入] {tr["date"]} 价格={tr["price"]:.3f} 综合排名={comp_str} {flag}')
else:
label = '卖出' if action == 'SELL' else '强平'
print(f' [{label}] {tr["date"]} 价格={tr["price"]:.3f} '
f'持仓{tr.get("hold_days", "?")}日 '
f'盈亏={tr.get("pnl", 0):+,.2f}元 ({tr.get("pnl_pct", 0):+.2f}%)')
# ── 本次策略参数速查卡 ─────────────────────────────────────────────────
print(f'\n{sep}')
print('【本次策略参数速查卡】')
print(sep)
_ref_date_str = next((s['ref_date'] for s in _flog if 'ref_date' in s), '?')
print(f'\n▌ 选股因子 {len(_scr_names)} 个(截面日 {_ref_date_str} 静态过滤)')
print(f' 保留比例随机范围: [5%, 20%] 每层独立抽取')
for _i, _name in enumerate(_scr_names, 1):
_pct = _top_pcts.get(_name, 0)
_step = next((s for s in _flog if s['factor'] == _name and s['status'] == 'ok'), {})
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
_b = _step.get('before', '?')
_a = _step.get('after', '?')
_ic = _ic_stats.get(_name, {})
_ic_tag = ''
if _ic and 'error' not in _ic:
_icir = _ic['ic_ir']
_ic_tag = f' ICIR={_icir:.2f}{"★★★" if _icir>1 else ("★★" if _icir>0.5 else ("★" if _icir>0 else "✗"))}'
print(f' 第{_i}层 {_name} [{_dtag}] 本次保留前 {int(_pct*100)}%{_ic_tag}')
print(f' {_defn}')
if _hi:
print(f' 高值: {_hi}')
print(f' 过滤: {_b} → {_a} 只')
print(f'\n▌ 信号因子 {len(_sig_names)} 个(逐日截面排名,均值作综合排名)')
print(f' ┌ 买入阈值: {_buy_thr} (随机范围 [0.55, 0.82]) 综合排名前 {int((1-_buy_thr)*100)}% 买入')
print(f' └ 卖出阈值: {_sell_thr} (随机范围 [0.30, 0.55]) 综合排名后 {int(_sell_thr*100)}% 卖出')
for _name in _sig_names:
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
_ic = _ic_stats.get(_name, {})
_ic_tag = ''
if _ic and 'error' not in _ic:
_icir = _ic['ic_ir']
_ic_tag = f' ICIR={_icir:.2f}{"★★★" if _icir>1 else ("★★" if _icir>0.5 else ("★" if _icir>0 else "✗"))}'
print(f' {_name} [{_dtag}]{_ic_tag}')
print(f' {_defn}')
if _hi:
print(f' 高值: {_hi}')
print(f'\n▌ 持仓参数')
if result.get('backtest'):
bt = result['backtest']
print(f' 候选池上限: {result["final_pool_count"]} 只 最大持仓: 5 只 单仓预算: 初始资金 ÷ 5')
print(f' 回测区间: {bt["start_date"]} → {bt["end_date"]} 手续费: 买入+卖出各 0.1%')
print(sep)
FILE:scripts/indicators.py
"""
indicators.py - 技术指标计算模块
功能:
1. 技术指标计算(SMA, EMA, RSI, MACD, BB, ATR等100+指标)
2. 计算结果缓存到数据库
3. 默认使用复权价格(后复权)
设计原则:
- 函数功能单一、最小粒度
- 查询优先使用缓存,计算后存入数据库
- 使用data_fetcher.py获取基础数据
- 默认使用复权价格,可通过参数控制
"""
import json
from typing import Optional, Dict, List, Tuple
from sqlalchemy import text
from data_fetcher import getEngine
from data_fetcher import (
query_daily_kline,
query_daily_basic,
query_stock_limit,
query_daily_limit_list,
query_daily_bomb_list,
)
from define import DailyKline, DailyBasic, StockLimit, DailyLimitList, DailyBombList
import math
def init_indicators_db():
"""初始化指标缓存数据库表
创建 cached_indicators 表(如不存在),用于缓存所有指标计算结果,并建立查询索引。
每次计算前先查缓存,命中则直接返回,避免对相同参数重复计算。
"""
with getEngine().connect() as conn:
conn.execute(text("""
CREATE TABLE IF NOT EXISTS cached_indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
indicator_type TEXT NOT NULL,
period INTEGER,
use_adjusted INTEGER DEFAULT 1,
date TEXT NOT NULL,
value TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, indicator_type, period, use_adjusted, date)
);
"""))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_indicators_lookup ON cached_indicators(code, indicator_type, period, use_adjusted, date);"))
conn.commit()
def _get_cached_indicator(code: str, indicator_type: str, period: int, date: str, use_adjusted: bool = True) -> Optional[str]:
"""从缓存表中查询指标值
Args:
code: 股票代码
indicator_type: 指标类型字符串,如 'SMA'、'RSI'
period: 周期参数(复合参数如MACD已编码为单个整数)
date: 查询日期
use_adjusted: 是否为复权计算结果
Returns:
str: 缓存的字符串值,未命中返回 None
"""
with getEngine().connect() as conn:
cursor = conn.execute(text(
"SELECT value FROM cached_indicators WHERE code=:code AND indicator_type=:indicator_type AND period=:period AND use_adjusted=:use_adjusted AND date=:date"
), {"code": code, "indicator_type": indicator_type, "period": period, "use_adjusted": 1 if use_adjusted else 0, "date": date})
row = cursor.fetchone()
return row[0] if row else None
def _save_indicator(code: str, indicator_type: str, period: int, date: str, value: str, use_adjusted: bool = True):
"""将指标计算结果保存到缓存表
已存在则替换(INSERT OR REPLACE),确保缓存始终为最新值。
Args:
code: 股票代码
indicator_type: 指标类型字符串
period: 周期参数
date: 计算日期
value: 指标值的字符串表示(float 用 str(),dict 用 str() 后 还原)
use_adjusted: 是否为复权计算结果
"""
with getEngine().connect() as conn:
conn.execute(text(
"INSERT OR REPLACE INTO cached_indicators (code, indicator_type, period, use_adjusted, date, value) VALUES (:code, :indicator_type, :period, :use_adjusted, :date, :value)"
), {"code": code, "indicator_type": indicator_type, "period": period, "use_adjusted": 1 if use_adjusted else 0, "date": date, "value": value})
conn.commit()
def _get_klines_before_date(code: str, date: str, limit: int) -> List[DailyKline]:
"""获取指定日期(含)前最近 limit 根K线,按时间升序排列
Args:
code: 股票代码
date: 截止日期(含)
limit: 最多返回的K线根数
Returns:
List[DailyKline]: 按日期升序排列的K线列表(最新一根在末尾)
"""
klines = query_daily_kline(
codes=[code],
end_date=date,
limit=limit,
order_by="date DESC"
)
return klines[::-1]
def _get_klines_range(code: str, start_date: str, end_date: str) -> List[DailyKline]:
"""获取指定日期范围内的K线,按时间升序排列
Args:
code: 股票代码
start_date: 起始日期(含),格式 'YYYY-MM-DD'
end_date: 结束日期(含),格式 'YYYY-MM-DD'
Returns:
List[DailyKline]: 按日期升序排列的K线列表
"""
klines = query_daily_kline(
codes=[code],
start_date=start_date,
end_date=end_date,
order_by="date ASC"
)
return klines
def _get_adj_factor(code: str, date: str) -> Optional[float]:
"""获取指定日期的复权因子
Args:
code: 股票代码
date: 查询日期,格式 'YYYY-MM-DD'
Returns:
float: 该日期的复权因子,查询不到返回 None
"""
daily_basics = query_daily_basic(ts_codes=[code], trade_date=date)
if daily_basics:
return daily_basics[0].adj_factor
return None
def _get_adj_factors_for_klines(klines: List[DailyKline]) -> Dict[str, float]:
"""批量获取K线覆盖日期范围内的所有复权因子
Args:
klines: K线列表,用于确定查询的日期范围和股票代码
Returns:
dict: {日期字符串: 复权因子} 的映射,klines 为空时返回空字典
"""
if not klines:
return {}
code = klines[0].code
start_date = klines[0].date
end_date = klines[-1].date
daily_basics = query_daily_basic(
ts_codes=[code],
start_date=start_date,
end_date=end_date
)
return {basic.trade_date: basic.adj_factor for basic in daily_basics}
def _adjust_price(price: float, adj_factor: float) -> float:
"""计算后复权价格
后复权从上市日起累积复权,公式:复权价 = 原始价 * 该日复权因子
Args:
price: 原始价格
adj_factor: 该K线日期对应的复权因子
Returns:
float: 后复权后的价格,因子为0时原样返回原始价格
"""
if adj_factor == 0:
return price
return price * adj_factor
def _adjust_klines(klines: List[DailyKline], adj_factors: Dict[str, float]) -> List[DailyKline]:
"""对K线列表执行后复权处理
每根K线的价格字段(open/high/low/close/amount/pre_close/change)乘以对应日期的
复权因子,成交量不做调整。
Args:
klines: 原始K线列表
adj_factors: 复权因子字典 {日期字符串: 复权因子}
Returns:
List[DailyKline]: 复权后的新K线列表(原始列表不被修改);
klines 或 adj_factors 为空时直接返回原始 klines
"""
if not klines or not adj_factors:
return klines
adjusted_klines = []
for kline in klines:
adj_factor = adj_factors.get(kline.date, 1.0)
adjusted_kline = DailyKline(
date=kline.date,
code=kline.code,
open=_adjust_price(kline.open, adj_factor),
high=_adjust_price(kline.high, adj_factor),
low=_adjust_price(kline.low, adj_factor),
close=_adjust_price(kline.close, adj_factor),
volume=kline.volume,
amount=_adjust_price(kline.amount, adj_factor) if kline.amount else 0.0,
adjustflag=kline.adjustflag,
turn=kline.turn,
pctChg=kline.pctChg,
pre_close=_adjust_price(kline.pre_close, adj_factor) if kline.pre_close else 0.0,
change=_adjust_price(kline.change, adj_factor) if kline.change else 0.0
)
adjusted_klines.append(adjusted_kline)
return adjusted_klines
def _ema_series(values: list, period: int) -> list:
"""计算EMA序列(内部辅助函数)
对输入数值列表计算指数移动平均,返回与输入等长的序列。
平滑因子 k = 2 / (period + 1),首值直接取第一个输入值。
Args:
values: 原始数值列表
period: EMA 平滑周期
Returns:
list: 与 values 等长的 EMA 值列表;values 为空时返回空列表
"""
if not values:
return []
k = 2.0 / (period + 1)
result = [values[0]]
for v in values[1:]:
result.append(result[-1] + k * (v - result[-1]))
return result
# ============================================================
# 第一梯队:最常用指标
# ============================================================
def get_sma(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""简单移动平均线 SMA(Simple Moving Average)
对过去 period 根K线的收盘价取算术平均,是最基础的趋势跟踪指标。
数值平滑,对价格变化反应较慢,适合判断中长期趋势方向。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 均线周期,默认20(即20日均线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日SMA值(元);数据不足 period 根K线时返回 None
"""
cached = _get_cached_indicator(code, 'SMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sma = sum(k.close for k in klines) / period
_save_indicator(code, 'SMA', period, date, json.dumps(sma), use_adjusted)
return sma
def get_ema(code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""指数移动平均线 EMA(Exponential Moving Average)
对近期价格赋予更高权重的移动平均,对价格变化比 SMA 更敏感。
公式:EMA = 上一EMA + 乘数 * (今收盘 - 上一EMA),乘数 = 2/(period+1)
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认12(常用12/26/9组合配合MACD)
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日EMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'EMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period * 2)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
ema = prices[0]
multiplier = 2 / (period + 1)
for price in prices[1:]:
ema = (price - ema) * multiplier + ema
_save_indicator(code, 'EMA', period, date, json.dumps(ema), use_adjusted)
return ema
def get_wma(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""加权移动平均线 WMA(Weighted Moving Average)
越近的K线权重越高(最近一天权重=period,最早一天权重=1),
比 SMA 更快响应近期价格变化,适合短中期趋势判断。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日WMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'WMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
weights = list(range(1, period + 1))
weighted_sum = sum(k.close * w for k, w in zip(klines, weights))
wma = weighted_sum / sum(weights)
_save_indicator(code, 'WMA', period, date, json.dumps(wma), use_adjusted)
return wma
def get_tema(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""三重指数移动平均线 TEMA(Triple Exponential Moving Average)
TEMA = 3*EMA1 - 3*EMA2 + EMA3,通过三重EMA消除滞后,
比单重/双重EMA对价格反应更迅速,适合短线趋势判断。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日TEMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'TEMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
ema1 = get_ema(code, date, period, use_adjusted)
if ema1 is None:
return None
ema2 = get_ema(code, date, period, use_adjusted)
if ema2 is None:
return None
ema3 = get_ema(code, date, period, use_adjusted)
if ema3 is None:
return None
tema = 3 * ema1 - 3 * ema2 + ema3
_save_indicator(code, 'TEMA', period, date, json.dumps(tema), use_adjusted)
return tema
def get_rsi(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""相对强弱指数 RSI(Relative Strength Index)
衡量过去 period 日内上涨幅度与下跌幅度的比值,反映超买超卖状态。
取值0-100,通常 >70 视为超买,<30 视为超卖。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日RSI值(0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'RSI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
gains, losses = [], []
for i in range(1, len(klines)):
diff = klines[i].close - klines[i-1].close
if diff > 0:
gains.append(diff)
losses.append(0)
else:
gains.append(0)
losses.append(abs(diff))
avg_gain = sum(gains) / period
avg_loss = sum(losses) / period
if avg_loss == 0:
rsi = 100.0
else:
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
_save_indicator(code, 'RSI', period, date, json.dumps(rsi), use_adjusted)
return rsi
def get_macd(code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""MACD 指数平滑异同移动平均线(Moving Average Convergence Divergence)
MACD线 = 快线EMA - 慢线EMA,Signal线 = MACD线的EMA,柱状图 = MACD - Signal。
用于判断价格动量和趋势转折,MACD上穿0轴为多头信号,下穿为空头信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fast: 快线EMA周期,默认12
slow: 慢线EMA周期,默认26
signal: 信号线EMA周期,默认9(当前近似处理)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'macd': MACD线, 'signal': 信号线, 'histogram': 柱状图}(单位:元);
数据不足时返回 None
"""
period_key = fast * 10000 + slow * 100 + signal
cached = _get_cached_indicator(code, 'MACD', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ema_fast = get_ema(code, date, fast, use_adjusted)
ema_slow = get_ema(code, date, slow, use_adjusted)
if ema_fast is None or ema_slow is None:
return None
macd_line = ema_fast - ema_slow
macd = {'macd': macd_line, 'signal': macd_line, 'histogram': 0}
_save_indicator(code, 'MACD', period_key, date, json.dumps(macd), use_adjusted)
return macd
def get_bollinger_bands(code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""布林带 BOLL(Bollinger Bands)
中轨 = SMA,上轨 = 中轨 + std_dev * 标准差,下轨 = 中轨 - std_dev * 标准差。
价格接近上轨为超买,接近下轨为超卖,带宽收窄预示行情即将爆发。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
std_dev: 标准差倍数,默认2(即±2σ,覆盖约95%的波动区间)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨, 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'BB', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
middle = sum(prices) / period
variance = sum((p - middle) ** 2 for p in prices) / period
std = variance ** 0.5
bb = {
'upper': middle + std_dev * std,
'middle': middle,
'lower': middle - std_dev * std
}
_save_indicator(code, 'BB', period, date, json.dumps(bb), use_adjusted)
return bb
def get_atr(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""平均真实波幅 ATR(Average True Range)
对过去 period 根K线的真实波幅(TR)取简单均值,衡量市场波动性。
TR = max(最高-最低, |最高-昨收|, |最低-昨收|),ATR 越大说明近期波动越剧烈。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ATR值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'ATR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
tr_values = []
for i in range(1, len(klines)):
high = klines[i].high
low = klines[i].low
prev_close = klines[i-1].close
tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
tr_values.append(tr)
atr = sum(tr_values) / period
_save_indicator(code, 'ATR', period, date, json.dumps(atr), use_adjusted)
return atr
def get_mom(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""动量指标 MOM(Momentum)
当前收盘价与 period 天前收盘价的差值,衡量价格变动的绝对速度。
正值表示上涨动能,负值表示下跌动能,0轴穿越可作为趋势转折信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日动量值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MOM', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
mom = klines[-1].close - klines[0].close
_save_indicator(code, 'MOM', period, date, json.dumps(mom), use_adjusted)
return mom
def get_roc(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""变动率指标 ROC(Rate of Change,%)
(今收盘 - N日前收盘) / N日前收盘 * 100,是动量的百分比表达。
正值表示相对N日前上涨,负值表示下跌,比 MOM 更适合横向对比不同价位股票。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ROC值(%);数据不足或N日前收盘为0时返回 None
"""
cached = _get_cached_indicator(code, 'ROC', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if klines[0].close == 0:
return None
roc = ((klines[-1].close - klines[0].close) / klines[0].close) * 100
_save_indicator(code, 'ROC', period, date, json.dumps(roc), use_adjusted)
return roc
def get_cci(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""顺势指标 CCI(Commodity Channel Index)
(典型价格 - SMA典型价格) / (0.015 * 平均绝对偏差),衡量价格偏离均值的程度。
通常 >100 视为超买,<-100 视为超卖,适合捕捉短期强弱拐点。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日CCI值(无量纲);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'CCI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
typical_prices = [(k.high + k.low + k.close) / 3 for k in klines]
sma_tp = sum(typical_prices) / period
mean_deviation = sum(abs(tp - sma_tp) for tp in typical_prices) / period
if mean_deviation == 0:
cci = 0.0
else:
cci = (typical_prices[-1] - sma_tp) / (0.015 * mean_deviation)
_save_indicator(code, 'CCI', period, date, json.dumps(cci), use_adjusted)
return cci
def get_obv(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""能量潮 OBV(On Balance Volume)
价格上涨日累加成交量,下跌日扣减成交量,累积值反映资金流入/流出方向。
OBV 持续上升说明主动买盘积极,用于验证价格趋势是否有量能支撑。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日OBV累计值(手);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'OBV', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
obv = 0.0
for i in range(1, len(klines)):
if klines[i].close > klines[i-1].close:
obv += klines[i].volume
elif klines[i].close < klines[i-1].close:
obv -= klines[i].volume
_save_indicator(code, 'OBV', period, date, json.dumps(obv), use_adjusted)
return obv
def get_volume(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""成交量统计 VOLUME
返回当日成交量及近 period 日的均量,用于判断量能是否放大/萎缩。
当日量 > 均量说明放量,当日量 < 均量说明缩量。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 均量计算周期,默认20
use_adjusted: 是否使用后复权价格,默认True(不影响成交量本身)
Returns:
dict: {'current': 当日成交量, 'sma': period日均量}(单位:手);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'VOLUME', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
current_vol = klines[-1].volume
sma_vol = sum(k.volume for k in klines) / len(klines)
vol_data = {'current': current_vol, 'sma': sma_vol}
_save_indicator(code, 'VOLUME', period, date, json.dumps(vol_data), use_adjusted)
return vol_data
def get_kdj(code: str, date: str, n: int = 9, m1: int = 3, m2: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""随机指标 KDJ
基于 n 日内最高/最低价计算RSV(未成熟随机值),再经平滑得到K、D、J值。
K>80 视为超买,K<20 视为超卖;J线最灵敏,常用K线与D线的交叉作为买卖信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
n: 计算RSV的周期,默认9
m1: K线平滑系数(1/m1),默认3
m2: D线平滑系数(1/m2),默认3
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'k': K值, 'd': D值, 'j': J值}(取值大致0-100,J可超出范围);
数据不足时返回 None
"""
period_key = n * 10000 + m1 * 100 + m2
cached = _get_cached_indicator(code, 'KDJ', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, n)
if len(klines) < n:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
low_n = min(k.low for k in klines)
high_n = max(k.high for k in klines)
if high_n - low_n == 0:
rsv = 50.0
else:
rsv = ((klines[-1].close - low_n) / (high_n - low_n)) * 100
k = rsv
d = k
j = 3 * k - 2 * d
kdj = {'k': k, 'd': d, 'j': j}
_save_indicator(code, 'KDJ', period_key, date, json.dumps(kdj), use_adjusted)
return kdj
def get_dmi(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""趋向指标 DMI(Directional Movement Index)
+DI 衡量上升趋势力度,-DI 衡量下降趋势力度,ADX 衡量趋势整体强弱(不含方向)。
+DI 上穿 -DI 为买入信号,ADX>25 说明市场趋势较强。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'pdi': +DI值, 'mdi': -DI值, 'adx': ADX值}(取值0-100);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DMI', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
plus_dm = 0.0
minus_dm = 0.0
tr_sum = 0.0
for i in range(1, len(klines)):
high_diff = klines[i].high - klines[i-1].high
low_diff = klines[i-1].low - klines[i].low
if high_diff > low_diff and high_diff > 0:
plus_dm += high_diff
if low_diff > high_diff and low_diff > 0:
minus_dm += low_diff
tr = max(klines[i].high - klines[i].low,
abs(klines[i].high - klines[i-1].close),
abs(klines[i].low - klines[i-1].close))
tr_sum += tr
if tr_sum == 0:
pdi = 0.0
mdi = 0.0
else:
pdi = (plus_dm / tr_sum) * 100
mdi = (minus_dm / tr_sum) * 100
adx = abs(pdi - mdi) / (pdi + mdi) * 100 if (pdi + mdi) > 0 else 0
dmi = {'pdi': pdi, 'mdi': mdi, 'adx': adx}
_save_indicator(code, 'DMI', period, date, json.dumps(dmi), use_adjusted)
return dmi
def get_trix(code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""三重指数平滑移动平均率 TRIX(Triple Exponential Average,%)
对收盘价连续做三次 EMA,取最后一次 EMA 的日变化率(%)。
EMA1 = EMA(close, N),EMA2 = EMA(EMA1, N),EMA3 = EMA(EMA2, N)
TRIX = (EMA3 - EMA3[prev]) / EMA3[prev] * 100
上穿 0 轴为买入信号,下穿为卖出信号;配合 MATRIX(TRIX 的 M 日均线)使用更佳。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: EMA 周期,默认12
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日 TRIX 值(%);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'TRIX', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period * 3 + 5)
if len(klines) < period * 3:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
closes = [k.close for k in klines]
ema1 = _ema_series(closes, period)
ema2 = _ema_series(ema1, period)
ema3 = _ema_series(ema2, period)
if len(ema3) < 2 or ema3[-2] == 0:
return None
trix = (ema3[-1] - ema3[-2]) / ema3[-2] * 100
_save_indicator(code, 'TRIX', period, date, json.dumps(trix), use_adjusted)
return trix
def get_sar(code: str, date: str, af_start: float = 0.02, af_max: float = 0.2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""抛物线转向指标 SAR(Parabolic Stop And Reverse)
价格上涨时 SAR 跟随在价格下方,下跌时跟随在价格上方,触碰 SAR 即为趋势反转信号。
加速因子(af)随趋势延续逐步增大,使 SAR 越来越贴近价格。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
af_start: 加速因子初始值,默认0.02
af_max: 加速因子最大值,默认0.2
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'sar': SAR价格(元), 'trend': 趋势方向(1=上涨, -1=下跌)};
数据不足时返回 None
"""
period_key = int(af_start * 10000 + af_max)
cached = _get_cached_indicator(code, 'SAR', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, 10)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sar = klines[0].low
trend = 1
ep = klines[0].high
af = af_start
sar_data = {'sar': sar, 'trend': trend}
_save_indicator(code, 'SAR', period_key, date, json.dumps(sar_data), use_adjusted)
return sar_data
def get_williams_r(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""威廉指标 WR(Williams %R)
(period日最高 - 今收) / (period日最高 - period日最低) * 100。
取值0-100,接近0为超买,接近100为超卖(注意:方向与RSI相反)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日WR值(0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'WR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
high_n = max(k.high for k in klines)
low_n = min(k.low for k in klines)
if high_n - low_n == 0:
wr = 50.0
else:
wr = ((high_n - klines[-1].close) / (high_n - low_n)) * 100
_save_indicator(code, 'WR', period, date, json.dumps(wr), use_adjusted)
return wr
def get_psycho(code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""心理线 PSY(Psychological Line)
过去 period 日中上涨天数占比(%),衡量多数投资者的心理倾向。
>75% 表示过度乐观(超买预警),<25% 表示过度悲观(超卖预警)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认12
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日PSY值(%,0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'PSY', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
up_days = 0
for i in range(1, len(klines)):
if klines[i].close > klines[i-1].close:
up_days += 1
psy = (up_days / period) * 100
_save_indicator(code, 'PSY', period, date, json.dumps(psy), use_adjusted)
return psy
def get_bias(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""乖离率 BIAS(Bias Ratio,%)
(今收盘 - N日SMA) / N日SMA * 100,衡量股价偏离均线的程度。
正值表示价格在均线上方,负值在下方,极端偏离值常预示均值回归行情。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 均线周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日乖离率(%);数据不足或SMA为0时返回 None
"""
cached = _get_cached_indicator(code, 'BIAS', period, date, use_adjusted)
if cached is not None:
return float(cached)
sma = get_sma(code, date, period, use_adjusted)
klines = _get_klines_before_date(code, date, 1)
if sma is None or len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if sma == 0:
return None
bias = ((klines[-1].close - sma) / sma) * 100
_save_indicator(code, 'BIAS', period, date, json.dumps(bias), use_adjusted)
return bias
def get_tr(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""真实波幅 TR(True Range)
单根K线的真实波动范围:max(最高-最低, |最高-昨收|, |最低-昨收|)。
是计算 ATR 的基础,跳空缺口越大则 TR 值越大。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日TR值(元);少于2根K线时返回 None
"""
cached = _get_cached_indicator(code, 'TR', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 2)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
high = klines[-1].high
low = klines[-1].low
prev_close = klines[-2].close
tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
_save_indicator(code, 'TR', 1, date, json.dumps(tr), use_adjusted)
return tr
def get_natr(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""归一化平均真实波幅 NATR(Normalized Average True Range,%)
ATR / 当日收盘价 * 100,是 ATR 的百分比形式,
消除了股价高低对波幅绝对值的影响,便于不同价位股票横向比较。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: ATR计算周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日NATR值(%);数据不足或收盘价为0时返回 None
"""
cached = _get_cached_indicator(code, 'NATR', period, date, use_adjusted)
if cached is not None:
return float(cached)
atr = get_atr(code, date, period, use_adjusted)
klines = _get_klines_before_date(code, date, 1)
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if atr is None or len(klines) == 0 or klines[-1].close == 0:
return None
natr = (atr / klines[-1].close) * 100
_save_indicator(code, 'NATR', period, date, json.dumps(natr), use_adjusted)
return natr
def get_vwap(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""成交量加权平均价 VWAP(Volume Weighted Average Price)
Σ(典型价格 * 成交量) / Σ(成交量),反映过去 period 日的成交重心。
价格在 VWAP 上方说明多头占优,机构常以 VWAP 作为买卖基准价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日VWAP值(元);成交量为0或数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'VWAP', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
total_pv = 0.0
total_vol = 0.0
for k in klines:
typical_price = (k.high + k.low + k.close) / 3
total_pv += typical_price * k.volume
total_vol += k.volume
if total_vol == 0:
return None
vwap = total_pv / total_vol
_save_indicator(code, 'VWAP', period, date, json.dumps(vwap), use_adjusted)
return vwap
def get_ad(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""累积/派发线 AD(Accumulation/Distribution Line)
每日 CLV = [(收-低) - (高-收)] / (高-低),CLV * 成交量后累加。
CLV 衡量收盘价在当日高低范围中的位置,AD 持续上升表示主力在吸筹(积累)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日AD累积值(量纲:手);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'AD', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
ad_line = 0.0
for k in klines:
high_low = k.high - k.low
if high_low == 0:
clv = 0.0
else:
clv = ((k.close - k.low) - (k.high - k.close)) / high_low
ad_line += clv * k.volume
_save_indicator(code, 'AD', period, date, json.dumps(ad_line), use_adjusted)
return ad_line
def get_adosc(code: str, date: str, fast: int = 3, slow: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""AD震荡指标 ADOSC(Accumulation/Distribution Oscillator)
快周期AD - 慢周期AD,衡量资金流向的变化速度(AD的动量)。
正值且上升表示买盘在增强,负值且下降表示卖盘在增强。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fast: 快速AD的周期,默认3
slow: 慢速AD的周期,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ADOSC值;数据不足时返回 None
"""
period_key = fast * 100 + slow
cached = _get_cached_indicator(code, 'ADOSC', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
ad_fast = get_ad(code, date, fast, use_adjusted)
ad_slow = get_ad(code, date, slow, use_adjusted)
if ad_fast is None or ad_slow is None:
return None
adosc = ad_fast - ad_slow
_save_indicator(code, 'ADOSC', period_key, date, json.dumps(adosc), use_adjusted)
return adosc
def get_mfi(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""资金流量指标 MFI(Money Flow Index,0-100)
结合典型价格和成交量的动量指标,原理类似 RSI 但加入了成交量权重(量价共振)。
取值0-100,>80 视为超买,<20 视为超卖。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日MFI值(0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MFI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
positive_mf = 0.0
negative_mf = 0.0
for i in range(1, len(klines)):
typical_price = (klines[i].high + klines[i].low + klines[i].close) / 3
prev_tp = (klines[i-1].high + klines[i-1].low + klines[i-1].close) / 3
money_flow = typical_price * klines[i].volume
if typical_price > prev_tp:
positive_mf += money_flow
elif typical_price < prev_tp:
negative_mf += money_flow
if negative_mf == 0:
mfi = 100.0
else:
mfr = positive_mf / negative_mf
mfi = 100 - (100 / (1 + mfr))
_save_indicator(code, 'MFI', period, date, json.dumps(mfi), use_adjusted)
return mfi
def get_cmo(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""钱德动量摆动指标 CMO(Chande Momentum Oscillator,-100到100)
(上涨幅度总和 - 下跌幅度总和) / (上涨+下跌幅度总和) * 100。
取值-100到100,>50 超买,<-50 超卖,穿越0轴视为趋势转变信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日CMO值(-100到100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'CMO', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
up_sum = 0.0
down_sum = 0.0
for i in range(1, len(klines)):
diff = klines[i].close - klines[i-1].close
if diff > 0:
up_sum += diff
else:
down_sum += abs(diff)
if up_sum + down_sum == 0:
cmo = 0.0
else:
cmo = ((up_sum - down_sum) / (up_sum + down_sum)) * 100
_save_indicator(code, 'CMO', period, date, json.dumps(cmo), use_adjusted)
return cmo
def get_rocp(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""价格变动率 ROCP(Rate of Change Percentage)
(今收盘 - N日前收盘) / N日前收盘,结果为小数而非百分比(区别于 ROC)。
例:上涨5%返回0.05,下跌3%返回-0.03。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ROCP值(小数,非百分比);数据不足或N日前收盘为0时返回 None
"""
cached = _get_cached_indicator(code, 'ROCP', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1 or klines[0].close == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
rocp = (klines[-1].close - klines[0].close) / klines[0].close
_save_indicator(code, 'ROCP', period, date, json.dumps(rocp), use_adjusted)
return rocp
def get_rocr(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""价格变动率比 ROCR(Rate of Change Ratio)
今收盘 / N日前收盘,即价格的倍数比。
=1.0 表示与N日前持平,=1.05 表示上涨5%,=0.95 表示下跌5%。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ROCR值(倍数);数据不足或N日前收盘为0时返回 None
"""
cached = _get_cached_indicator(code, 'ROCR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1 or klines[0].close == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
rocr = klines[-1].close / klines[0].close
_save_indicator(code, 'ROCR', period, date, json.dumps(rocr), use_adjusted)
return rocr
def get_aroon(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""阿隆指标 AROON(Aroon Indicator)
AROON_UP = (period - 距最高点天数) / period * 100
AROON_DOWN = (period - 距最低点天数) / period * 100
AROON_OSC = UP - DOWN,取值-100到100,衡量趋势强度和方向变化。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'up': 上行强度(0-100), 'down': 下行强度(0-100), 'osc': 震荡值(-100到100)};
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'AROON', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
highs = [k.high for k in klines]
lows = [k.low for k in klines]
high_idx = highs.index(max(highs))
low_idx = lows.index(min(lows))
aroon_up = ((period - high_idx) / period) * 100
aroon_down = ((period - low_idx) / period) * 100
aroon_osc = aroon_up - aroon_down
aroon = {'up': aroon_up, 'down': aroon_down, 'osc': aroon_osc}
_save_indicator(code, 'AROON', period, date, json.dumps(aroon), use_adjusted)
return aroon
def get_ultosc(code: str, date: str, period1: int = 7, period2: int = 14, period3: int = 28, use_adjusted: bool = True) -> Optional[float]:
"""终极振荡器 ULTOSC(Ultimate Oscillator,0-100)
[存根函数] 理论上综合三个不同周期的买盘压力计算超买超卖,
>70 超买,<30 超卖;当前固定返回 50.0(中性值)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period1: 短周期,默认7
period2: 中周期,默认14
period3: 长周期,默认28
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 50.0(未完整实现);数据不足时返回 None
"""
period_key = period1 * 10000 + period2 * 100 + period3
cached = _get_cached_indicator(code, 'ULTOSC', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, max(period1, period2, period3) + 1)
if len(klines) < max(period1, period2, period3) + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
ultosc = 50.0
_save_indicator(code, 'ULTOSC', period_key, date, json.dumps(ultosc), use_adjusted)
return ultosc
# ============================================================
# 第四梯队:专业指标
# ============================================================
def get_dema(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""双重指数移动平均线 DEMA(Double Exponential Moving Average)
DEMA = 2 * EMA - EMA(EMA),比单重 EMA 减少滞后,对价格变化响应更快。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日DEMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DEMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
ema1 = get_ema(code, date, period, use_adjusted)
ema2 = get_ema(code, date, period, use_adjusted)
if ema1 is None or ema2 is None:
return None
dema = 2 * ema1 - ema2
_save_indicator(code, 'DEMA', period, date, json.dumps(dema), use_adjusted)
return dema
def get_kama(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""考夫曼自适应移动平均线 KAMA(Kaufman Adaptive Moving Average)
[存根函数] 理论上根据市场效率比率自动调整平滑系数,
趋势行情时快速跟踪,震荡行情时近乎平坦;当前直接返回最新收盘价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 效率比率计算周期,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日KAMA值(元),当前近似为最新收盘价;数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'KAMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
kama = klines[-1].close
_save_indicator(code, 'KAMA', period, date, json.dumps(kama), use_adjusted)
return kama
def get_midpoint(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""中点价格 MIDPOINT
过去 period 日内 (最高价极值 + 最低价极值) / 2,代表价格区间的中心位置。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回看周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 过去period日的中点价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MIDPOINT', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
highest = max(k.high for k in klines)
lowest = min(k.low for k in klines)
midpoint = (highest + lowest) / 2
_save_indicator(code, 'MIDPOINT', period, date, json.dumps(midpoint), use_adjusted)
return midpoint
def get_midprice(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""中点价格别名 MIDPRICE(等同于 MIDPOINT)
直接委托给 get_midpoint,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回看周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 过去period日的中点价格(元);数据不足时返回 None
"""
return get_midpoint(code, date, period, use_adjusted)
def get_pvi(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""正成交量指标 PVI(Positive Volume Index)
[存根函数] 理论上只在成交量增大时更新累计价格变化,反映跟风散户的行为;
当前固定返回 100.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 100.0(未完整实现);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'PVI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
pvi = 100.0
_save_indicator(code, 'PVI', period, date, json.dumps(pvi), use_adjusted)
return pvi
def get_nvi(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""负成交量指标 NVI(Negative Volume Index)
[存根函数] 理论上只在成交量缩小时更新累计价格变化,反映主力资金的悄然动向;
当前固定返回 100.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 100.0(未完整实现);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'NVI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
nvi = 100.0
_save_indicator(code, 'NVI', period, date, json.dumps(nvi), use_adjusted)
return nvi
def get_ppo(code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""价格震荡百分比指标 PPO(Percentage Price Oscillator)
(快EMA - 慢EMA) / 慢EMA * 100,是 MACD 的百分比版本,
消除了股价绝对值差异,便于不同价位股票横向比较。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fast: 快线EMA周期,默认12
slow: 慢线EMA周期,默认26
signal: 信号线周期,默认9(当前近似处理)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'ppo': PPO线(%), 'signal': 信号线(%), 'histogram': 柱状图};
数据不足或慢EMA为0时返回 None
"""
period_key = fast * 10000 + slow * 100 + signal
cached = _get_cached_indicator(code, 'PPO', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ema_fast = get_ema(code, date, fast, use_adjusted)
ema_slow = get_ema(code, date, slow, use_adjusted)
if ema_fast is None or ema_slow is None or ema_slow == 0:
return None
ppo_line = ((ema_fast - ema_slow) / ema_slow) * 100
ppo = {'ppo': ppo_line, 'signal': ppo_line, 'histogram': 0}
_save_indicator(code, 'PPO', period_key, date, json.dumps(ppo), use_adjusted)
return ppo
def get_roc_r(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""变动率比别名 ROC_R(等同于 ROCR)
直接委托给 get_rocr,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 今收盘 / N日前收盘的比值;数据不足时返回 None
"""
return get_rocr(code, date, period, use_adjusted)
def get_stoch(code: str, date: str, fastk_period: int = 14, slowk_period: int = 3, slowd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""慢速随机指标 STOCH(Stochastic Oscillator)
基于 fastk_period 日高低价范围计算快速K,再经平滑得到慢速K和D值。
slowk>80 超买,slowk<20 超卖,K线上穿D线为买入信号,下穿为卖出信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fastk_period: 快速K值计算的高低价范围周期,默认14
slowk_period: 慢速K的平滑周期,默认3(当前近似处理)
slowd_period: 慢速D的平滑周期,默认3(当前近似处理)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'slowk': 慢速K值, 'slowd': 慢速D值}(0-100);数据不足时返回 None
"""
period_key = fastk_period * 10000 + slowk_period * 100 + slowd_period
cached = _get_cached_indicator(code, 'STOCH', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, fastk_period)
if len(klines) < fastk_period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
low_n = min(k.low for k in klines)
high_n = max(k.high for k in klines)
if high_n - low_n == 0:
fastk = 50.0
else:
fastk = ((klines[-1].close - low_n) / (high_n - low_n)) * 100
stoch = {'slowk': fastk, 'slowd': fastk}
_save_indicator(code, 'STOCH', period_key, date, json.dumps(stoch), use_adjusted)
return stoch
def get_stochf(code: str, date: str, fastk_period: int = 14, fastd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""快速随机指标 STOCHF(Fast Stochastic Oscillator)
[存根函数] 与 STOCH 类似但不做慢速平滑,反应更灵敏;
当前固定返回 {'fastk': 50.0, 'fastd': 50.0}。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fastk_period: 快速K值计算周期,默认14
fastd_period: 快速D的平滑周期,默认3
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'fastk': 快速K值, 'fastd': 快速D值};当前固定返回各50.0(未完整实现)
"""
period_key = fastk_period * 100 + fastd_period
cached = _get_cached_indicator(code, 'STOCHF', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
stochf = {'fastk': 50.0, 'fastd': 50.0}
_save_indicator(code, 'STOCHF', period_key, date, json.dumps(stochf), use_adjusted)
return stochf
def get_stochrsi(code: str, date: str, rsi_period: int = 14, stoch_period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""随机RSI STOCHRSI(Stochastic RSI)
[存根函数] 将 RSI 值再经随机指标公式处理,对超买超卖更敏感;
当前固定返回 {'fastk': 50.0, 'fastd': 50.0}。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
rsi_period: RSI计算周期,默认14
stoch_period: STOCH计算周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'fastk': K值, 'fastd': D值};当前固定返回各50.0(未完整实现)
"""
period_key = rsi_period * 100 + stoch_period
cached = _get_cached_indicator(code, 'STOCHRSI', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
stochrsi = {'fastk': 50.0, 'fastd': 50.0}
_save_indicator(code, 'STOCHRSI', period_key, date, json.dumps(stochrsi), use_adjusted)
return stochrsi
def get_trange(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""真实波幅别名 TRANGE(等同于 TR)
直接委托给 get_tr,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日TR值(元);数据不足时返回 None
"""
return get_tr(code, date, use_adjusted)
# ============================================================
# 第五梯队:通道和其他指标
# ============================================================
def get_ma_channel(code: str, date: str, period: int = 20, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""移动平均通道 MA_CHANNEL
以 SMA 为中轨,上下各扩展 multiplier 倍 ATR 作为上下轨,形成动态通道。
价格突破上轨可能是超强趋势信号,跌破下轨可能是弱势信号,通道宽度随波动率变化。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: SMA和ATR的计算周期,默认20
multiplier: ATR倍数,控制通道宽窄,默认2.0
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨(SMA), 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MA_CHANNEL', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
sma = get_sma(code, date, period, use_adjusted)
atr = get_atr(code, date, period, use_adjusted)
if sma is None or atr is None:
return None
channel = {
'upper': sma + multiplier * atr,
'middle': sma,
'lower': sma - multiplier * atr
}
_save_indicator(code, 'MA_CHANNEL', period, date, json.dumps(channel), use_adjusted)
return channel
def get_donchian(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""唐奇安通道 DONCHIAN(Donchian Channel)
上轨 = 过去 period 日最高价,下轨 = 过去 period 日最低价,中轨 = (上+下)/2。
价格突破上轨为买入信号,突破下轨为卖出信号(海龟交易系统的核心)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 通道计算周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨, 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DONCHIAN', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
upper = max(k.high for k in klines)
lower = min(k.low for k in klines)
middle = (upper + lower) / 2
donchian = {'upper': upper, 'middle': middle, 'lower': lower}
_save_indicator(code, 'DONCHIAN', period, date, json.dumps(donchian), use_adjusted)
return donchian
def get_keltner(code: str, date: str, ma_period: int = 20, atr_period: int = 10, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""凯尔特纳通道 KELTNER(Keltner Channel)
以 EMA 为中轨,上下各扩展 multiplier 倍 ATR。
布林带在内、凯特纳在外时称"挤压"(Squeeze),是大行情前兆的判断依据之一。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
ma_period: EMA计算周期,默认20
atr_period: ATR计算周期,默认10
multiplier: ATR倍数,控制通道宽窄,默认2.0
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨(EMA), 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
period_key = ma_period * 10000 + atr_period * 100 + int(multiplier * 10)
cached = _get_cached_indicator(code, 'KELTNER', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ema = get_ema(code, date, ma_period, use_adjusted)
atr = get_atr(code, date, atr_period, use_adjusted)
if ema is None or atr is None:
return None
keltner = {
'upper': ema + multiplier * atr,
'middle': ema,
'lower': ema - multiplier * atr
}
_save_indicator(code, 'KELTNER', period_key, date, json.dumps(keltner), use_adjusted)
return keltner
def get_bbands_width(code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""布林带宽度 BBANDS_WIDTH(%)
(上轨 - 下轨) / 中轨 * 100,衡量布林带的相对宽窄程度(标准化后的带宽)。
带宽极度收窄(挤压)通常预示即将发生大行情;带宽扩大表示行情波动加剧。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 布林带周期,默认20
std_dev: 标准差倍数,默认2
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日布林带宽度(%);数据不足或中轨为0时返回 None
"""
period_key = period * 10 + std_dev
cached = _get_cached_indicator(code, 'BBANDS_WIDTH', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
bb = get_bollinger_bands(code, date, period, std_dev, use_adjusted)
if bb is None or bb['middle'] == 0:
return None
width = ((bb['upper'] - bb['lower']) / bb['middle']) * 100
_save_indicator(code, 'BBANDS_WIDTH', period_key, date, json.dumps(width), use_adjusted)
return width
def get_bbands_pct(code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""布林带百分比位置 BBANDS_PCT(Bollinger Bands %B)
(今收 - 下轨) / (上轨 - 下轨),衡量价格在布林带中的相对位置。
=1.0 表示触及上轨(超买),=0.0 触及下轨(超卖),=0.5 在中轨;极端时可超出[0,1]范围。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 布林带周期,默认20
std_dev: 标准差倍数,默认2
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日%B值(通常0-1,极端时可超出范围);数据不足时返回 None
"""
period_key = period * 10 + std_dev
cached = _get_cached_indicator(code, 'BBANDS_PCT', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
bb = get_bollinger_bands(code, date, period, std_dev, use_adjusted)
klines = _get_klines_before_date(code, date, 1)
if bb is None or len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if bb['upper'] - bb['lower'] == 0:
pct = 0.5
else:
pct = (klines[-1].close - bb['lower']) / (bb['upper'] - bb['lower'])
_save_indicator(code, 'BBANDS_PCT', period_key, date, json.dumps(pct), use_adjusted)
return pct
# ============================================================
# 第六梯队:其他指标
# ============================================================
def get_linearreg(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归预测值 LINEARREG
对过去 period 日收盘价做最小二乘线性回归,返回回归线在最后一天的预测值。
可理解为去噪后的"理论收盘价",与实际价格的偏差反映超买超卖程度。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日线性回归预测价(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
intercept = mean_y - slope * mean_x
linearreg = intercept + slope * (period - 1)
_save_indicator(code, 'LINEARREG', period, date, json.dumps(linearreg), use_adjusted)
return linearreg
def get_linearreg_angle(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归角度 LINEARREG_ANGLE(度)
对过去 period 日收盘价做线性回归,将斜率转换为角度(arctan)。
正角度表示上升趋势,负角度表示下降趋势,角度绝对值越大趋势越陡峭。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日回归线角度(度,-90到90);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG_ANGLE', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
angle = math.degrees(math.atan(slope))
_save_indicator(code, 'LINEARREG_ANGLE', period, date, json.dumps(angle), use_adjusted)
return angle
def get_linearreg_intercept(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归截距 LINEARREG_INTERCEPT(元)
过去 period 日收盘价线性回归直线的Y轴截距(x=0时的理论价格)。
通常配合 LINEARREG_SLOPE 和 LINEARREG 一起使用,单独使用意义不大。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 线性回归截距值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG_INTERCEPT', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
intercept = mean_y - slope * mean_x
_save_indicator(code, 'LINEARREG_INTERCEPT', period, date, json.dumps(intercept), use_adjusted)
return intercept
def get_linearreg_slope(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归斜率 LINEARREG_SLOPE(元/天)
过去 period 日收盘价线性回归直线的斜率(每交易日平均涨跌幅)。
正值表示上升趋势,负值表示下降趋势,绝对值越大趋势越强劲。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 线性回归斜率(元/天);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG_SLOPE', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
_save_indicator(code, 'LINEARREG_SLOPE', period, date, json.dumps(slope), use_adjusted)
return slope
def get_stddev(code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""标准差 STDDEV(Standard Deviation)
过去 period 日收盘价的总体标准差,乘以 nbdev 倍数。
衡量价格的离散程度,是布林带计算的基础;nbdev=1为原始标准差,=2则与布林带2σ对应。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 计算周期,默认20
nbdev: 标准差倍数,默认1
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日标准差值(元);数据不足时返回 None
"""
period_key = period * 10 + nbdev
cached = _get_cached_indicator(code, 'STDDEV', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
mean = sum(prices) / period
variance = sum((p - mean) ** 2 for p in prices) / period
stddev = (variance ** 0.5) * nbdev
_save_indicator(code, 'STDDEV', period_key, date, json.dumps(stddev), use_adjusted)
return stddev
def get_tsf(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""时间序列预测别名 TSF(Time Series Forecast,等同于 LINEARREG)
直接委托给 get_linearreg,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日线性回归预测价(元);数据不足时返回 None
"""
return get_linearreg(code, date, period, use_adjusted)
def get_var(code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""方差 VAR(Variance)
过去 period 日收盘价的总体方差,乘以 nbdev² 倍数。
方差 = 标准差²,是 STDDEV 的平方,衡量价格离散程度;与 STDDEV 配合使用。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 计算周期,默认20
nbdev: 倍数,默认1(实际方差再乘以nbdev²)
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日方差值(元²);数据不足时返回 None
"""
period_key = period * 10 + nbdev
cached = _get_cached_indicator(code, 'VAR', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
mean = sum(prices) / period
variance = sum((p - mean) ** 2 for p in prices) / period
var = variance * nbdev * nbdev
_save_indicator(code, 'VAR', period_key, date, json.dumps(var), use_adjusted)
return var
def get_correl(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""相关系数 CORREL(Pearson Correlation Coefficient)
[存根函数] 理论上计算两个价格序列的皮尔逊相关系数(-1到1);
当前仅支持单只股票(与自身序列相关,结果恒为1.0)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 1.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'CORREL', period, date, use_adjusted)
if cached is not None:
return float(cached)
correl = 1.0
_save_indicator(code, 'CORREL', period, date, json.dumps(correl), use_adjusted)
return correl
def get_beta(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""贝塔系数 BETA
[存根函数] 理论上衡量个股相对市场指数的系统性风险(>1波动大于市场,<1反之);
需要基准指数数据,当前仅支持单只股票(与自身比较,固定返回1.0)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 1.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'BETA', period, date, use_adjusted)
if cached is not None:
return float(cached)
beta = 1.0
_save_indicator(code, 'BETA', period, date, json.dumps(beta), use_adjusted)
return beta
def get_ht_dcperiod(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""希尔伯特变换-主导周期 HT_DCPERIOD
[存根函数] 通过希尔伯特变换检测价格序列当前的主导振荡周期(天数),
用于自适应指标的动态周期参数;当前固定返回 10.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 10.0(未完整实现),单位:天
"""
cached = _get_cached_indicator(code, 'HT_DCPERIOD', 1, date, use_adjusted)
if cached is not None:
return float(cached)
ht_dcperiod = 10.0
_save_indicator(code, 'HT_DCPERIOD', 1, date, json.dumps(ht_dcperiod), use_adjusted)
return ht_dcperiod
def get_ht_dcphase(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""希尔伯特变换-主导相位 HT_DCPHASE
[存根函数] 当前价格在主导周期中所处的相位角(度),
配合 HT_DCPERIOD 使用,可判断周期性行情的位置;当前固定返回 0.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 0.0(未完整实现),单位:度
"""
cached = _get_cached_indicator(code, 'HT_DCPHASE', 1, date, use_adjusted)
if cached is not None:
return float(cached)
ht_dcphase = 0.0
_save_indicator(code, 'HT_DCPHASE', 1, date, json.dumps(ht_dcphase), use_adjusted)
return ht_dcphase
def get_ht_phasor(code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""希尔伯特变换-相位分量 HT_PHASOR
[存根函数] 将价格信号分解为同相(InPhase)和正交(Quadrature)两个正交分量,
用于计算相位和主导周期;当前固定返回零值。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'inphase': 同相分量, 'quadrature': 正交分量};当前固定返回各0.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'HT_PHASOR', 1, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ht_phasor = {'inphase': 0.0, 'quadrature': 0.0}
_save_indicator(code, 'HT_PHASOR', 1, date, json.dumps(ht_phasor), use_adjusted)
return ht_phasor
def get_ht_sine(code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""希尔伯特变换-正弦波 HT_SINE
[存根函数] 基于希尔伯特变换输出正弦波和超前正弦波(领先约45度),
两线交叉预示周期性趋势转折;当前固定返回零值。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'sine': 正弦值, 'leadsine': 超前正弦值};当前固定返回各0.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'HT_SINE', 1, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ht_sine = {'sine': 0.0, 'leadsine': 0.0}
_save_indicator(code, 'HT_SINE', 1, date, json.dumps(ht_sine), use_adjusted)
return ht_sine
def get_ht_trendmode(code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""希尔伯特变换-趋势模式 HT_TRENDMODE
[存根函数] 判断当前市场处于趋势行情(1)还是周期性震荡行情(0),
用于切换使用趋势类或震荡类指标;当前固定返回1(趋势模式)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
int: 1=趋势行情,0=周期性震荡;当前固定返回 1(未完整实现)
"""
cached = _get_cached_indicator(code, 'HT_TRENDMODE', 1, date, use_adjusted)
if cached is not None:
return int(float(cached))
ht_trendmode = 1
_save_indicator(code, 'HT_TRENDMODE', 1, date, json.dumps(ht_trendmode), use_adjusted)
return ht_trendmode
# ============================================================
# 辅助指标
# ============================================================
def get_typical_price(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""典型价格 TP(Typical Price)
当日 (最高价 + 最低价 + 收盘价) / 3,是 MFI、CCI、VWAP 等指标的基础计算单元,
比单纯收盘价更能代表当日的价格重心。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日典型价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'TYPICAL', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
tp = (klines[-1].high + klines[-1].low + klines[-1].close) / 3
_save_indicator(code, 'TYPICAL', 1, date, json.dumps(tp), use_adjusted)
return tp
def get_median_price(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""中位数价格 MEDPRICE(Median Price)
当日 (最高价 + 最低价) / 2,代表当日价格区间的中心点,
不考虑收盘价位置,适合作为对称通道的基准价格。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日中位数价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MEDIAN', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
mp = (klines[-1].high + klines[-1].low) / 2
_save_indicator(code, 'MEDIAN', 1, date, json.dumps(mp), use_adjusted)
return mp
def get_weighted_close(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""加权收盘价 WCL(Weighted Close Price)
当日 (最高价 + 最低价 + 2 * 收盘价) / 4,给收盘价赋予双倍权重,
比典型价格更强调收盘价的重要性,反映市场对当日收盘位置的认可。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日加权收盘价(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'WCL', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
wcl = (klines[-1].high + klines[-1].low + 2 * klines[-1].close) / 4
_save_indicator(code, 'WCL', 1, date, json.dumps(wcl), use_adjusted)
return wcl
def get_avgp(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""平均价格 AVGP(Average Price)
当日 (开盘价 + 最高价 + 最低价 + 收盘价) / 4,四价平均,
综合反映当日全程的价格水平,可用于判断当日多空博弈的均衡点。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日四价平均价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'AVGP', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
avgp = (klines[-1].open + klines[-1].high + klines[-1].low + klines[-1].close) / 4
_save_indicator(code, 'AVGP', 1, date, json.dumps(avgp), use_adjusted)
return avgp
# ============================================================
# 新增指标
# ============================================================
def get_asi(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""振动升降指标 ASI(Accumulation Swing Index)
由 Wilder 提出,综合 open/high/low/close 计算每日摆动指数 SI,再累计求和。
ASI 上穿前高为强烈买入信号,下穿前低为卖出信号,常用于确认趋势突破的真实性。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 累计 SI 的 K 线根数,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 ASI 值;数据不足(< period+1 根)时返回 None
"""
cached = _get_cached_indicator(code, 'ASI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
asi = 0.0
for i in range(1, len(klines)):
cur = klines[i]
prev = klines[i - 1]
A = abs(cur.high - prev.close)
B = abs(cur.low - prev.close)
C = abs(cur.high - prev.low)
D = abs(prev.close - prev.open) if prev.open else 0.0
if A >= B and A >= C:
R = A - 0.5 * B + 0.25 * D
elif B >= A and B >= C:
R = B - 0.5 * A + 0.25 * D
else:
R = C + 0.25 * D
X = (cur.close - prev.close) + 0.5 * (cur.close - cur.open) + 0.25 * (prev.close - prev.open if prev.open else 0.0)
si = (50.0 * X / R) if R != 0 else 0.0
asi += si
_save_indicator(code, 'ASI', period, date, json.dumps(asi), use_adjusted)
return asi
def get_vr(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""成交量变异率 VR(Volume Ratio)
将过去 period 日的成交量按涨/跌/平分类累加,计算多空力量之比。
VR = (上涨日成交量 + 平盘日成交量/2) / (下跌日成交量 + 平盘日成交量/2) * 100
VR 在 70~150 为盘整区,>250 超买,<70 超卖,<40 极度超卖(可能反弹)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 VR 值;数据不足或分母为 0 时返回 None
"""
cached = _get_cached_indicator(code, 'VR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
avs = bvs = cvs = 0.0
for i in range(1, len(klines)):
vol = klines[i].volume or 0.0
if klines[i].close > klines[i - 1].close:
avs += vol
elif klines[i].close < klines[i - 1].close:
bvs += vol
else:
cvs += vol
denominator = bvs + cvs / 2.0
if denominator == 0:
return None
vr = (avs + cvs / 2.0) / denominator * 100.0
_save_indicator(code, 'VR', period, date, json.dumps(vr), use_adjusted)
return vr
def get_ar(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""AR 人气指标(Atmosphere Ratio)
衡量当前市场人气,反映多空双方争夺的激烈程度。
AR = sum(High - Open) / sum(Open - Low) * 100,值越高说明买方越强势。
AR > 180 为超买区,AR < 40 为超卖区,一般在 80~120 间震荡。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 AR 值;数据不足或分母为 0 时返回 None
"""
cached = _get_cached_indicator(code, 'AR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sum_ho = sum(k.high - k.open for k in klines)
sum_ol = sum(k.open - k.low for k in klines)
if sum_ol == 0:
return None
ar = sum_ho / sum_ol * 100.0
_save_indicator(code, 'AR', period, date, json.dumps(ar), use_adjusted)
return ar
def get_br(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""BR 意愿指标(Willingness Ratio)
衡量市场买卖意愿,以前收盘价为参考基准区分主动多空力量。
BR = sum(max(0, High - prevClose)) / sum(max(0, prevClose - Low)) * 100
BR > 400 超买,BR < 40 超卖。与 AR 配合使用可判断主力意图。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 BR 值;数据不足或分母为 0 时返回 None
"""
cached = _get_cached_indicator(code, 'BR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sum_hpc = sum_pcl = 0.0
for i in range(1, len(klines)):
pc = klines[i - 1].close
sum_hpc += max(0.0, klines[i].high - pc)
sum_pcl += max(0.0, pc - klines[i].low)
if sum_pcl == 0:
return None
br = sum_hpc / sum_pcl * 100.0
_save_indicator(code, 'BR', period, date, json.dumps(br), use_adjusted)
return br
def get_brar(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""BRAR 情绪指标(BR + AR 组合)
同时返回 AR(人气指标)和 BR(意愿指标),综合衡量市场情绪。
AR 反映多空争夺强度,BR 反映主力买卖意愿;两者配合判断超买超卖。
AR/BR 同时超买 → 市场过热;AR/BR 同时超卖 → 可能见底。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
dict: {'ar': AR值, 'br': BR值};任一指标无法计算时返回 None
"""
cached = _get_cached_indicator(code, 'BRAR', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ar = get_ar(code, date, period, use_adjusted)
br = get_br(code, date, period, use_adjusted)
if ar is None or br is None:
return None
result = {'ar': ar, 'br': br}
_save_indicator(code, 'BRAR', period, date, json.dumps(result), use_adjusted)
return result
def get_dpo(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""区间震荡线 DPO(Detrended Price Oscillator)
通过去除价格中的趋势成分,突出周期性波动。
DPO = 今收盘 - SMA(close, N) 向前偏移 (N/2 + 1) 根K线
偏移后的 SMA 反映 N/2+1 天前的均价水平,DPO > 0 表示价格高于历史均值。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认 20
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 DPO 值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DPO', period, date, use_adjusted)
if cached is not None:
return float(cached)
shift = period // 2 + 1
needed = period + shift + 1
klines = _get_klines_before_date(code, date, needed)
if len(klines) < needed:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
# SMA 使用 shift 根前的那段 N 根 K 线
sma_klines = klines[-(period + shift):-shift]
sma_old = sum(k.close for k in sma_klines) / period
dpo = klines[-1].close - sma_old
_save_indicator(code, 'DPO', period, date, json.dumps(dpo), use_adjusted)
return dpo
def get_bbi(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""多空指标 BBI(Bull and Bear Index)
四条均线(3/6/12/24日)的简单平均,综合短中长期趋势。
BBI = (MA3 + MA6 + MA12 + MA24) / 4
价格上穿 BBI 为买入信号,下穿为卖出信号;比单一均线更平滑稳定。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 BBI 值(元);数据不足 24 根时返回 None
"""
cached = _get_cached_indicator(code, 'BBI', 24, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 24)
if len(klines) < 24:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
closes = [k.close for k in klines]
ma3 = sum(closes[-3:]) / 3
ma6 = sum(closes[-6:]) / 6
ma12 = sum(closes[-12:]) / 12
ma24 = sum(closes[-24:]) / 24
bbi = (ma3 + ma6 + ma12 + ma24) / 4.0
_save_indicator(code, 'BBI', 24, date, json.dumps(bbi), use_adjusted)
return bbi
def get_mass(code: str, date: str, period: int = 25, use_adjusted: bool = True) -> Optional[float]:
"""梅斯线 MASS(Mass Index)
通过计算高低价之差的两次 EMA 之比,并累加,识别价格反转信号。
EMA1 = EMA(High-Low, 9),EMA2 = EMA(EMA1, 9),MASS = sum(EMA1/EMA2, period)
MASS 超过 27 后回落至 26.5 以下,为"反转隆起",预示趋势反转。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 累加周期,默认 25
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 MASS 值;数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MASS', period, date, use_adjusted)
if cached is not None:
return float(cached)
ema_period = 9
needed = ema_period * 2 + period
klines = _get_klines_before_date(code, date, needed)
if len(klines) < needed:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
hl = [k.high - k.low for k in klines]
ema1 = _ema_series(hl, ema_period)
ema2 = _ema_series(ema1, ema_period)
ratios = [e1 / e2 if e2 != 0 else 1.0 for e1, e2 in zip(ema1, ema2)]
if len(ratios) < period:
return None
mass = sum(ratios[-period:])
_save_indicator(code, 'MASS', period, date, json.dumps(mass), use_adjusted)
return mass
def get_xue_channel(code: str, date: str, period: int = 20, pct: float = 3.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""薛斯通道(Xue's Channel)
以 SMA 为中轨,向上/下按固定百分比扩展形成通道,类似布林带但用固定比例替代标准差。
上轨 = SMA * (1 + pct/100),下轨 = SMA * (1 - pct/100)
价格触及上轨为短期超买,触及下轨为超卖,通道内震荡则看中轨支撑/压力。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: SMA 周期,默认 20
pct: 通道偏移百分比,默认 3.0(即 ±3%)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
dict: {'upper': 上轨, 'middle': 中轨, 'lower': 下轨}(元);数据不足时返回 None
"""
period_key = period * 1000 + int(pct * 10)
cached = _get_cached_indicator(code, 'XUE', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
middle = sum(k.close for k in klines) / period
upper = middle * (1.0 + pct / 100.0)
lower = middle * (1.0 - pct / 100.0)
result = {'upper': upper, 'middle': middle, 'lower': lower}
_save_indicator(code, 'XUE', period_key, date, json.dumps(result), use_adjusted)
return result
def get_consecutive_rise(code: str, date: str, max_days: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""连涨天数
从 date 向前数,连续收盘价高于前一日的天数。
使用复权价格,避免除权除息日的价格跳空被误判为下跌。
连涨天数过长(如 >7)往往预示短期超买,注意回调风险。
Args:
code: 股票代码,如 '000001.SZ'
date: 统计截止日期,格式 'YYYY-MM-DD'
max_days: 最多回溯天数,默认 60(防止数据量过大)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 连涨天数(0 表示当日未上涨);数据不足时返回 None
"""
klines = _get_klines_before_date(code, date, max_days + 1)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
count = 0
for i in range(len(klines) - 1, 0, -1):
if klines[i].close > klines[i - 1].close:
count += 1
else:
break
return count
def get_consecutive_fall(code: str, date: str, max_days: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""连跌天数
从 date 向前数,连续收盘价低于前一日的天数。
使用复权价格,避免除权除息日的价格跳空被误判为下跌。
连跌天数过长(如 >7)往往预示短期超卖,可能存在反弹机会。
Args:
code: 股票代码,如 '000001.SZ'
date: 统计截止日期,格式 'YYYY-MM-DD'
max_days: 最多回溯天数,默认 60(防止数据量过大)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 连跌天数(0 表示当日未下跌);数据不足时返回 None
"""
klines = _get_klines_before_date(code, date, max_days + 1)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
count = 0
for i in range(len(klines) - 1, 0, -1):
if klines[i].close < klines[i - 1].close:
count += 1
else:
break
return count
def get_bomb_board(code: str, date: str) -> Optional[int]:
"""炸板判断
判断指定日期该股票是否发生炸板:当日价格曾触及涨停价,但收盘时未封住(收盘价 < 涨停价)。
炸板往往意味着多头动能不足,短期筹码松动,可作为规避信号。
不使用复权价格——炸板依据的是市场实际涨停价,与复权无关。
数据来源:
- 有效交易日判断:stock_limit 表
- 炸板记录:daily_bomb_list 表(bomb_type='U',曾涨停炸板)
Args:
code: 股票代码,如 '000001.SZ'
date: 判断日期,格式 'YYYY-MM-DD'
Returns:
int: 1=当日炸板,0=当日未炸板;无数据(非交易日/数据缺失)返回 None
"""
cached = _get_cached_indicator(code, 'BOMB_BOARD', 0, date, False)
if cached is not None:
return int(cached)
# 用 stock_limit 确认是否为有效交易日
limits = query_stock_limit(ts_codes=[code], trade_date=date)
if not limits:
return None
bombs = query_daily_bomb_list(ts_codes=[code], trade_date=date, bomb_type='U')
result = 1 if bombs else 0
_save_indicator(code, 'BOMB_BOARD', 0, date, json.dumps(result), False)
return result
def get_bomb_board_count(code: str, date: str, period: int = 20) -> Optional[int]:
"""近N个交易日炸板次数
统计截至指定日期最近 period 个交易日内的炸板次数(含当日)。
炸板频繁说明股票多次冲板失败,多头信心不足,高频炸板个股追高风险较大。
不使用复权价格——炸板依据的是市场实际涨停价,与复权无关。
数据来源:daily_bomb_list 表(bomb_type='U')
Args:
code: 股票代码,如 '000001.SZ'
date: 统计截止日期,格式 'YYYY-MM-DD'
period: 回溯的交易日天数,默认 20
Returns:
int: 近 period 个交易日内炸板次数(0 表示无炸板);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'BOMB_BOARD_COUNT', period, date, False)
if cached is not None:
return int(cached)
klines = _get_klines_before_date(code, date, period)
if not klines:
return None
start_date = klines[0].date
bombs = query_daily_bomb_list(ts_codes=[code], start_date=start_date, end_date=date, bomb_type='U')
result = len(bombs)
_save_indicator(code, 'BOMB_BOARD_COUNT', period, date, json.dumps(result), False)
return result
def get_consecutive_limit_up(code: str, date: str) -> Optional[int]:
"""连续涨停天数(连板数)
返回截至指定日期连续收盘涨停的天数。直接读取 daily_limit_list.limit_streak 字段,
该字段由数据源维护,是官方连板计数,比自行计算更准确(含一字板等特殊情形)。
连板数可用于:
- 筛选高位连板股(>= 3 板往往已属强势)
- 衡量涨停板的持续性与封板质量
不使用复权价格——涨停判断基于市场实际价格。
数据来源:
- 有效交易日判断:stock_limit 表
- 连板数:daily_limit_list 表(limit_type='U',limit_streak 字段)
Args:
code: 股票代码,如 '000001.SZ'
date: 判断日期,格式 'YYYY-MM-DD'
Returns:
int: 连板数(0 表示当日未涨停);无数据(非交易日/数据缺失)返回 None
"""
cached = _get_cached_indicator(code, 'CONSEC_LIMIT_UP', 0, date, False)
if cached is not None:
return int(cached)
# 用 stock_limit 确认是否为有效交易日
limits = query_stock_limit(ts_codes=[code], trade_date=date)
if not limits:
return None
records = query_daily_limit_list(ts_codes=[code], trade_date=date, limit_type='U')
result = records[0].limit_streak if records else 0
_save_indicator(code, 'CONSEC_LIMIT_UP', 0, date, json.dumps(result), False)
return result
if __name__ == "__main__":
init_indicators_db()
print("指标数据库初始化完成")
print(f"已实现指标数量: 100+")
FILE:scripts/logger.py
def log(message:str):
print(f"[BitSoulStockLog]{message}")
FILE:scripts/metrics.py
"""
metrics.py - 性能指标计算模块
功能:
1. 回测性能指标计算
2. 风险指标计算
设计原则:
- 函数功能单一、最小粒度
- 基于权益曲线和交易记录计算
"""
from typing import List, Dict, Tuple
from track_logger import TrackLogger
def get_max_drawdown(equity_curve: List[float]) -> Tuple[float, int, int]:
"""计算最大回撤
遍历权益曲线,找出从最高峰到随后最低谷的最大下跌幅度(比例)。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
Tuple[float, int, int]:
- float: 最大回撤比例(0~1),如 0.2 表示回撤 20%
- int: 最低谷索引(回撤结束位置)
- int: 最高峰索引(回撤开始位置)
equity_curve 为空时返回 (0, 0, 0)
"""
if not equity_curve:
return 0, 0, 0
max_dd = 0
max_idx, min_idx = 0, 0
peak = equity_curve[0]
peak_idx = 0
for i, value in enumerate(equity_curve):
if value > peak:
peak, peak_idx = value, i
dd = (peak - value) / peak if peak > 0 else 0
if dd > max_dd:
max_dd, max_idx, min_idx = dd, i, peak_idx
return max_dd, max_idx, min_idx
def get_max_drawdown_pct(equity_curve: List[float]) -> float:
"""获取最大回撤比例
get_max_drawdown 的简化版本,只返回最大回撤比例。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
float: 最大回撤比例(0~1),equity_curve 为空时返回 0
"""
dd, _, _ = get_max_drawdown(equity_curve)
return dd
def get_annualized_return(total_return: float, days: int) -> float:
"""计算年化收益率
以 252 个交易日为一年,将持有期总收益折算为年化复利收益率。
公式:(1 + total_return) ^ (252 / days) - 1
Args:
total_return: 持有期总收益率(小数形式,如 0.5 表示 50%)
days: 实际持有交易日数
Returns:
float: 年化收益率(小数形式);days <= 0 时返回 0
"""
if days <= 0:
return 0
years = days / 252
if years <= 0:
return 0
return ((1 + total_return) ** (1 / years) - 1)
def get_total_return(initial_value: float, final_value: float) -> float:
"""计算总收益率
Args:
initial_value: 初始账户价值(元)
final_value: 最终账户价值(元)
Returns:
float: 总收益率(小数形式,如 0.3 表示盈利 30%);
initial_value <= 0 时返回 0
"""
if initial_value <= 0:
return 0
return (final_value - initial_value) / initial_value
def get_sharpe_ratio(equity_curve: List[float], risk_free_rate: float = 0.03) -> float:
"""计算夏普比率 (Sharpe Ratio)
衡量策略每承担一单位风险所获得的超额收益。
夏普比率越高越好,>1 为良好,>2 为优秀,<0 表示跑不赢无风险利率。
公式:(日均超额收益 / 日收益标准差) * sqrt(252)
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
risk_free_rate: 年化无风险利率(小数形式),默认 0.03(即 3%)
Returns:
float: 年化夏普比率;数据不足或波动率为 0 时返回 0
"""
if len(equity_curve) < 2:
return 0
returns = []
for i in range(1, len(equity_curve)):
if equity_curve[i-1] > 0:
ret = (equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
returns.append(ret)
if not returns:
return 0
avg_return = sum(returns) / len(returns)
variance = sum((r - avg_return) ** 2 for r in returns) / len(returns)
std_dev = variance ** 0.5
if std_dev == 0:
return 0
daily_rf = risk_free_rate / 252
sharpe = (avg_return - daily_rf) / std_dev * (252 ** 0.5)
return sharpe
def get_win_rate(trades: List[Dict]) -> float:
"""计算胜率
盈利交易笔数占总交易笔数的百分比。
Args:
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
Returns:
float: 胜率百分比(0~100);trades 为空时返回 0
"""
if not trades:
return 0
wins = sum(1 for t in trades if t.get('profit', 0) > 0)
return (wins / len(trades)) * 100
def get_profit_loss_ratio(trades: List[Dict]) -> float:
"""计算盈亏比
平均每笔盈利 / 平均每笔亏损,反映策略的风险回报结构。
盈亏比 > 1 表示平均盈利大于平均亏损,结合胜率共同衡量策略质量。
Args:
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
Returns:
float: 盈亏比;无亏损交易时若有盈利返回 inf,否则返回 0
"""
profits = [t['profit'] for t in trades if t.get('profit', 0) > 0]
losses = [abs(t['profit']) for t in trades if t.get('profit', 0) < 0]
avg_profit = sum(profits) / len(profits) if profits else 0
avg_loss = sum(losses) / len(losses) if losses else 0
if avg_loss == 0:
return float('inf') if avg_profit > 0 else 0
return avg_profit / avg_loss
def get_calmar_ratio(equity_curve: List[float], days: int) -> float:
"""计算卡尔玛比率 (Calmar Ratio)
年化收益率与最大回撤之比,衡量每单位最大亏损所对应的年化收益。
卡尔玛比率越高越好,>3 为优秀。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
days: 实际持有交易日数
Returns:
float: 卡尔玛比率;equity_curve 不足 2 条或最大回撤为 0 时返回 0
"""
if len(equity_curve) < 2:
return 0
total_return = get_total_return(equity_curve[0], equity_curve[-1])
annualized = get_annualized_return(total_return, days)
max_dd = get_max_drawdown_pct(equity_curve)
if max_dd == 0:
return 0
return annualized / max_dd
def get_volatility(equity_curve: List[float]) -> float:
"""计算年化波动率
日收益率的标准差乘以 sqrt(252),反映策略收益的波动程度。
波动率越低、夏普比率越高,策略稳定性越好。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
float: 年化波动率(小数形式,如 0.2 表示 20%);数据不足时返回 0
"""
if len(equity_curve) < 2:
return 0
returns = []
for i in range(1, len(equity_curve)):
if equity_curve[i-1] > 0:
ret = (equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
returns.append(ret)
if not returns:
return 0
variance = sum((r - sum(returns)/len(returns)) ** 2 for r in returns) / len(returns)
daily_vol = variance ** 0.5
return daily_vol * (252 ** 0.5)
def get_trade_stats(trades: List[Dict]) -> Dict:
"""统计交易明细汇总
Args:
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
Returns:
Dict: 包含以下字段:
- total_trades (int): 总交易笔数
- wins (int): 盈利笔数
- losses (int): 亏损笔数
- win_rate (float): 胜率百分比(0~100)
- profit_loss_ratio (float): 盈亏比
- total_profit (float): 所有盈利交易的盈利总额(元)
- total_loss (float): 所有亏损交易的亏损总额(元,负数)
- avg_profit (float): 平均每笔盈利(元)
- avg_loss (float): 平均每笔亏损(元,负数)
trades 为空时各字段均返回 0
"""
if not trades:
return {
'total_trades': 0, 'wins': 0, 'losses': 0, 'win_rate': 0,
'profit_loss_ratio': 0, 'total_profit': 0, 'total_loss': 0,
'avg_profit': 0, 'avg_loss': 0,
}
wins = [t for t in trades if t.get('profit', 0) > 0]
losses = [t for t in trades if t.get('profit', 0) < 0]
return {
'total_trades': len(trades),
'wins': len(wins),
'losses': len(losses),
'win_rate': get_win_rate(trades),
'profit_loss_ratio': get_profit_loss_ratio(trades),
'total_profit': sum(t['profit'] for t in wins),
'total_loss': sum(t['profit'] for t in losses),
'avg_profit': sum(t['profit'] for t in wins) / len(wins) if wins else 0,
'avg_loss': sum(t['profit'] for t in losses) / len(losses) if losses else 0,
}
def generate_report(equity_curve: List[float], trades: List[Dict], initial_cash: float, days: int, track_logger:TrackLogger) -> Dict:
"""生成完整的回测绩效报告
汇总权益曲线和交易记录,一次性计算所有常用绩效指标。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
initial_cash: 初始资金(元)
days: 回测持有的实际交易日数
Returns:
Dict: 包含以下字段:
- initial_cash (float): 初始资金(元)
- final_value (float): 最终账户价值(元)
- total_return (float): 总收益率(小数)
- total_return_pct (float): 总收益率百分比
- annualized_return (float): 年化收益率(小数)
- annualized_return_pct (float): 年化收益率百分比
- max_drawdown (float): 最大回撤(小数)
- max_drawdown_pct (float): 最大回撤百分比
- sharpe_ratio (float): 夏普比率
- calmar_ratio (float): 卡尔玛比率
- volatility (float): 年化波动率(小数)
- trading_days (int): 交易日数
- trade_stats (Dict): 交易统计汇总,见 get_trade_stats 返回说明
"""
final_value = equity_curve[-1] if equity_curve else initial_cash
total_return = get_total_return(initial_cash, final_value)
return {
'initial_cash': initial_cash,
'final_value': final_value,
'total_return': total_return,
'total_return_pct': total_return * 100,
'annualized_return': get_annualized_return(total_return, days),
'annualized_return_pct': get_annualized_return(total_return, days) * 100,
'max_drawdown': get_max_drawdown_pct(equity_curve),
'max_drawdown_pct': get_max_drawdown_pct(equity_curve) * 100,
'sharpe_ratio': get_sharpe_ratio(equity_curve),
'calmar_ratio': get_calmar_ratio(equity_curve, days),
'volatility': get_volatility(equity_curve),
'trading_days': days,
'trade_stats': get_trade_stats(trades),
}
FILE:scripts/moe_signal.py
"""
moe_signal.py — MoE 混合专家买卖时机分析 + 遗传算法权重训练
功能一:分析单只股票当前买卖时机
python moe_signal.py --code 000001.SZ [--date 2026-03-01]
python moe_signal.py analyze --code 000001.SZ
功能二:跑回测训练最优权重(遗传算法,目标:最大化总收益)
python moe_signal.py train --start-date 2025-09-01 --end-date 2026-03-01
python moe_signal.py train # 默认最近半年
输出 analyze:JSON 格式的综合评分和 BUY/SELL/HOLD 建议
输出 train:优化后的权重写入 moe_weights.json
"""
import sys
import os
import json
import argparse
import random
import copy
from datetime import datetime, timedelta
from typing import Optional, Dict, List, Any, Tuple
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import numpy as np
import pandas as pd
from data_fetcher import (
query_daily_basic, query_stock_limit, query_top_list,
query_daily_kline, query_stock_basic,
)
import indicators as ind
from indicators import init_indicators_db
import config
# ─────────────────────────────────────────────────────────────────────────────
# 权重配置文件
# ─────────────────────────────────────────────────────────────────────────────
_WEIGHTS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'moe_weights.json')
_DEFAULT_WEIGHTS: Dict[str, Any] = {
'expert_weights': {'technical': 0.35, 'alpha': 0.35, 'fundamental': 0.15, 'behavior': 0.15},
'signal_thresholds': {'buy': 0.65, 'sell': 0.35},
}
def load_weights() -> Dict[str, Any]:
"""加载权重配置文件,文件不存在则返回默认值。"""
if os.path.exists(_WEIGHTS_PATH):
try:
with open(_WEIGHTS_PATH, 'r', encoding='utf-8') as f:
data = json.load(f)
for k, v in _DEFAULT_WEIGHTS.items():
if k not in data:
data[k] = copy.deepcopy(v)
return data
except Exception:
pass
return copy.deepcopy(_DEFAULT_WEIGHTS)
def save_weights(weights: Dict[str, Any], train_period: Optional[str] = None) -> None:
"""保存权重配置文件。"""
weights['_version'] = weights.get('_version', 1)
weights['_trained_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if train_period:
weights['_train_period'] = train_period
with open(_WEIGHTS_PATH, 'w', encoding='utf-8') as f:
json.dump(weights, f, ensure_ascii=False, indent=2)
print(f'[MoE] 权重已保存到 {_WEIGHTS_PATH}')
# ─────────────────────────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────────────────────────
def _clamp(v: float, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, v))
def _linear(v: float, lo: float, hi: float, reverse: bool = False) -> float:
"""将 v 线性映射到 [0,1],lo→1(看多), hi→0(看空);reverse=True 时反转。"""
if hi == lo:
return 0.5
ratio = (v - lo) / (hi - lo)
score = _clamp(1.0 - ratio)
return score if not reverse else 1.0 - score
def _get_close(code: str, date: str) -> Optional[float]:
"""获取指定日期或之前最近一个交易日的收盘价。"""
klines = query_daily_kline(codes=[code], end_date=date, limit=1, order_by="date DESC")
if klines:
return klines[0].close
return None
def _prev_date(date: str, n: int = 1) -> str:
"""往前推 n 个自然日(近似交易日偏移)。"""
return (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=n * 2)).strftime('%Y-%m-%d')
def _weighted_mean(scores: Dict[str, float], weights: Dict[str, float]) -> float:
"""按权重计算加权平均,权重自动归一化。"""
total_w = 0.0
total_v = 0.0
for k, v in scores.items():
w = weights.get(k, 1.0)
total_w += w
total_v += w * v
return (total_v / total_w) if total_w > 0 else 0.5
# ─────────────────────────────────────────────────────────────────────────────
# Expert 1:技术指标专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_tech(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Dict[str, Any]:
close = _get_close(code, date)
scores: Dict[str, float] = {}
# ── 趋势均线类 ────────────────────────────────────────────────────────────
for period, fname in [(5, 'sma5'), (10, 'sma10'), (20, 'sma20'), (60, 'sma60')]:
v = ind.get_sma(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
for period, fname in [(5, 'ema5'), (12, 'ema12'), (20, 'ema20'), (26, 'ema26')]:
v = ind.get_ema(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
ema5 = ind.get_ema(code, date, 5)
ema20 = ind.get_ema(code, date, 20)
if ema5 is not None and ema20 is not None:
scores['ema_cross'] = 1.0 if ema5 > ema20 else 0.0
for period, fname in [(20, 'wma20')]:
v = ind.get_wma(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
for period, fname in [(20, 'tema20')]:
v = ind.get_tema(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
for period, fname in [(20, 'dema20')]:
v = ind.get_dema(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
v = ind.get_kama(code, date, 10)
if v is not None and close is not None:
scores['kama'] = 1.0 if close > v else 0.0
v = ind.get_bbi(code, date)
if v is not None and close is not None:
scores['bbi'] = 1.0 if close > v else 0.0
v = ind.get_trix(code, date, 12)
if v is not None:
scores['trix'] = 1.0 if v > 0 else 0.0
dmi = ind.get_dmi(code, date, 14)
if dmi is not None:
adx = dmi.get('adx', 0) or 0
pdi = dmi.get('pdi', 0) or 0
mdi = dmi.get('mdi', 0) or 0
scores['dmi'] = 1.0 if (adx > 25 and pdi > mdi) else (0.0 if (adx > 25 and mdi > pdi) else 0.5)
sar_r = ind.get_sar(code, date)
if sar_r is not None and close is not None:
sar_v = sar_r.get('sar')
if sar_v is not None:
scores['sar'] = 1.0 if close > sar_v else 0.0
v = ind.get_linearreg_slope(code, date, 14)
if v is not None:
scores['linearreg_slope'] = 1.0 if v > 0 else 0.0
v = ind.get_linearreg(code, date, 14)
if v is not None and close is not None:
scores['linearreg'] = 1.0 if v > close else 0.0
v = ind.get_linearreg_angle(code, date, 14)
if v is not None:
scores['linearreg_angle'] = 1.0 if v > 0 else 0.0
v = ind.get_linearreg_intercept(code, date, 14)
if v is not None and close is not None:
scores['linearreg_intercept'] = 1.0 if close > v else 0.0
aroon = ind.get_aroon(code, date, 14)
if aroon is not None:
au = aroon.get('aroon_up', 50) or 50
ad = aroon.get('aroon_down', 50) or 50
scores['aroon'] = 1.0 if au > ad else (0.0 if ad > au else 0.5)
v = ind.get_tsf(code, date, 14)
if v is not None and close is not None:
scores['tsf'] = 1.0 if v > close else 0.0
v = ind.get_ht_trendmode(code, date)
if v is not None:
scores['ht_trendmode'] = 1.0 if v == 1 else 0.5
v = ind.get_ht_dcphase(code, date)
if v is not None:
scores['ht_dcphase'] = 1.0 if (0 <= v % 360 <= 180) else 0.0
ht_sine = ind.get_ht_sine(code, date)
if ht_sine is not None:
sine_v = ht_sine.get('sine', 0) or 0
scores['ht_sine'] = _clamp((sine_v + 1) / 2)
# ── 动量振荡类 ────────────────────────────────────────────────────────────
v = ind.get_rsi(code, date, 14)
if v is not None:
scores['rsi14'] = _linear(v, 70, 30)
v = ind.get_rsi(code, date, 6)
if v is not None:
scores['rsi6'] = _linear(v, 70, 30)
v = ind.get_cci(code, date, 20)
if v is not None:
scores['cci'] = _linear(v, 100, -100)
for period, fname in [(10, 'mom10'), (20, 'mom20')]:
v = ind.get_mom(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 0 else 0.0
for period, fname in [(10, 'roc10')]:
v = ind.get_roc(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 0 else 0.0
for period, fname in [(10, 'rocp10')]:
v = ind.get_rocp(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 0 else 0.0
for period, fname in [(10, 'rocr10')]:
v = ind.get_rocr(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 1.0 else 0.0
v = ind.get_roc_r(code, date, 10)
if v is not None:
scores['roc_r'] = 1.0 if v > 0 else 0.0
v = ind.get_williams_r(code, date, 14)
if v is not None:
scores['willr'] = _linear(v, -20, -80)
v = ind.get_cmo(code, date, 14)
if v is not None:
scores['cmo'] = 1.0 if v > 0 else 0.0
v = ind.get_bias(code, date, 20)
if v is not None:
if v < -10:
scores['bias'] = 1.0
elif v > 10:
scores['bias'] = 0.0
else:
scores['bias'] = _clamp(0.5 - v / 20)
v = ind.get_psycho(code, date, 12)
if v is not None:
scores['psycho'] = _clamp(1.0 - v / 100)
v = ind.get_dpo(code, date, 20)
if v is not None:
scores['dpo'] = 1.0 if v > 0 else 0.0
v = ind.get_mass(code, date, 25)
if v is not None:
scores['mass'] = 0.3 if v > 27 else 0.5
# ── KDJ / Stoch 类 ────────────────────────────────────────────────────────
kdj = ind.get_kdj(code, date)
if kdj is not None:
j = kdj.get('j', 50) or 50
scores['kdj_j'] = _linear(j, 80, 20)
k = kdj.get('k', 50) or 50
d = kdj.get('d', 50) or 50
scores['kdj_kd'] = 1.0 if k > d else 0.0
stoch = ind.get_stoch(code, date)
if stoch is not None:
sk = stoch.get('slowk', 50) or 50
scores['stoch_k'] = _linear(sk, 80, 20)
stochf = ind.get_stochf(code, date)
if stochf is not None:
fk = stochf.get('fastk', 50) or 50
scores['stochf_k'] = _linear(fk, 80, 20)
stochrsi = ind.get_stochrsi(code, date)
if stochrsi is not None:
sk = stochrsi.get('fastk', 50) or 50
scores['stochrsi'] = _linear(sk, 80, 20)
v = ind.get_ultosc(code, date)
if v is not None:
scores['ultosc'] = _linear(v, 70, 30)
# ── MACD 类 ───────────────────────────────────────────────────────────────
macd = ind.get_macd(code, date)
if macd is not None:
hist = macd.get('histogram', 0) or 0
scores['macd_hist'] = 1.0 if hist > 0 else 0.0
macd_v = macd.get('macd', 0) or 0
sig_v = macd.get('signal', 0) or 0
scores['macd_cross'] = 1.0 if macd_v > sig_v else 0.0
ppo = ind.get_ppo(code, date)
if ppo is not None:
hist = ppo.get('histogram', 0) or 0
scores['ppo_hist'] = 1.0 if hist > 0 else 0.0
v = ind.get_adosc(code, date)
if v is not None:
scores['adosc'] = 1.0 if v > 0 else 0.0
# ── 成交量类 ──────────────────────────────────────────────────────────────
v = ind.get_obv(code, date, 20)
if v is not None:
v_prev = ind.get_obv(code, _prev_date(date, 5), 20)
if v_prev is not None:
scores['obv'] = 1.0 if v > v_prev else 0.0
v = ind.get_ad(code, date, 20)
if v is not None:
v_prev = ind.get_ad(code, _prev_date(date, 5), 20)
if v_prev is not None:
scores['ad'] = 1.0 if v > v_prev else 0.0
v = ind.get_mfi(code, date, 14)
if v is not None:
scores['mfi'] = _linear(v, 80, 20)
v = ind.get_vwap(code, date, 20)
if v is not None and close is not None:
scores['vwap'] = 1.0 if close > v else 0.0
vol_d = ind.get_volume(code, date, 20)
if vol_d is not None:
ratio = vol_d.get('ratio', 1.0) or 1.0
scores['volume_ratio'] = 1.0 if ratio > 1.5 else (0.3 if ratio < 0.5 else 0.5)
v = ind.get_vr(code, date, 26)
if v is not None:
scores['vr'] = _linear(v, 180, 70)
v = ind.get_pvi(code, date, 20)
if v is not None:
pvi_ma = ind.get_sma(code, date, 20)
if pvi_ma is not None:
scores['pvi'] = 1.0 if v > pvi_ma else 0.0
v = ind.get_nvi(code, date, 20)
if v is not None:
nvi_ma = ind.get_sma(code, date, 20)
if nvi_ma is not None:
scores['nvi'] = 1.0 if v > nvi_ma else 0.0
v = ind.get_ar(code, date, 26)
if v is not None:
scores['ar'] = _linear(v, 150, 50)
v = ind.get_br(code, date, 26)
if v is not None:
scores['br'] = _linear(v, 200, 50)
brar = ind.get_brar(code, date, 26)
if brar is not None:
ar_v = brar.get('ar', 100) or 100
br_v = brar.get('br', 100) or 100
scores['brar'] = _clamp(((200 - ar_v) / 300 + (200 - br_v) / 400) / 2)
v = ind.get_asi(code, date, 26)
if v is not None:
scores['asi'] = 1.0 if v > 0 else 0.0
# ── 通道类 ────────────────────────────────────────────────────────────────
bb = ind.get_bollinger_bands(code, date, 20, 2)
if bb is not None and close is not None:
upper = bb.get('upper', close) or close
lower = bb.get('lower', close) or close
if upper != lower:
scores['bb_pos'] = _clamp((close - lower) / (upper - lower))
bb_score = _clamp((close - lower) / (upper - lower))
scores['bb_signal'] = 1.0 - bb_score if bb_score > 0.8 else (1.0 if bb_score < 0.2 else 0.5)
v = ind.get_bbands_pct(code, date, 20, 2)
if v is not None:
scores['bbands_pct'] = _linear(v, 1.0, 0.0)
v = ind.get_bbands_width(code, date, 20, 2)
if v is not None:
scores['bbands_width'] = 0.5
ma_ch = ind.get_ma_channel(code, date, 20, 2.0)
if ma_ch is not None and close is not None:
upper = ma_ch.get('upper', close) or close
lower = ma_ch.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['ma_channel'] = 1.0 if pos < 0.2 else (0.0 if pos > 0.8 else 0.5)
dc = ind.get_donchian(code, date, 20)
if dc is not None and close is not None:
upper = dc.get('upper', close) or close
lower = dc.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['donchian'] = 1.0 - pos
kelt = ind.get_keltner(code, date, 20, 10, 2.0)
if kelt is not None and close is not None:
upper = kelt.get('upper', close) or close
lower = kelt.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['keltner'] = 1.0 if pos < 0.2 else (0.0 if pos > 0.8 else 0.5)
xue = ind.get_xue_channel(code, date, 20, 3.0)
if xue is not None and close is not None:
upper = xue.get('upper', close) or close
lower = xue.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['xue_channel'] = 1.0 - pos
v = ind.get_midpoint(code, date, 14)
if v is not None and close is not None:
scores['midpoint'] = 1.0 if close > v else 0.0
v = ind.get_midprice(code, date, 14)
if v is not None and close is not None:
scores['midprice'] = 1.0 if close > v else 0.0
for key in ['atr', 'natr', 'tr', 'trange', 'stddev', 'var', 'correl', 'beta', 'ht_dcperiod']:
scores[key] = 0.5
for fname, fn in [('typical', ind.get_typical_price), ('median', ind.get_median_price),
('wclose', ind.get_weighted_close), ('avgp', ind.get_avgp)]:
v = fn(code, date)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
ht_ph = ind.get_ht_phasor(code, date)
if ht_ph is not None:
inphase = ht_ph.get('inphase', 0) or 0
scores['ht_phasor'] = 1.0 if inphase > 0 else 0.0
v = ind.get_consecutive_rise(code, date, 60)
if v is not None:
scores['consec_rise'] = _clamp(v / 10)
v = ind.get_consecutive_fall(code, date, 60)
if v is not None:
scores['consec_fall'] = _clamp(v / 5)
v = ind.get_bomb_board(code, date)
if v is not None:
scores['bomb_board'] = 0.0 if v > 0 else 0.5
v = ind.get_bomb_board_count(code, date, 20)
if v is not None:
scores['bomb_board_count'] = _clamp(1.0 - v * 0.2)
v = ind.get_consecutive_limit_up(code, date)
if v is not None:
scores['consec_limit_up'] = _clamp(v * 0.2)
if not scores:
return {'score': 0.5, 'valid_count': 0, 'total_count': 80, 'details': {}}
w = weights or {}
final = _weighted_mean(scores, w)
return {
'score': round(final, 4),
'valid_count': len(scores),
'total_count': 80,
'details': {k: round(v, 4) for k, v in list(scores.items())[:20]},
'_raw_scores': scores,
}
# ─────────────────────────────────────────────────────────────────────────────
# Expert 2:Alpha 因子专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_alpha(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Optional[Dict[str, Any]]:
try:
from formulaicAlphas.alpha101 import Alpha101
from formulaicAlphas.data_loader import AlphaDataLoader
except ImportError:
return None
end_date = date
start_date = (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=120)).strftime('%Y-%m-%d')
all_codes = [b.ts_code for b in query_stock_basic() if b.ts_code]
all_codes = random.sample(all_codes, min(200, len(all_codes)))
if code not in all_codes:
all_codes.insert(0, code)
loader = AlphaDataLoader()
data = loader.load(codes=all_codes, start_date=start_date, end_date=end_date)
if not data or 'close' not in data:
return None
close_df = data['close']
if code not in close_df.columns:
return None
target_date = pd.Timestamp(date)
available_dates = close_df.index[close_df.index <= target_date]
if len(available_dates) == 0:
return None
actual_date = available_dates[-1]
alpha_obj = Alpha101(data)
alpha_scores: Dict[str, float] = {}
for i in range(1, 102):
fname = f'alpha{i:03d}'
try:
fn = getattr(alpha_obj, fname, None)
if fn is None:
continue
result = fn()
if result is None or not isinstance(result, pd.DataFrame):
continue
if actual_date not in result.index or code not in result.columns:
continue
val = result.loc[actual_date, code]
if pd.isna(val):
continue
row = result.loc[actual_date].dropna()
if len(row) < 5:
continue
rank = float((row < val).sum()) / len(row)
alpha_scores[fname] = rank
except Exception:
continue
if not alpha_scores:
return None
w = weights or {}
final = _weighted_mean(alpha_scores, w)
return {
'score': round(final, 4),
'valid_count': len(alpha_scores),
'total_count': 101,
'details': {k: round(v, 4) for k, v in list(alpha_scores.items())[:10]},
'_raw_scores': alpha_scores,
}
# ─────────────────────────────────────────────────────────────────────────────
# Expert 3:基本面专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_fundamental(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Optional[Dict[str, Any]]:
basics = query_daily_basic(ts_codes=[code], end_date=date, limit=1, order_by="trade_date DESC")
if not basics:
return None
b = basics[0]
scores: Dict[str, float] = {}
pe = b.pe_ttm
if pe is not None:
if pe <= 0:
scores['pe_ttm'] = 0.1
elif pe < 30:
scores['pe_ttm'] = 1.0
elif pe < 60:
scores['pe_ttm'] = 0.5
else:
scores['pe_ttm'] = 0.2
pb = b.pb
if pb is not None and pb > 0:
if pb < 3:
scores['pb'] = 1.0
elif pb < 6:
scores['pb'] = 0.5
else:
scores['pb'] = 0.2
tr = b.turnover_rate
if tr is not None:
if 1 <= tr <= 5:
scores['turnover_rate'] = 1.0
elif 5 < tr <= 10:
scores['turnover_rate'] = 0.7
elif tr < 1:
scores['turnover_rate'] = 0.4
else:
scores['turnover_rate'] = 0.3
vr = b.volume_ratio
if vr is not None:
if 1 <= vr <= 2:
scores['volume_ratio'] = 1.0
elif 2 < vr <= 3:
scores['volume_ratio'] = 0.7
elif vr < 0.5:
scores['volume_ratio'] = 0.3
else:
scores['volume_ratio'] = 0.4
ps = b.ps_ttm
if ps is not None and ps > 0:
if ps < 1:
scores['ps_ttm'] = 1.0
elif ps < 5:
scores['ps_ttm'] = 0.7
elif ps < 10:
scores['ps_ttm'] = 0.5
else:
scores['ps_ttm'] = 0.3
if not scores:
return None
w = weights or {}
final = _weighted_mean(scores, w)
return {
'score': round(final, 4),
'details': {'pe_ttm': b.pe_ttm, 'pb': b.pb, 'turnover_rate': b.turnover_rate,
'volume_ratio': b.volume_ratio, 'ps_ttm': b.ps_ttm, 'scores': scores},
'_raw_scores': scores,
}
# ─────────────────────────────────────────────────────────────────────────────
# Expert 4:量价行为专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_behavior(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Optional[Dict[str, Any]]:
start_d = (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=30)).strftime('%Y-%m-%d')
details: Dict[str, Any] = {}
named_signals: Dict[str, float] = {}
limits = query_stock_limit(ts_codes=[code], start_date=start_d, end_date=date)
limit_up_5d = 0
limit_down_5d = 0
klines_5d = query_daily_kline(codes=[code], end_date=date, limit=5, order_by="date DESC")
dates_5d = {k.date for k in klines_5d}
for lim in limits:
if lim.trade_date in dates_5d:
if hasattr(lim, 'limit_up') and lim.limit_up:
limit_up_5d += 1
if hasattr(lim, 'limit_down') and lim.limit_down:
limit_down_5d += 1
details['limit_up_5d'] = limit_up_5d
details['limit_down_5d'] = limit_down_5d
named_signals['limit_score'] = _clamp(0.5 + limit_up_5d * 0.15 - limit_down_5d * 0.15)
consec_up = ind.get_consecutive_limit_up(code, date)
if consec_up is not None and consec_up > 0:
details['consecutive_limit_up'] = consec_up
named_signals['consecutive_limit_up'] = _clamp(consec_up * 0.2)
bomb_cnt = ind.get_bomb_board_count(code, date, 10)
if bomb_cnt is not None:
details['bomb_board_10d'] = bomb_cnt
named_signals['bomb_board'] = _clamp(1.0 - bomb_cnt * 0.15)
top_lists = query_top_list(ts_codes=[code], start_date=start_d, end_date=date)
if top_lists:
net_buy = sum((getattr(tl, 'net_buy', 0) or 0) for tl in top_lists)
details['top_list_net_buy'] = net_buy
named_signals['top_list'] = _clamp(0.5 + (0.3 if net_buy > 0 else (-0.3 if net_buy < 0 else 0)))
if klines_5d and len(klines_5d) >= 2:
latest_close = klines_5d[0].close
oldest_close = klines_5d[-1].pre_close if hasattr(klines_5d[-1], 'pre_close') else klines_5d[-1].close
if oldest_close and oldest_close > 0:
chg_5d = (latest_close - oldest_close) / oldest_close * 100
details['pct_chg_5d'] = round(chg_5d, 2)
named_signals['pct_chg_5d'] = _linear(chg_5d, 20, -20)
if not named_signals:
return None
w = weights or {}
final = _weighted_mean(named_signals, w)
return {
'score': round(final, 4),
'details': details,
'_raw_scores': named_signals,
}
# ─────────────────────────────────────────────────────────────────────────────
# 门控网络 + 汇总
# ─────────────────────────────────────────────────────────────────────────────
def _compute_final_score(
tech_result: Optional[Dict],
alpha_result: Optional[Dict],
fund_result: Optional[Dict],
behav_result: Optional[Dict],
expert_weights: Dict[str, float],
buy_thresh: float = 0.65,
sell_thresh: float = 0.35,
) -> Dict[str, Any]:
"""门控网络:根据有效专家和权重计算最终评分。"""
expert_results = {
'technical': tech_result,
'alpha': alpha_result,
'fundamental': fund_result,
'behavior': behav_result,
}
active = {k: v for k, v in expert_results.items() if v is not None}
if not active:
return {'error': '所有专家数据不足'}
total_w = sum(expert_weights.get(k, 0.0) for k in active)
if total_w <= 0:
total_w = len(active)
norm_weights = {k: 1.0 / total_w for k in active}
else:
norm_weights = {k: expert_weights.get(k, 0.0) / total_w for k in active}
final_score = sum(norm_weights[k] * active[k]['score'] for k in active)
final_score = round(float(final_score), 4)
signal = 'BUY' if final_score >= buy_thresh else ('SELL' if final_score <= sell_thresh else 'HOLD')
score_vals = [active[k]['score'] for k in active]
variance = float(np.var(score_vals)) if len(score_vals) > 1 else 0.0
confidence = '高' if variance < 0.005 else ('中' if variance < 0.020 else '低')
reasons = []
for k, v in active.items():
s = v['score']
label = {'technical': '技术面', 'alpha': 'Alpha因子', 'fundamental': '基本面', 'behavior': '量价行为'}[k]
if s >= buy_thresh:
reasons.append(f'{label}看多({s:.2f})')
elif s <= sell_thresh:
reasons.append(f'{label}看空({s:.2f})')
else:
reasons.append(f'{label}中性({s:.2f})')
experts_out = {}
for k in ['technical', 'alpha', 'fundamental', 'behavior']:
if k in active:
entry = {'score': active[k]['score'], 'weight': round(norm_weights[k], 4)}
if 'valid_count' in active[k]:
entry['valid_count'] = active[k]['valid_count']
entry['total_count'] = active[k]['total_count']
if 'details' in active[k]:
entry['details'] = active[k]['details']
experts_out[k] = entry
else:
experts_out[k] = {'score': None, 'weight': 0.0, 'note': '数据补充中,敬请期待'}
return {
'final_score': final_score,
'signal': signal,
'confidence': confidence,
'experts': experts_out,
'reason': ','.join(reasons),
}
def analyze(code: str, date: str) -> Dict[str, Any]:
"""
功能一:基于当前价格结合 MoE 做买卖决策。
从 moe_weights.json 加载权重(若不存在则使用默认值),
运行 4 个专家打分,门控网络汇总,输出 BUY/SELL/HOLD。
"""
print(f'[MoE] 正在分析 {code} 日期={date}')
# 退市检查
try:
basics = query_stock_basic(ts_codes=[code])
if basics:
b = basics[0]
if getattr(b, 'list_status', None) == 'D':
delist_date = getattr(b, 'delist_date', None) or '未知'
name = getattr(b, 'name', code)
print(f'[MoE] ⚠️ {code}({name})已于 {delist_date} 退市,以下分析仅供参考,该股票已无法交易。')
except Exception:
pass
weights = load_weights()
expert_w = weights.get('expert_weights', _DEFAULT_WEIGHTS['expert_weights'])
tech_w = weights.get('technical', {})
alpha_w = weights.get('alpha', {})
fund_w = weights.get('fundamental', {})
behav_w = weights.get('behavior', {})
buy_thresh = weights.get('signal_thresholds', {}).get('buy', 0.65)
sell_thresh = weights.get('signal_thresholds', {}).get('sell', 0.35)
_setup_analyze_cache(code, date)
try:
print('[MoE] Expert 1: 技术指标...')
tech = _score_tech(code, date, tech_w)
finally:
_teardown_analyze_cache()
print('[MoE] Expert 2: Alpha因子...')
alpha = _score_alpha(code, date, alpha_w)
print('[MoE] Expert 3: 基本面...')
fund = _score_fundamental(code, date, fund_w)
print('[MoE] Expert 4: 量价行为...')
behav = _score_behavior(code, date, behav_w)
result = _compute_final_score(tech, alpha, fund, behav, expert_w, buy_thresh, sell_thresh)
result['code'] = code
result['date'] = date
# 退市标记写入返回值
try:
basics = query_stock_basic(ts_codes=[code])
if basics:
b = basics[0]
if getattr(b, 'list_status', None) == 'D':
result['delisted'] = True
result['delist_date'] = getattr(b, 'delist_date', None) or '未知'
result['delist_warning'] = (
f"⚠️ {code}({getattr(b, 'name', code)})已于 {result['delist_date']} 退市,"
f"该股票已无法交易,以下分析仅供参考。"
)
except Exception:
pass
return result
# ─────────────────────────────────────────────────────────────────────────────
# 功能二:遗传算法权重训练
# ─────────────────────────────────────────────────────────────────────────────
def _get_trading_dates(start_date: str, end_date: str) -> List[str]:
"""从数据库获取月度采样日期列表(用000001.SZ取交易日历,每月取第一个交易日)。"""
klines = query_daily_kline(codes=['000001.SZ'], start_date=start_date, end_date=end_date,
order_by='date ASC', limit=None)
dates = sorted(set(k.date for k in klines))
monthly: Dict[str, str] = {}
for d in dates:
ym = d[:7]
if ym not in monthly:
monthly[ym] = d
return list(monthly.values())
# ── 训练缓存结构 ──────────────────────────────────────────────────────────────
# cache[(code, date)] = {
# 'tech_raw': Dict[str, float], # 技术指标原始分
# 'fund_raw': Dict[str, float], # 基本面原始分
# 'behav_raw': Dict[str, float], # 行为原始分
# 'future_ret': float, # 5日后实际收益率(用于适应度)
# }
_TRAIN_CACHE: Dict[Tuple[str, str], Dict] = {}
# ── analyze() 单次调用级指标内存缓存(消除 _score_tech 的 80+ 次重复 DB 查询)──────
from sqlalchemy import text as _sa_text
from data_fetcher import getEngine as _getEngine
_TECH_MEM_CACHE: Dict[Tuple, Optional[str]] = {}
_orig_get_cached_fn = None
_orig_save_indicator_fn = None
def _setup_analyze_cache(code: str, date: str) -> None:
"""在 analyze() 开始时调用:一次 SQL 批量读取该 code+date 的所有缓存指标到内存,
并 monkey-patch ind 模块的缓存函数,让 _score_tech 的 80+ 次查询走内存 dict 而非 DB。"""
global _TECH_MEM_CACHE, _orig_get_cached_fn, _orig_save_indicator_fn
_TECH_MEM_CACHE = {}
try:
with _getEngine().connect() as conn:
rows = conn.execute(_sa_text(
"SELECT indicator_type, period, use_adjusted, value "
"FROM cached_indicators WHERE code=:code AND date=:date"
), {"code": code, "date": date}).fetchall()
for r in rows:
_TECH_MEM_CACHE[(code, r[0], r[1], r[2], date)] = r[3]
except Exception:
pass # 失败时退化到原始 DB 查询,无副作用
_orig_get_cached_fn = ind._get_cached_indicator
_orig_save_indicator_fn = ind._save_indicator
def _patched_get(c, itype, period, idate, use_adj=True):
k = (c, itype, period, 1 if use_adj else 0, idate)
if k in _TECH_MEM_CACHE:
return _TECH_MEM_CACHE[k]
return _orig_get_cached_fn(c, itype, period, idate, use_adj)
def _patched_save(c, itype, period, idate, value, use_adj=True):
k = (c, itype, period, 1 if use_adj else 0, idate)
_TECH_MEM_CACHE[k] = value
_orig_save_indicator_fn(c, itype, period, idate, value, use_adj)
ind._get_cached_indicator = _patched_get
ind._save_indicator = _patched_save
def _teardown_analyze_cache() -> None:
"""在 analyze() 结束时调用:恢复 ind 模块原始缓存函数,清空内存缓存。"""
global _TECH_MEM_CACHE
if _orig_get_cached_fn is not None:
ind._get_cached_indicator = _orig_get_cached_fn
if _orig_save_indicator_fn is not None:
ind._save_indicator = _orig_save_indicator_fn
_TECH_MEM_CACHE = {}
def _precompute_cache(train_codes: List[str], train_dates: List[str], hold_days: int = 5) -> None:
"""
预计算阶段:一次性算好所有 (code, date) 的原始指标分和未来收益,
存入内存缓存。遗传算法迭代时只做纯内存加权,不再查DB。
"""
global _TRAIN_CACHE
_TRAIN_CACHE = {}
total = len(train_codes) * len(train_dates)
done = 0
print(f'[预计算] 共 {len(train_codes)} 只股票 × {len(train_dates)} 个日期 = {total} 个样本点', flush=True)
# 批量预取未来收益:查询每只股票在训练区间内的K线,避免逐条查DB
# key: code -> {date -> (buy_price, sell_price)}
price_map: Dict[str, Dict[str, Tuple[float, float]]] = {}
print('[预计算] 批量加载K线价格...', flush=True)
start_dt = train_dates[0] if train_dates else '2025-09-01'
end_future = (datetime.strptime(train_dates[-1], '%Y-%m-%d') + timedelta(days=hold_days * 3)).strftime('%Y-%m-%d')
batch_size = 200
for i in range(0, len(train_codes), batch_size):
batch = train_codes[i:i+batch_size]
klines = query_daily_kline(codes=batch, start_date=start_dt, end_date=end_future,
order_by='date ASC', limit=None)
for kl in klines:
if kl.code not in price_map:
price_map[kl.code] = {}
price_map[kl.code][kl.date] = kl.close
if (i // batch_size) % 5 == 0:
print(f' K线加载: {min(i+batch_size, len(train_codes))}/{len(train_codes)} 只...', flush=True)
def _future_ret(code: str, date: str) -> Optional[float]:
"""取买入日后第 hold_days 个已有交易日的收盘价计算收益。"""
cdates = price_map.get(code)
if not cdates:
return None
sorted_dates = sorted(cdates.keys())
try:
idx = sorted_dates.index(date)
except ValueError:
# 找最近的日期
before = [d for d in sorted_dates if d <= date]
if not before:
return None
idx = sorted_dates.index(before[-1])
buy_price = cdates[sorted_dates[idx]]
sell_idx = min(idx + hold_days, len(sorted_dates) - 1)
if sell_idx == idx:
return None
sell_price = cdates[sorted_dates[sell_idx]]
if buy_price > 0:
return (sell_price - buy_price) / buy_price
return None
print('[预计算] 计算技术/基本面/行为指标...', flush=True)
for ci, code in enumerate(train_codes):
for date in train_dates:
key = (code, date)
try:
tech_r = _score_tech(code, date, weights=None)
fund_r = _score_fundamental(code, date, weights=None)
behav_r = _score_behavior(code, date, weights=None)
fret = _future_ret(code, date)
_TRAIN_CACHE[key] = {
'tech_raw': tech_r.get('_raw_scores', {}) if tech_r else {},
'fund_raw': fund_r.get('_raw_scores', {}) if fund_r else {},
'behav_raw': behav_r.get('_raw_scores', {}) if behav_r else {},
'future_ret': fret,
}
except Exception:
pass
done += 1
if (ci + 1) % 100 == 0 or ci == len(train_codes) - 1:
print(f' 指标预计算: {ci+1}/{len(train_codes)} 只,缓存={len(_TRAIN_CACHE)} 条', flush=True)
print(f'[预计算] 完成,共缓存 {len(_TRAIN_CACHE)} 个样本点', flush=True)
def _evaluate_weights_fast(wconfig: Dict[str, Any]) -> float:
"""
快速适应度函数:直接从内存缓存读取指标原始分,
按当前权重重新加权,统计 BUY 信号命中率(预测准确率 × 平均收益)。
"""
tech_w = wconfig.get('technical', {})
fund_w = wconfig.get('fundamental', {})
behav_w = wconfig.get('behavior', {})
expert_w = {k: v for k, v in wconfig['expert_weights'].items()}
expert_w['alpha'] = 0.0 # 训练阶段不用 alpha
buy_thresh = wconfig.get('signal_thresholds', {}).get('buy', 0.65)
sell_thresh = wconfig.get('signal_thresholds', {}).get('sell', 0.35)
total_ret = 0.0
buy_count = 0
for (code, date), cache in _TRAIN_CACHE.items():
future_ret = cache.get('future_ret')
if future_ret is None:
continue
# 纯内存加权
tech_score = _weighted_mean(cache['tech_raw'], tech_w) if cache['tech_raw'] else None
fund_score = _weighted_mean(cache['fund_raw'], fund_w) if cache['fund_raw'] else None
behav_score = _weighted_mean(cache['behav_raw'], behav_w) if cache['behav_raw'] else None
experts = {}
if tech_score is not None: experts['technical'] = tech_score
if fund_score is not None: experts['fundamental'] = fund_score
if behav_score is not None: experts['behavior'] = behav_score
if not experts:
continue
total_ew = sum(expert_w.get(k, 0.0) for k in experts)
if total_ew <= 0:
continue
final = sum(expert_w.get(k, 0.0) / total_ew * s for k, s in experts.items())
if final >= buy_thresh:
total_ret += future_ret
buy_count += 1
return (total_ret / buy_count) if buy_count > 0 else 0.0
def _mutate(wconfig: Dict[str, Any], mutation_rate: float = 0.15, mutation_strength: float = 0.3) -> Dict[str, Any]:
"""变异:随机扰动部分权重。"""
new = copy.deepcopy(wconfig)
ew = new['expert_weights']
for k in ew:
if random.random() < mutation_rate:
ew[k] = max(0.01, ew[k] + random.gauss(0, mutation_strength * ew[k]))
total = sum(ew.values())
for k in ew:
ew[k] = ew[k] / total
for section in ['technical', 'fundamental', 'behavior']:
sec = new.get(section, {})
keys = list(sec.keys())
n_mutate = max(1, int(len(keys) * mutation_rate))
for k in random.sample(keys, min(n_mutate, len(keys))):
sec[k] = max(0.0, sec[k] + random.gauss(0, mutation_strength))
thresh = new.get('signal_thresholds', {'buy': 0.65, 'sell': 0.35})
if random.random() < mutation_rate:
thresh['buy'] = _clamp(thresh['buy'] + random.gauss(0, 0.05), 0.55, 0.85)
if random.random() < mutation_rate:
thresh['sell'] = _clamp(thresh['sell'] + random.gauss(0, 0.05), 0.15, 0.45)
new['signal_thresholds'] = thresh
return new
def _crossover(p1: Dict[str, Any], p2: Dict[str, Any]) -> Dict[str, Any]:
"""交叉:每个 key 随机选一个亲本。"""
child = copy.deepcopy(p1)
for k in child['expert_weights']:
if random.random() < 0.5:
child['expert_weights'][k] = p2['expert_weights'].get(k, child['expert_weights'][k])
total = sum(child['expert_weights'].values())
for k in child['expert_weights']:
child['expert_weights'][k] /= total
for section in ['technical', 'alpha', 'fundamental', 'behavior']:
sec1 = child.get(section, {})
sec2 = p2.get(section, {})
for k in sec1:
if random.random() < 0.5 and k in sec2:
sec1[k] = sec2[k]
if random.random() < 0.5:
child['signal_thresholds'] = copy.deepcopy(p2.get('signal_thresholds', {'buy': 0.65, 'sell': 0.35}))
return child
def train_weights(
start_date: str,
end_date: str,
population_size: int = 20,
generations: int = 30,
elite_count: int = 4,
train_stock_count: int = 0,
) -> Dict[str, Any]:
"""
功能二:遗传算法训练最优权重,目标:最大化 BUY 信号后5日平均收益。
架构:两阶段
1. 预计算阶段(一次性):批量计算所有股票×日期的指标原始分 + 未来收益,存入内存
2. 迭代阶段(快速):遗传算法每代只做纯内存加权,不再查DB,速度极快
Args:
start_date: 训练开始日期
end_date: 训练结束日期
population_size: 种群大小(默认20)
generations: 迭代代数(默认30)
elite_count: 每代保留的精英数量
train_stock_count: 训练股票数量(0=全量)
Returns:
优化后的权重配置字典(已写入 moe_weights.json)
"""
print(f'\n[遗传算法] 开始训练 {start_date} ~ {end_date}')
print(f' 种群={population_size} 代数={generations} 精英={elite_count} 股票数={"全量" if train_stock_count <= 0 else train_stock_count}')
all_stocks = [b.ts_code for b in query_stock_basic() if b.ts_code]
random.seed(42)
if train_stock_count <= 0 or train_stock_count >= len(all_stocks):
train_codes = all_stocks
else:
train_codes = random.sample(all_stocks, train_stock_count)
print(f' 训练股票: {train_codes[:5]}... 共{len(train_codes)}只')
train_dates = _get_trading_dates(start_date, end_date)
print(f' 训练日期: {len(train_dates)}个月度采样点 {train_dates}')
if not train_dates:
print('[遗传算法] 没有找到训练日期,退出')
return load_weights()
# ── 阶段一:预计算(只跑一次)──────────────────────────────────────────────
_precompute_cache(train_codes, train_dates, hold_days=5)
if not _TRAIN_CACHE:
print('[遗传算法] 预计算缓存为空,退出')
return load_weights()
# ── 阶段二:遗传算法迭代(纯内存)──────────────────────────────────────────
base = load_weights()
population = [base]
for _ in range(population_size - 1):
population.append(_mutate(base, mutation_rate=0.3, mutation_strength=0.5))
best_config = base
best_fitness = -999.0
for gen in range(generations):
print(f'\n[遗传算法] 第 {gen+1}/{generations} 代 评估{len(population)}个个体...')
fitness_scores = []
for i, wconfig in enumerate(population):
try:
fit = _evaluate_weights_fast(wconfig)
except Exception:
fit = -1.0
fitness_scores.append(fit)
print(f' 个体{i+1:2d}: BUY平均收益={fit*100:.2f}%')
ranked = sorted(zip(fitness_scores, population), key=lambda x: x[0], reverse=True)
best_gen_fit, best_gen_cfg = ranked[0]
if best_gen_fit > best_fitness:
best_fitness = best_gen_fit
best_config = copy.deepcopy(best_gen_cfg)
print(f' ★ 新最优: {best_fitness*100:.2f}%')
elites = [cfg for _, cfg in ranked[:elite_count]]
new_population = list(elites)
while len(new_population) < population_size:
if random.random() < 0.6 and len(elites) >= 2:
p1, p2 = random.sample(elites, 2)
child = _crossover(p1, p2)
else:
child = copy.deepcopy(random.choice(elites))
child = _mutate(child, mutation_rate=0.15, mutation_strength=0.2)
new_population.append(child)
population = new_population
print(f'\n[遗传算法] 训练完成!最优BUY平均收益: {best_fitness*100:.2f}%')
save_weights(best_config, train_period=f'{start_date}~{end_date}')
return best_config
# ─────────────────────────────────────────────────────────────────────────────
# 命令行入口
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='MoE 混合专家买卖时机分析 + 权重训练')
subparsers = parser.add_subparsers(dest='cmd')
p_analyze = subparsers.add_parser('analyze', help='分析股票买卖时机(默认)')
p_analyze.add_argument('--code', required=True, help='股票代码,如 000001.SZ')
p_analyze.add_argument('--date', default=None, help='分析日期 YYYY-MM-DD,默认今天')
p_train = subparsers.add_parser('train', help='遗传算法训练权重')
p_train.add_argument('--start-date', default=None, help='训练开始日期')
p_train.add_argument('--end-date', default=None, help='训练结束日期')
p_train.add_argument('--population', type=int, default=20, help='种群大小')
p_train.add_argument('--generations', type=int, default=30, help='迭代代数')
p_train.add_argument('--stocks', type=int, default=30, help='训练股票数量')
# 兼容旧的直接参数
parser.add_argument('--code', default=None, help='股票代码(兼容)')
parser.add_argument('--date', default=None, help='分析日期(兼容)')
parser.add_argument('--train', action='store_true', help='训练模式(兼容)')
parser.add_argument('--start-date', default=None, dest='train_start')
parser.add_argument('--end-date', default=None, dest='train_end')
args = parser.parse_args()
init_indicators_db()
is_train = (args.cmd == 'train') or getattr(args, 'train', False)
if is_train:
_end = getattr(args, 'end_date', None) or getattr(args, 'train_end', None) \
or datetime.today().strftime('%Y-%m-%d')
_start = getattr(args, 'start_date', None) or getattr(args, 'train_start', None) \
or (datetime.today() - timedelta(days=180)).strftime('%Y-%m-%d')
pop = getattr(args, 'population', 20)
gens = getattr(args, 'generations', 30)
stocks = getattr(args, 'stocks', 30)
train_weights(_start, _end, population_size=pop, generations=gens, train_stock_count=stocks)
else:
code = (getattr(args, 'code', None) if args.cmd == 'analyze' else None) or args.code
if not code:
parser.print_help()
sys.exit(1)
date_val = (getattr(args, 'date', None) if args.cmd == 'analyze' else None) or args.date
analysis_date = date_val or datetime.today().strftime('%Y-%m-%d')
result = analyze(code, analysis_date)
print('\n' + '='*60)
out = {k: v for k, v in result.items() if not k.startswith('_')}
print(json.dumps(out, ensure_ascii=False, indent=2))
print('='*60)
if 'signal' in result:
sig = result['signal']
score = result['final_score']
conf = result['confidence']
sig_cn = {'BUY': '买入 ▲', 'SELL': '卖出 ▼', 'HOLD': '持有 —'}[sig]
print(f'\n {result["code"]} 综合评分: {score:.4f} → {sig_cn} (置信度: {conf})')
print(f' {result["reason"]}')
FILE:scripts/moe_weights.json
{
"_comment": "MoE权重配置文件。通过 train_weights 命令跑回测优化后自动更新。",
"_version": 1,
"_trained_at": "2026-03-19 14:58:16",
"_train_period": "2025-09-01~2026-03-19",
"expert_weights": {
"technical": 0.35717595519629336,
"alpha": 0.36057193372593965,
"fundamental": 0.15416369933688986,
"behavior": 0.12808841174087712
},
"signal_thresholds": {
"buy": 0.7313263753423049,
"sell": 0.35
},
"technical": {
"sma5": 1.181801288003714,
"sma10": 0.7570724714871282,
"sma20": 1.0,
"sma60": 1.1118537267876476,
"ema5": 1.0,
"ema12": 1.0,
"ema20": 0.27926056566514107,
"ema26": 1.0,
"ema_cross": 0.6575509109292674,
"wma20": 1.0,
"tema20": 1.0,
"dema20": 0.8608835251864292,
"kama": 1.0,
"bbi": 0.8636807483782426,
"trix": 1.833969089253665,
"dmi": 1.0,
"sar": 0.8951085513864419,
"linearreg_slope": 1.3211429094664284,
"linearreg": 1.0923904719425455,
"linearreg_angle": 0.5262131258427737,
"linearreg_intercept": 0.9885568378088866,
"aroon": 0.7826516778972384,
"tsf": 1.0,
"ht_trendmode": 0.8814204448925165,
"ht_dcphase": 1.0,
"ht_sine": 1.0,
"rsi14": 0.8662579600815672,
"rsi6": 1.0,
"cci": 1.0,
"mom10": 0.7352808685133152,
"mom20": 1.0,
"roc10": 1.0,
"rocp10": 1.7929313995073208,
"rocr10": 1.0,
"roc_r": 2.064310646291524,
"willr": 1.0,
"cmo": 1.0,
"bias": 1.0371024748469209,
"psycho": 1.0,
"dpo": 1.0,
"mass": 1.0,
"kdj_j": 1.0,
"kdj_kd": 1.0,
"stoch_k": 1.1291875450945286,
"stochf_k": 1.0,
"stochrsi": 1.0,
"ultosc": 0.727829745700426,
"macd_hist": 0.6826161537737307,
"macd_cross": 0.9130399186907544,
"ppo_hist": 0.9136754128716245,
"adosc": 1.0,
"obv": 1.2333726939245337,
"ad": 1.0,
"mfi": 1.1649303644243527,
"vwap": 0.32248302163927334,
"volume_ratio": 1.6461768542684325,
"vr": 1.0,
"pvi": 0.9351558341111518,
"nvi": 1.0,
"ar": 1.6225854372575186,
"br": 0.7882101600971572,
"brar": 1.0,
"asi": 1.0,
"bb_pos": 1.0778193939643872,
"bb_signal": 1.201047760357908,
"bbands_pct": 1.0219943929294277,
"bbands_width": 1.0074095539385972,
"ma_channel": 0.25496657546761314,
"donchian": 0.053137051875268626,
"keltner": 1.0,
"xue_channel": 1.0487232147691394,
"midpoint": 0.3385211409278348,
"midprice": 1.0,
"atr": 1.0792445125369383,
"natr": 1.0,
"tr": 1.2644451626865996,
"trange": 0.2911803623562474,
"stddev": 1.0,
"var": 1.0,
"correl": 1.0,
"beta": 0.7630957463073703,
"ht_dcperiod": 1.2793115495425174,
"typical": 1.0,
"median": 1.0,
"wclose": 1.0,
"avgp": 1.0,
"ht_phasor": 1.0826827348531094,
"consec_rise": 1.1666511139322437,
"consec_fall": 1.0,
"bomb_board": 1.0,
"bomb_board_count": 1.0,
"consec_limit_up": 1.0
},
"alpha": {
"alpha001": 1.0,
"alpha002": 1.0,
"alpha003": 1.0,
"alpha004": 1.0,
"alpha005": 1.0,
"alpha006": 1.0,
"alpha007": 1.0,
"alpha008": 1.0,
"alpha009": 1.0,
"alpha010": 1.0,
"alpha011": 1.0,
"alpha012": 1.0,
"alpha013": 1.0,
"alpha014": 1.0,
"alpha015": 1.0,
"alpha016": 1.0,
"alpha017": 1.0,
"alpha018": 1.0,
"alpha019": 1.0,
"alpha020": 1.0,
"alpha021": 1.0,
"alpha022": 1.0,
"alpha023": 1.0,
"alpha024": 1.0,
"alpha025": 1.0,
"alpha026": 1.0,
"alpha027": 1.0,
"alpha028": 1.0,
"alpha029": 1.0,
"alpha030": 1.0,
"alpha031": 1.0,
"alpha032": 1.0,
"alpha033": 1.0,
"alpha034": 1.0,
"alpha035": 1.0,
"alpha036": 1.0,
"alpha037": 1.0,
"alpha038": 1.0,
"alpha039": 1.0,
"alpha040": 1.0,
"alpha041": 1.0,
"alpha042": 1.0,
"alpha043": 1.0,
"alpha044": 1.0,
"alpha045": 1.0,
"alpha046": 1.0,
"alpha047": 1.0,
"alpha048": 1.0,
"alpha049": 1.0,
"alpha050": 1.0,
"alpha051": 1.0,
"alpha052": 1.0,
"alpha053": 1.0,
"alpha054": 1.0,
"alpha055": 1.0,
"alpha056": 1.0,
"alpha057": 1.0,
"alpha058": 1.0,
"alpha059": 1.0,
"alpha060": 1.0,
"alpha061": 1.0,
"alpha062": 1.0,
"alpha063": 1.0,
"alpha064": 1.0,
"alpha065": 1.0,
"alpha066": 1.0,
"alpha067": 1.0,
"alpha068": 1.0,
"alpha069": 1.0,
"alpha070": 1.0,
"alpha071": 1.0,
"alpha072": 1.0,
"alpha073": 1.0,
"alpha074": 1.0,
"alpha075": 1.0,
"alpha076": 1.0,
"alpha077": 1.0,
"alpha078": 1.0,
"alpha079": 1.0,
"alpha080": 1.0,
"alpha081": 1.0,
"alpha082": 1.0,
"alpha083": 1.0,
"alpha084": 1.0,
"alpha085": 1.0,
"alpha086": 1.0,
"alpha087": 1.0,
"alpha088": 1.0,
"alpha089": 1.0,
"alpha090": 1.0,
"alpha091": 1.0,
"alpha092": 1.0,
"alpha093": 1.0,
"alpha094": 1.0,
"alpha095": 1.0,
"alpha096": 1.0,
"alpha097": 1.0,
"alpha098": 1.0,
"alpha099": 1.0,
"alpha100": 1.0,
"alpha101": 1.0
},
"fundamental": {
"pe_ttm": 1.7102072285522736,
"pb": 1.0,
"turnover_rate": 1.0,
"volume_ratio": 0.7606881867266152,
"ps_ttm": 1.4705142753123992
},
"behavior": {
"limit_score": 1.2956770069079582,
"consecutive_limit_up": 1.0,
"bomb_board": 1.0768788113552823,
"top_list": 0.8377720620665754,
"pct_chg_5d": 1.0
}
}
FILE:scripts/realtime_data_featcher.py
import requests
import time
import json
import random
import re
from typing import Dict, List, Optional, Union, Any
from dataclasses import dataclass
from define import RealtimeStockQuote
class RealTimeDataFetcher:
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
self.last_request_time = {}
self.min_interval = 1.0 # Minimum int
def _convert_to_sina_symbol(self, ts_code: str) -> str:
# 000001.SZ -> sz000001
# 600519.SH -> sh600519
code, market = ts_code.split('.')
return f"{market.lower()}{code}"
def _wait_for_rate_limit(self, source: str):
current_time = time.time()
last_time = self.last_request_time.get(source, 0)
elapsed = current_time - last_time
if elapsed < self.min_interval:
sleep_time = self.min_interval - elapsed
time.sleep(sleep_time)
self.last_request_time[source] = time.time()
def request_stock_info(self, ts_code: str) -> Optional[RealtimeStockQuote]:
"""
查询实时股价信息
参数
ts_code 股票代码
返回
RealtimeStockQuote 实时股票报价信息数据结构
"""
self._wait_for_rate_limit('sina')
symbol = self._convert_to_sina_symbol(ts_code)
url = f"http://hq.sinajs.cn/list={symbol}"
headers = self.headers.copy()
headers['Referer'] = 'https://finance.sina.com.cn/'
try:
response = requests.get(url, headers=headers, timeout=5)
response.raise_for_status()
# Format: var hq_str_sh601006="大秦铁路, 27.55, 27.25, 26.91, 27.55, 26.20, 26.91, 26.92, ...";
content = response.text
match = re.search(r'="(.*)";', content)
if match:
data_str = match.group(1)
parts = data_str.split(',')
if len(parts) > 30:
pre_close = float(parts[2])
high = float(parts[4])
low = float(parts[5])
amplitude = 0.0
if pre_close > 0:
amplitude = (high - low) / pre_close * 100
return RealtimeStockQuote(
ts_code=ts_code,
name=parts[0],
open=float(parts[1]),
pre_close=pre_close,
price=float(parts[3]),
high=high,
low=low,
bid=float(parts[6]),
ask=float(parts[7]),
volume=int(parts[8]), # Sina returns shares
amount=float(parts[9]),
date=parts[30],
time=parts[31],
amplitude=round(amplitude, 2),
turnover_rate=None,
total_cap=None,
circ_cap=None,
pb=None,
pe_ttm=None,
total_shares=None,
circ_shares=None,
status="success"
)
return None
except Exception:
return None
if __name__ == "__main__":
RealTimeDataFetcher().fetch_sina("000001.SZ")
FILE:scripts/remote_api.py
from typing import List, Optional, Dict, Any
import define
import requests
from define import AppVersion,TokenCheckResult
import config
class PatchItem:
def __init__(self):
self.patch_date:str = ""
self.patch_name:str = ""
self.version:int = int
def request_patch_list() -> List[PatchItem]:
"""
获取所有表的patch列表
返回json说明:
key: 表名
value: 所有可用patch列表
"""
ret: List[PatchItem] = []
token = config.get_token()
response = requests.get(f"{define.BASE_URL}/api/patch_list/all", params={"token": token})
if response.status_code == 200:
rsp = response.json()
datas_json = rsp["data"]
for data_json in datas_json:
item: PatchItem = PatchItem()
item.patch_name = data_json["patch_name"]
item.patch_date = data_json["patch_date"]
item.version = int(float(data_json["version"]) * 10)
ret.append(item)
return ret
def request_decrypt_key(file_name:str, token_key:str) -> str:
url = f"{define.BASE_URL}/api/get_decryption_key"
params = {
"file_name": file_name,
"token_key": token_key
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
key = data.get("key")
return key
else:
return ""
except Exception as e:
return ""
def request_download_url(file_name: str, token_key: str) -> str:
"""
获取文件的下载链接。
参数:
file_name: 文件名(如 data_1.0.bin)
token_key: API 访问令牌
返回:
str: 下载链接,失败返回空字符串
"""
url = f"{define.BASE_URL}/api/download_file"
params = {
"file_name": file_name,
"token_key": token_key
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
download_url = data.get("download_url", "")
return download_url
else:
return ""
except Exception as e:
print(f"request_download_url error: {e}")
return ""
def request_version() -> AppVersion:
url = f"{define.BASE_URL}/api/get_latest_version"
try:
params = {
"token": config.get_token()
}
response = requests.post(url, json=params)
if response.status_code == 200:
data = response.json()
ver = AppVersion.from_dict(data)
return ver
else:
return None
except Exception as e:
print(e)
return None
def request_check_token(token:str) -> TokenCheckResult:
url = f"{define.BASE_URL}/api/check_token"
params = {
"token": token
}
try:
response = requests.post(url, json=params)
data = response.json()
return TokenCheckResult.from_dict(data)
except Exception as e:
print(e)
return TokenCheckResult.from_dict({"status": "failure", "message": str(e)})
def request_get_benchmark(token: str) -> Optional[Dict[str, float]]:
"""
获取服务器配置的基准线阈值。
返回 dict,包含 total_yield / annualized_yield / max_drawdown / sharpe_ratio;
失败(token无效、网络错误)返回 None。
"""
url = f"{define.BASE_URL}/api/get_benchmark"
try:
response = requests.post(url, json={"token": token}, timeout=define.HTTP_TIMEOUT)
if response.status_code == 200:
return response.json()
else:
print(f"get_benchmark failed: {response.status_code} {response.text}")
return None
except Exception as e:
print(f"get_benchmark error: {e}")
return None
class SubmitYieldResult:
"""提交收益数据的结果"""
def __init__(self, success: bool, message: str, details: Optional[List[str]] = None):
self.success = success
self.message = message
self.details = details or []
def __repr__(self) -> str:
return f"SubmitYieldResult(success={self.success!r}, message={self.message!r}, details={self.details!r})"
def request_submit_yield(
token: str,
total_yield: float,
annualized_yield: float,
max_drawdown: float,
sharpe_ratio: float,
positions: Any = None,
) -> SubmitYieldResult:
"""
提交收益数据到排行榜。
服务器仅强制校验 total_yield 是否超过配置基准线,其余指标不做强制要求。
成功返回 SubmitYieldResult(success=True);
失败返回 SubmitYieldResult(success=False, message=..., details=...)。
"""
url = f"{define.BASE_URL}/api/submit_yield"
yield_data = {
"total_yield": total_yield,
"annualized_yield": annualized_yield,
"max_drawdown": max_drawdown,
"sharpe_ratio": sharpe_ratio,
"positions": positions if positions is not None else [],
}
body = {
"token": token,
"data": yield_data,
}
try:
response = requests.post(url, json=body, timeout=define.HTTP_TIMEOUT)
data = response.json()
if response.status_code == 200 and data.get("status") == "ok":
return SubmitYieldResult(success=True, message="提交成功")
else:
msg = data.get("message", f"HTTP {response.status_code}")
details = data.get("details", [])
return SubmitYieldResult(success=False, message=msg, details=details)
except Exception as e:
return SubmitYieldResult(success=False, message=str(e))
if __name__ == "__main__":
_token = "8ACw626fHId31d3OWwVE62yzGkA7p9vCyg1kIV9AKSiU"
print("=== check_token ===")
check_result = request_check_token(_token)
print(check_result)
print("\n=== get_benchmark ===")
bm = request_get_benchmark(_token)
print(bm)
print("\n=== submit_yield (应该被基准线拒绝) ===")
r1 = request_submit_yield(_token, total_yield=0.01, annualized_yield=0.05,
max_drawdown=-0.10, sharpe_ratio=0.8)
print(r1)
print("\n=== submit_yield (超过基准线,应该成功) ===")
r2 = request_submit_yield(_token, total_yield=0.50, annualized_yield=0.35,
max_drawdown=-0.08, sharpe_ratio=2.1,
positions=[{"code": "000001.SZ", "weight": 1.0}])
print(r2)
FILE:scripts/signals.py
"""
signal.py - 裸K形态信号模块
功能:
1. 识别经典K线形态,输出形态信号
2. 形态出现当日信号值为 1,未出现为 0
3. 计算结果缓存到数据库(cached_signals 表)
支持形态:
- 早晨之星 / 启明星(Morning Star) :底部三K线看涨反转
- 黄昏之星 / 黄昏星(Evening Star) :顶部三K线看跌反转
- 红三兵(Three White Soldiers) :底部三根连续阳线看涨确认
- 三只乌鸦(Three Black Crows) :顶部三根连续阴线看跌确认
- 乌云盖顶(Dark Cloud Cover) :顶部两K线看跌反转
- 圆弧底(Rounding Bottom) :多K线底部圆弧形态(默认20日)
- 上升三角形(Ascending Triangle) :整理后向上突破前夕(默认20日)
- 顶部形态(Top Pattern) :双重顶等多K线顶部反转结构(默认30日)
设计原则:
- 函数接口与 indicators.py 统一(code/date/params/use_adjusted)
- 查询优先使用缓存,计算后存入数据库
- 默认使用后复权价格,避免除权日跳空干扰形态识别
- 缓存键仅包含 period 参数;修改 body_ratio 等形态阈值时需手动清缓存
缓存表 cached_signals 结构:
UNIQUE(code, signal_type, param, use_adjusted, date)
value INTEGER:1 表示形态出现,0 表示未出现
"""
from typing import Dict, List, Optional
from sqlalchemy import text
from data_fetcher import getEngine
from data_fetcher import query_daily_kline, query_daily_basic
from define import DailyKline
# ── 缓存机制 ─────────────────────────────────────────────────────────────────
def init_signals_db() -> None:
"""初始化信号缓存数据库表
创建 cached_signals 表(如不存在),用于缓存形态信号计算结果,并建立查询索引。
每次计算前先查缓存,命中则直接返回,避免对相同参数重复计算。
"""
with getEngine().connect() as conn:
conn.execute(text("""
CREATE TABLE IF NOT EXISTS cached_signals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
signal_type TEXT NOT NULL,
param INTEGER NOT NULL DEFAULT 0,
use_adjusted INTEGER NOT NULL DEFAULT 1,
date TEXT NOT NULL,
value INTEGER NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, signal_type, param, use_adjusted, date)
)
"""))
conn.execute(text(
"CREATE INDEX IF NOT EXISTS idx_signals_lookup "
"ON cached_signals(code, signal_type, param, use_adjusted, date)"
))
conn.commit()
_signals_table_ready: bool = False
def _ensure_table() -> None:
global _signals_table_ready
if not _signals_table_ready:
init_signals_db()
_signals_table_ready = True
def _get_cached_signal(
code: str, signal_type: str, param: int, date: str, use_adjusted: bool
) -> Optional[int]:
"""从缓存表查询信号值
Args:
code: 股票代码
signal_type: 信号类型字符串,如 'MORNING_STAR'
param: 主要周期参数(无周期的形态传 0)
date: 查询日期,格式 'YYYY-MM-DD'
use_adjusted: 是否为复权计算结果
Returns:
int: 缓存的信号值(0 或 1),未命中返回 None
"""
_ensure_table()
with getEngine().connect() as conn:
cursor = conn.execute(text(
"SELECT value FROM cached_signals "
"WHERE code=:code AND signal_type=:st AND param=:p "
" AND use_adjusted=:adj AND date=:d"
), {"code": code, "st": signal_type, "p": param,
"adj": 1 if use_adjusted else 0, "d": date})
row = cursor.fetchone()
return int(row[0]) if row else None
def _save_signal(
code: str, signal_type: str, param: int, date: str, value: int, use_adjusted: bool
) -> None:
"""将信号值保存到缓存表
已存在则替换(INSERT OR REPLACE),确保缓存始终为最新值。
Args:
code: 股票代码
signal_type: 信号类型字符串
param: 主要周期参数(无周期的形态传 0)
date: 计算日期,格式 'YYYY-MM-DD'
value: 信号值,0 或 1
use_adjusted: 是否为复权计算结果
"""
_ensure_table()
with getEngine().connect() as conn:
conn.execute(text(
"INSERT OR REPLACE INTO cached_signals "
"(code, signal_type, param, use_adjusted, date, value) "
"VALUES (:code, :st, :p, :adj, :d, :v)"
), {"code": code, "st": signal_type, "p": param,
"adj": 1 if use_adjusted else 0, "d": date, "v": value})
conn.commit()
# ── K 线获取与复权(与 indicators.py 保持一致)─────────────────────────────
def _get_klines_before_date(code: str, date: str, limit: int) -> List[DailyKline]:
"""获取指定日期(含)前最近 limit 根K线,按时间升序排列"""
klines = query_daily_kline(codes=[code], end_date=date, limit=limit, order_by="date DESC")
return klines[::-1]
def _get_adj_factors_for_klines(klines: List[DailyKline]) -> Dict[str, float]:
"""批量获取K线覆盖日期范围内的复权因子"""
if not klines:
return {}
code = klines[0].code
start_date = klines[0].date
end_date = klines[-1].date
daily_basics = query_daily_basic(ts_codes=[code], start_date=start_date, end_date=end_date)
return {b.trade_date: b.adj_factor for b in daily_basics}
def _adjust_klines(klines: List[DailyKline], adj_factors: Dict[str, float]) -> List[DailyKline]:
"""后复权处理:adjusted_price = raw_price × adj_factor
成交量不做调整,价格字段(open/high/low/close/pre_close/change/amount)全部乘以因子。
"""
if not klines or not adj_factors:
return klines
result = []
for k in klines:
f = adj_factors.get(k.date) or 1.0
result.append(DailyKline(
date=k.date, code=k.code,
open=k.open * f,
high=k.high * f,
low=k.low * f,
close=k.close * f,
volume=k.volume,
amount=k.amount * f if k.amount else 0.0,
adjustflag=k.adjustflag,
turn=k.turn,
pctChg=k.pctChg,
pre_close=k.pre_close * f if k.pre_close else 0.0,
change=k.change * f if k.change else 0.0,
))
return result
# ── 内部辅助函数 ─────────────────────────────────────────────────────────────
def _body_ratio(k: DailyKline) -> float:
"""实体比:K线实体大小 / 总振幅(0~1);振幅为 0 时返回 0"""
rng = k.high - k.low
return abs(k.close - k.open) / rng if rng > 0 else 0.0
def _upper_shadow_ratio(k: DailyKline) -> float:
"""上影线比:上影线长度 / 总振幅(0~1);振幅为 0 时返回 0"""
rng = k.high - k.low
return (k.high - max(k.open, k.close)) / rng if rng > 0 else 0.0
def _lower_shadow_ratio(k: DailyKline) -> float:
"""下影线比:下影线长度 / 总振幅(0~1);振幅为 0 时返回 0"""
rng = k.high - k.low
return (min(k.open, k.close) - k.low) / rng if rng > 0 else 0.0
def _is_bullish(k: DailyKline) -> bool:
"""阳线判断:收盘价 > 开盘价"""
return k.close > k.open
def _is_bearish(k: DailyKline) -> bool:
"""阴线判断:收盘价 < 开盘价"""
return k.close < k.open
def _linear_slope(values: List[float]) -> float:
"""最小二乘线性回归斜率(每格的平均变化量);数据不足 2 个时返回 0"""
n = len(values)
if n < 2:
return 0.0
x_mean = (n - 1) / 2.0
y_mean = sum(values) / n
num = sum((i - x_mean) * (values[i] - y_mean) for i in range(n))
den = sum((i - x_mean) ** 2 for i in range(n))
return num / den if den > 0 else 0.0
def _find_local_highs(values: List[float], window: int = 3) -> List[int]:
"""返回局部极大值的索引列表
在 [i-window, i+window] 范围内,values[i] 是最大值则认为是局部极大值。
"""
result = []
n = len(values)
for i in range(window, n - window):
if values[i] == max(values[i - window: i + window + 1]):
result.append(i)
return result
def _find_local_lows(values: List[float], window: int = 3) -> List[int]:
"""返回局部极小值的索引列表
在 [i-window, i+window] 范围内,values[i] 是最小值则认为是局部极小值。
"""
result = []
n = len(values)
for i in range(window, n - window):
if values[i] == min(values[i - window: i + window + 1]):
result.append(i)
return result
# ── 形态信号函数 ─────────────────────────────────────────────────────────────
def get_morning_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""早晨之星 / 启明星(Morning Star)—— 底部三K线看涨反转信号
由三根连续K线构成的经典底部反转形态,信号在第三根K线(确认阳线)日期触发:
· 第1根:大阴线(实体比 >= large_body_ratio),确认此前下跌趋势;
· 第2根:十字星或小实体(实体比 <= doji_ratio),多空力量均衡,市场犹豫;
· 第3根:大阳线(实体比 >= large_body_ratio),收盘高于第1根阴线实体
底部向上 penetrate_ratio 处(默认刺穿中点),确认买方入场。
使用复权价格可避免除权日产生的价格跳空被误判为星线间隙。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期(信号触发日期),格式 'YYYY-MM-DD'
large_body_ratio: 大实体最小实体比阈值,默认 0.6
doji_ratio: 星线(第2根)最大实体比阈值,默认 0.3
penetrate_ratio: 第3根K线需从第1根阴线底部(close)向上穿透的比例,
默认 0.5(即收盘至少在第1根实体中点以上)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现早晨之星(启明星),0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'MORNING_STAR', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
# k1 阴线:open > close;实体从 close(底)到 open(顶)
k1_body = abs(k1.open - k1.close)
value = 1 if (
_is_bearish(k1) and _body_ratio(k1) >= large_body_ratio
and _body_ratio(k2) <= doji_ratio
and _is_bullish(k3) and _body_ratio(k3) >= large_body_ratio
and k3.close >= k1.close + penetrate_ratio * k1_body
) else 0
_save_signal(code, 'MORNING_STAR', 0, date, value, use_adjusted)
return value
def get_evening_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""黄昏之星 / 黄昏星(Evening Star)—— 顶部三K线看跌反转信号
早晨之星(启明星)的镜像形态,信号在第三根K线(确认阴线)日期触发:
· 第1根:大阳线(实体比 >= large_body_ratio),确认此前上涨趋势;
· 第2根:十字星或小实体(实体比 <= doji_ratio),多空犹豫;
· 第3根:大阴线(实体比 >= large_body_ratio),收盘低于第1根阳线实体
顶部向下 penetrate_ratio 处(默认刺穿中点),确认卖方入场。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期(信号触发日期),格式 'YYYY-MM-DD'
large_body_ratio: 大实体最小实体比阈值,默认 0.6
doji_ratio: 星线(第2根)最大实体比阈值,默认 0.3
penetrate_ratio: 第3根K线需从第1根阳线顶部(close)向下穿透的比例,
默认 0.5(即收盘至少在第1根实体中点以下)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现黄昏之星(黄昏星),0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'EVENING_STAR', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
# k1 阳线:close > open;实体从 open(底)到 close(顶)
k1_body = abs(k1.close - k1.open)
value = 1 if (
_is_bullish(k1) and _body_ratio(k1) >= large_body_ratio
and _body_ratio(k2) <= doji_ratio
and _is_bearish(k3) and _body_ratio(k3) >= large_body_ratio
and k3.close <= k1.close - penetrate_ratio * k1_body
) else 0
_save_signal(code, 'EVENING_STAR', 0, date, value, use_adjusted)
return value
def get_three_white_soldiers(
code: str,
date: str,
body_ratio: float = 0.5,
shadow_ratio: float = 0.2,
use_adjusted: bool = True,
) -> Optional[int]:
"""红三兵(Three White Soldiers)—— 底部连续三阳看涨确认信号
三根连续上涨的大实体阳线,表明多方持续主导:
· 三根全为阳线(close > open);
· 每根收盘价高于前一根;
· 每根开盘价在前一根阳线实体内(逐步递进,拒绝跳空);
· 实体比均 >= body_ratio;
· 上影线比均 <= shadow_ratio(收盘接近当日最高价,多方强势)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
body_ratio: 每根K线的最小实体比阈值,默认 0.5
shadow_ratio: 每根K线的最大上影线比阈值,默认 0.2
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现红三兵,0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'THREE_WHITE_SOLDIERS', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
value = 1 if (
_is_bullish(k1) and _is_bullish(k2) and _is_bullish(k3)
and _body_ratio(k1) >= body_ratio
and _body_ratio(k2) >= body_ratio
and _body_ratio(k3) >= body_ratio
and _upper_shadow_ratio(k1) <= shadow_ratio
and _upper_shadow_ratio(k2) <= shadow_ratio
and _upper_shadow_ratio(k3) <= shadow_ratio
and k2.close > k1.close and k3.close > k2.close # 逐步创新高
and k1.open <= k2.open <= k1.close # k2 开盘在 k1 实体内
and k2.open <= k3.open <= k2.close # k3 开盘在 k2 实体内
) else 0
_save_signal(code, 'THREE_WHITE_SOLDIERS', 0, date, value, use_adjusted)
return value
def get_three_black_crows(
code: str,
date: str,
body_ratio: float = 0.5,
shadow_ratio: float = 0.2,
use_adjusted: bool = True,
) -> Optional[int]:
"""三只乌鸦(Three Black Crows)—— 顶部连续三阴看跌确认信号
红三兵的镜像形态,三根连续下跌的大实体阴线,表明空方持续主导:
· 三根全为阴线(close < open);
· 每根收盘价低于前一根;
· 每根开盘价在前一根阴线实体内(逐步递进,拒绝跳空);
· 实体比均 >= body_ratio;
· 下影线比均 <= shadow_ratio(收盘接近当日最低价,空方强势)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
body_ratio: 每根K线的最小实体比阈值,默认 0.5
shadow_ratio: 每根K线的最大下影线比阈值,默认 0.2
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现三只乌鸦,0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'THREE_BLACK_CROWS', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
value = 1 if (
_is_bearish(k1) and _is_bearish(k2) and _is_bearish(k3)
and _body_ratio(k1) >= body_ratio
and _body_ratio(k2) >= body_ratio
and _body_ratio(k3) >= body_ratio
and _lower_shadow_ratio(k1) <= shadow_ratio
and _lower_shadow_ratio(k2) <= shadow_ratio
and _lower_shadow_ratio(k3) <= shadow_ratio
and k2.close < k1.close and k3.close < k2.close # 逐步创新低
and k1.close <= k2.open <= k1.open # k2 开盘在 k1 实体内
and k2.close <= k3.open <= k2.open # k3 开盘在 k2 实体内
) else 0
_save_signal(code, 'THREE_BLACK_CROWS', 0, date, value, use_adjusted)
return value
def get_dark_cloud_cover(
code: str,
date: str,
body_ratio: float = 0.5,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""乌云盖顶(Dark Cloud Cover)—— 顶部两K线看跌反转信号
两根K线构成的顶部反转形态:
· 第1根:大阳线(实体比 >= body_ratio),确认此前上涨;
· 第2根:阴线,开盘价高于第1根收盘价(高位开盘),随后大幅回落,
收盘低于第1根阳线实体顶部向下 penetrate_ratio 处(默认中点),
且高于第1根开盘价(未完全吞没,否则为吞噬形态)。
乌云盖顶说明多方在高位遭遇强力抛压,多空转换信号明显。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
body_ratio: 第1根阳线的最小实体比阈值,默认 0.5
penetrate_ratio: 第2根阴线向下穿透第1根实体的最小比例,
默认 0.5(收盘须低于第1根实体中点)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现乌云盖顶,0 表示未出现;数据不足 2 根时返回 None
"""
cached = _get_cached_signal(code, 'DARK_CLOUD_COVER', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 2)
if len(klines) < 2:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2 = klines[-2], klines[-1]
k1_body = k1.close - k1.open # k1 阳线:close > open,k1_body > 0
value = 1 if (
_is_bullish(k1) and _body_ratio(k1) >= body_ratio
and _is_bearish(k2)
and k2.open > k1.close # 向上跳空开盘(高于前收)
and k2.close < k1.close - penetrate_ratio * k1_body # 刺入 k1 实体下半段
and k2.close > k1.open # 未完全吞没 k1(区别于吞噬形态)
) else 0
_save_signal(code, 'DARK_CLOUD_COVER', 0, date, value, use_adjusted)
return value
def get_rounding_bottom(
code: str,
date: str,
period: int = 20,
symmetry_tol: float = 0.1,
use_adjusted: bool = True,
) -> Optional[int]:
"""圆弧底(Rounding Bottom)—— 多K线底部圆弧反转形态
收盘价在 period 根K线内呈平滑"U"形分布,反映多空力量的渐进转换:
· 左侧(前 1/3)平均收盘 > 中部(中 1/3)平均收盘(价格从高位缓慢下行);
· 右侧(后 1/3)平均收盘 > 中部平均收盘(价格从低位缓慢回升);
· 期间最低收盘价出现在中间三分之一区域(底部圆弧的谷底在中部);
· 右侧平均收盘 >= 左侧平均收盘 × (1 - symmetry_tol)(右侧已充分回升);
· 最近 3 根K线收盘价呈上升趋势(线性斜率 > 0,当前处于上升右侧)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯K线根数,默认 20
symmetry_tol: 右侧回升相对左侧的最小比例容差,默认 0.1(即右侧须恢复至左侧 90% 以上)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现圆弧底,0 表示未出现;数据不足 period 根时返回 None
"""
cached = _get_cached_signal(code, 'ROUNDING_BOTTOM', period, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
closes = [k.close for k in klines]
n = len(closes)
t = n // 3
left = closes[:t]
mid = closes[t: 2 * t]
right = closes[2 * t:]
avg_left = sum(left) / len(left)
avg_mid = sum(mid) / len(mid)
avg_right = sum(right) / len(right)
min_idx = closes.index(min(closes))
value = 1 if (
avg_left > avg_mid # 左侧高于底部
and avg_right > avg_mid # 右侧高于底部
and t <= min_idx < 2 * t # 最低点在中间三分之一
and avg_right >= avg_left * (1 - symmetry_tol) # 右侧已充分回升
and _linear_slope(closes[-3:]) > 0 # 近期处于上升趋势
) else 0
_save_signal(code, 'ROUNDING_BOTTOM', period, date, value, use_adjusted)
return value
def get_ascending_triangle(
code: str,
date: str,
period: int = 20,
resistance_tol: float = 0.02,
use_adjusted: bool = True,
) -> Optional[int]:
"""上升三角形(Ascending Triangle)—— 突破前夕看涨整理形态
价格在一段时间内呈现"水平阻力 + 上升支撑"的三角形收敛结构:
· 阻力线:期间局部高点聚集在同一水平(局部极大值的标准差/均值 <= resistance_tol);
· 支撑线:期间局部低点依次抬高(局部极小值的线性回归斜率 > 0);
· 当前收盘价接近阻力位(>= 阻力位 × (1 - resistance_tol)),处于突破蓄势阶段。
判断局部极大/极小值时采用 window=3(左右各3根K线),period 至少应为 10 才能
检测到足够数量的极值点。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯K线根数,默认 20
resistance_tol: 阻力位水平性判断的最大变异系数(std/mean),默认 0.02(即 2%)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日存在上升三角形形态,0 表示未出现;数据不足 period 根时返回 None
"""
cached = _get_cached_signal(code, 'ASCENDING_TRIANGLE', period, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
highs = [k.high for k in klines]
lows = [k.low for k in klines]
closes = [k.close for k in klines]
# 阻力线:局部高点需聚集在同一水平(至少 2 个局部极大值)
peak_idxs = _find_local_highs(highs, window=3)
if len(peak_idxs) < 2:
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, 0, use_adjusted)
return 0
peak_vals = [highs[i] for i in peak_idxs]
mean_peak = sum(peak_vals) / len(peak_vals)
if mean_peak == 0:
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, 0, use_adjusted)
return 0
variance = sum((v - mean_peak) ** 2 for v in peak_vals) / len(peak_vals)
std_peak = variance ** 0.5
flat_resistance = (std_peak / mean_peak) <= resistance_tol
# 支撑线:局部低点斜率 > 0(至少 2 个局部极小值)
trough_idxs = _find_local_lows(lows, window=3)
if len(trough_idxs) < 2:
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, 0, use_adjusted)
return 0
trough_vals = [lows[i] for i in trough_idxs]
rising_support = _linear_slope(trough_vals) > 0
# 当前价格接近阻力位(突破前夕)
resistance = max(highs)
near_breakout = closes[-1] >= resistance * (1 - resistance_tol)
value = 1 if (flat_resistance and rising_support and near_breakout) else 0
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, value, use_adjusted)
return value
def get_top_pattern(
code: str,
date: str,
period: int = 30,
tolerance: float = 0.03,
min_decline: float = 0.03,
use_adjusted: bool = True,
) -> Optional[int]:
"""顶部形态(Top Pattern)—— 双重顶等多K线顶部反转结构
以双重顶(Double Top)为核心,检测 period 根K线内的顶部反转结构:
· 在期间内找到至少 2 个局部高点;
· 取高度最高的两个局部高点,其价差 / 均值 <= tolerance(两顶高度相近);
· 两顶之间存在有意义的颈线回调:颈线最低价低于两顶均价的 min_decline 比例;
· 第二个高点出现在期间后半段;
· 当前收盘价已从第二个高点回落(< 第二高点 × (1 - tolerance)),确认顶部形成。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯K线根数,默认 30
tolerance: 两高点价格允许偏差比例 及 回落确认比例,默认 0.03(即 3%)
min_decline: 颈线相对两顶均价的最小回落比例,默认 0.03(即 3%)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日存在顶部形态,0 表示未出现;数据不足 period 根时返回 None
"""
cached = _get_cached_signal(code, 'TOP_PATTERN', period, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
highs = [k.high for k in klines]
lows = [k.low for k in klines]
closes = [k.close for k in klines]
# 找局部高点(至少需要 2 个)
peak_idxs = _find_local_highs(highs, window=3)
if len(peak_idxs) < 2:
_save_signal(code, 'TOP_PATTERN', period, date, 0, use_adjusted)
return 0
# 取高度最高的两个局部高点,按时间排序(idx1 更早,idx2 更晚)
sorted_by_height = sorted(peak_idxs, key=lambda i: highs[i], reverse=True)
idx1, idx2 = sorted(sorted_by_height[:2]) # 按时间先后排序
peak1 = highs[idx1]
peak2 = highs[idx2]
avg_peak = (peak1 + peak2) / 2.0
# 两顶高度相近
if avg_peak == 0 or abs(peak1 - peak2) / avg_peak > tolerance:
_save_signal(code, 'TOP_PATTERN', period, date, 0, use_adjusted)
return 0
# 两顶之间的颈线(最低价)
neckline = min(lows[idx1: idx2 + 1])
if (avg_peak - neckline) / avg_peak < min_decline:
_save_signal(code, 'TOP_PATTERN', period, date, 0, use_adjusted)
return 0
# 第二高点在后半段,且当前价格已从第二顶回落
half = period // 2
value = 1 if (
idx2 >= half
and closes[-1] < peak2 * (1 - tolerance)
) else 0
_save_signal(code, 'TOP_PATTERN', period, date, value, use_adjusted)
return value
# ── 别名函数 ─────────────────────────────────────────────────────────────────
def get_qiming_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""启明星(早晨之星,Morning Star)—— get_morning_star 的别名
与 get_morning_star 完全等价,共享同一缓存键(MORNING_STAR)。
Returns:
int: 1 表示当日出现启明星(早晨之星),0 表示未出现;数据不足 3 根时返回 None
"""
return get_morning_star(code, date, large_body_ratio, doji_ratio, penetrate_ratio, use_adjusted)
def get_huanghun_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""黄昏星(黄昏之星,Evening Star)—— get_evening_star 的别名
与 get_evening_star 完全等价,共享同一缓存键(EVENING_STAR)。
Returns:
int: 1 表示当日出现黄昏星(黄昏之星),0 表示未出现;数据不足 3 根时返回 None
"""
return get_evening_star(code, date, large_body_ratio, doji_ratio, penetrate_ratio, use_adjusted)
FILE:scripts/stock_api.py
"""
stock_api.py — 股票数据与回测API接口
策略逻辑可调用此模块中的所有函数来获取股票数据。
当前为模拟实现,真实环境中替换为实际数据源即可。
策略逻辑可调用此模块中的所有函数来获取股票数据和技术指标。
本模块是项目对外的唯一接口,其他模块的实现在内部4个独立文件中。
使用示例:
from stock_api import StockApi
api = StockApi()
# 获取日线行情表
klines = api.get_daily_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# 获取技术指标
sma = api.get_sma('600519.SH', '2026-03-01', 20)
rsi = api.get_rsi('600519.SH', '2026-03-01', 14)
# 获取性能指标
report = api.calculate_metrics([1000000, 1050000, 1020000], trades, 1000000, 30)
"""
import sys
from typing import Optional, List, Dict, Union
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from sqlalchemy import text
from realtime_data_featcher import (
RealtimeStockQuote,
RealTimeDataFetcher
)
from data_fetcher import (
query_stock_basic,
query_daily_kline,
query_hour_kline,
query_weekly_kline,
query_monthly_kline,
query_daily_basic,
query_income,
query_stock_limit,
query_daily_limit_list,
query_daily_bomb_list,
query_sector_stock_map,
query_top_list,
query_top_inst,
query_sector_flow_daily,
query_index_basic,
query_index_daily,
query_index_weekly,
query_index_monthly,
)
from define import (
DailyKline,
HourKline,
WeeklyKline,
MonthlyKline,
StockBasic,
DailyBasic,
Income,
StockLimit,
DailyLimitList,
DailyBombList,
SectorStockMap,
TopList,
TopInst,
SectorFlowDaily,
IndexBasic,
IndexDaily,
IndexWeekly,
IndexMonthly,
AppVersion,
TokenCheckResult
)
from data_fetcher import getEngine
from formulaicAlphas import AlphaDataLoader, Alpha101
from indicators import (
get_sma,
get_ema,
get_rsi,
get_bollinger_bands,
get_macd,
get_atr,
get_wma,
get_tema,
get_mom,
get_roc,
get_cci,
get_obv,
get_volume,
get_kdj,
get_dmi,
get_trix,
get_sar,
get_williams_r,
get_psycho,
get_bias,
get_tr,
get_natr,
get_vwap,
get_ad,
get_adosc,
get_mfi,
get_cmo,
get_rocp,
get_rocr,
get_aroon,
get_ultosc,
get_dema,
get_kama,
get_midpoint,
get_midprice,
get_pvi,
get_nvi,
get_ppo,
get_roc_r,
get_stoch,
get_stochf,
get_stochrsi,
get_trange,
get_ma_channel,
get_donchian,
get_keltner,
get_bbands_width,
get_bbands_pct,
get_linearreg,
get_linearreg_angle,
get_linearreg_intercept,
get_linearreg_slope,
get_stddev,
get_tsf,
get_var,
get_correl,
get_beta,
get_ht_dcperiod,
get_ht_dcphase,
get_ht_phasor,
get_ht_sine,
get_ht_trendmode,
get_typical_price,
get_median_price,
get_weighted_close,
get_avgp,
get_asi,
get_vr,
get_ar,
get_br,
get_brar,
get_dpo,
get_bbi,
get_mass,
get_xue_channel,
get_consecutive_rise,
get_consecutive_fall,
get_bomb_board,
get_bomb_board_count,
get_consecutive_limit_up,
init_indicators_db,
)
from signals import (
get_morning_star,
get_qiming_star,
get_evening_star,
get_huanghun_star,
get_three_white_soldiers,
get_three_black_crows,
get_dark_cloud_cover,
get_rounding_bottom,
get_ascending_triangle,
get_top_pattern,
init_signals_db,
)
from metrics import (
get_max_drawdown,
get_max_drawdown_pct,
get_annualized_return,
get_total_return,
get_sharpe_ratio,
get_win_rate,
get_profit_loss_ratio,
get_calmar_ratio,
get_volatility,
get_trade_stats,
generate_report,
)
from backtest_tools import (
Position,
simulate_trade,
calculate_trade_cost,
create_position,
update_position,
get_position_value,
get_position_profit,
calculate_portfolio_value,
get_portfolio_positions,
build_equity_curve,
calculate_daily_returns,
should_buy,
should_sell,
calculate_drawdown,
buy,
sell,
)
import data_fetcher, config
from track_logger import TrackLogger
class StockApi:
"""
股票数据与回测API接口
本类是项目对外提供的唯一接口,封装了以下功能:
- 股票基础信息查询
- K线数据获取
- 技术指标计算(带缓存)
- 性能指标计算
- 回测工具函数
"""
def __init__(self, logger:TrackLogger = None):
if logger is None:
import os as _os
_log_path = _os.path.join(config.get_cache_dir(), 'track.log')
logger = TrackLogger(_log_path)
self.track_logger = logger
# ============================================================
# 工具类
# ============================================================
@staticmethod
def get_user_token() -> str:
"""
获取用户当前token
返回值: 用户token(从环境变量 BITSOUL_TOKEN 或 BITSOUL_TOKEN_ENV_FILE 获取)
"""
return config.get_token()
@staticmethod
def set_user_token(token: str):
"""
设置用户当前token
参数:
token 设置的token
"""
return config.set_token(token=token)
# ============================================================
# 初始化
# ============================================================
def initialSetup(self):
self.track_logger.write("initialSetup()")
data_fetcher.init_db()
data_fetcher.syn_table_datas()
init_indicators_db()
init_signals_db()
def update_vip_basic_data(self):
"""
更新vip基础数据包
"""
data_fetcher.syn_vip_basic_data()
def update_data(self):
"""
更新本地数据库,获取最新的增量数据。
会对比服务器上的 patch 列表,下载并导入缺失的数据。
"""
self.track_logger.write("update_data()")
data_fetcher.syn_table_datas()
# ============================================================
# 股票基础信息类接口
# ============================================================
def get_all_symbols(self) -> List[str]:
"""
获取所有股票代码列表。
:return: 股票代码列表,格式如 ['000001.SZ', '600519.SH', ...]
"""
self.track_logger.write("get_all_symbols()")
stocks = query_stock_basic()
return [s.ts_code for s in stocks]
def get_symbol_basic_infomation(self, ts_code: str) -> Optional[StockBasic]:
"""
根据股票代码获取股票基础信息
:param ts_code: 股票代码,如 000001.SZ
:return: 股票基础信息数据结构,没查询到则返回None
"""
self.track_logger.write(f"get_symbol_basic_infomation(ts_code={ts_code!r})")
stocks = query_stock_basic(ts_code=ts_code)
if len(stocks) > 0:
return stocks[0]
else:
return None
# ─────────────────────────────────────────────
# 价格行情类接口
# ─────────────────────────────────────────────
def get_realtime_stock_info(self, code:str) -> RealtimeStockQuote:
"""
获取指定股票代码的股票实时信息
参数:
code 股票代码,如000001.SZ
返回:
RealtimeStockQuote 实时股票报价信息
"""
self.track_logger.write(f"get_realtime_stock_info(code={code!r})")
return RealTimeDataFetcher().request_stock_info(code)
def query_income(
self,
ts_codes: List[str] = [],
report_type: Optional[str] = None,
end_date: Optional[str] = None,
start_end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "end_date ASC",
) -> List[Income]:
"""
根据条件获取利润信息。
参数:
ts_codes 按股票代码列表过滤
report_type 按报告类型精确过滤(如 "1" 表示合并报表)
end_date 按报告期结束日期精确过滤,格式 YYYY-MM-DD
start_end_date 按报告期结束日期范围过滤下限(含),格式 YYYY-MM-DD
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "end_date ASC"
返回:
List[Income] 符合条件的利润表对象列表
示例:
# 查询某只股票全部利润表(合并报表)
records = query_income(ts_codes=["000001.SZ"], report_type="1")
# 查询某报告期全市场数据
records = query_income(end_date="20231231")
# 查询最新一期
records = query_income(ts_codes=["000001.SZ"], order_by="end_date DESC", limit=1)
"""
self.track_logger.write(f"query_income(ts_codes={ts_codes!r}, report_type={report_type!r}, end_date={end_date!r}, start_end_date={start_end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_income(
ts_codes=ts_codes,
report_type=report_type,
end_date=end_date,
start_end_date=start_end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_basic(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBasic]:
"""
查询每日基本面指标列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBasic] 符合条件的每日基本面指标对象列表
示例:
# 查询某只股票全部历史基本面数据
basics = query_daily_basic(ts_codes=["000001.SZ"])
# 查询某天全市场基本面数据
basics = query_daily_basic(trade_date="2024-06-03")
"""
self.track_logger.write(f"get_daily_basic(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_daily_basic(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_stock_limit(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[StockLimit]:
"""
查询每日涨跌停价格列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[StockLimit] 符合条件的每日涨跌停价格对象列表
示例:
# 查询某只股票的涨跌停价格历史
limits = api.get_stock_limit(ts_codes=["000001.SZ"])
# 查询某天全市场涨跌停价格
limits = api.get_stock_limit(trade_date="2024-06-03")
"""
self.track_logger.write(f"get_stock_limit(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_stock_limit(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_limit_list(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyLimitList]:
"""
查询每日涨跌停榜单列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit_type 按榜单类型过滤(U=涨停, D=跌停)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyLimitList] 符合条件的每日涨跌停榜单对象列表
示例:
# 查询某天所有涨停股
records = api.get_daily_limit_list(trade_date="2024-06-03", limit_type="U")
# 查询某只股票历史上榜记录
records = api.get_daily_limit_list(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_daily_limit_list(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit_type={limit_type!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_daily_limit_list(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit_type=limit_type,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_bomb_list(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
bomb_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBombList]:
"""
查询每日炸板榜单列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
bomb_type 按炸板类型过滤(U=曾涨停, D=曾跌停/撬板)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBombList] 符合条件的每日炸板榜单对象列表
示例:
# 查询某天所有炸板(曾涨停)股票
records = api.get_daily_bomb_list(trade_date="2024-06-03", bomb_type="U")
# 查询某只股票历史炸板记录
records = api.get_daily_bomb_list(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_daily_bomb_list(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, bomb_type={bomb_type!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_daily_bomb_list(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
bomb_type=bomb_type,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_sector_stock_map(
self,
sector_codes: List[str] = [],
stock_codes: List[str] = [],
source: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[SectorStockMap]:
"""
查询板块成分股映射列表
参数:
sector_codes 按板块代码列表过滤
stock_codes 按股票代码列表过滤
source 按数据来源精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询某个板块下的所有股票
records = api.get_sector_stock_map(sector_codes=["BK0475"])
# 查询某只股票归属的所有板块
records = api.get_sector_stock_map(stock_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_sector_stock_map(sector_codes={sector_codes!r}, stock_codes={stock_codes!r}, source={source!r}, limit={limit!r}, offset={offset!r})")
return query_sector_stock_map(
sector_codes=sector_codes,
stock_codes=stock_codes,
source=source,
limit=limit,
offset=offset,
)
def get_top_list(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopList]:
"""
查询龙虎榜每日明细列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天龙虎榜数据
records = api.get_top_list(trade_date="2024-06-03")
# 查询某只股票历史上榜记录
records = api.get_top_list(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_top_list(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_top_list(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_top_inst(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
side: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopInst]:
"""
查询龙虎榜机构交易明细列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
side 按买卖类型过滤("0"=买入最大的前5名, "1"=卖出最大的前5名)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天机构交易明细
records = api.get_top_inst(trade_date="2024-06-03")
# 查询某只股票历史机构上榜记录
records = api.get_top_inst(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_top_inst(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, side={side!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_top_inst(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
side=side,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_sector_flow_daily(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[SectorFlowDaily]:
"""
查询板块资金流向列表
参数:
ts_codes 按板块代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天所有板块资金流向
records = api.get_sector_flow_daily(trade_date="2024-06-03")
# 查询某个板块历史资金流向
records = api.get_sector_flow_daily(ts_codes=["BK0475"])
"""
self.track_logger.write(f"get_sector_flow_daily(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_sector_flow_daily(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_index_basic(
self,
ts_code: Optional[str] = None,
market: Optional[str] = None,
publisher: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[IndexBasic]:
"""
查询指数基础信息列表
参数:
ts_code 按指数代码精确过滤
market 按市场精确过滤
publisher 按发布方精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询所有指数
records = api.get_index_basic()
# 查询上证指数信息
records = api.get_index_basic(ts_code="000001.SH")
"""
self.track_logger.write(f"get_index_basic(ts_code={ts_code!r}, market={market!r}, publisher={publisher!r}, limit={limit!r}, offset={offset!r})")
return query_index_basic(
ts_code=ts_code,
market=market,
publisher=publisher,
limit=limit,
offset=offset,
)
def get_index_daily(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexDaily]:
"""
查询指数日线行情列表
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数历史日线
records = api.get_index_daily(ts_codes=["000001.SH"])
# 查询某天所有指数行情
records = api.get_index_daily(trade_date="2024-06-03")
"""
self.track_logger.write(f"get_index_daily(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_index_daily(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_index_weekly(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexWeekly]:
"""
查询指数周线行情列表
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数周线
records = api.get_index_weekly(ts_codes=["000001.SH"])
"""
self.track_logger.write(f"get_index_weekly(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_index_weekly(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_index_monthly(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexMonthly]:
"""
查询指数月线行情列表
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数月线
records = api.get_index_monthly(ts_codes=["000001.SH"])
"""
self.track_logger.write(f"get_index_monthly(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_index_monthly(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[DailyKline]:
"""
获取指定日期范围内的股票日线行情(按日期升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: 收盘价列表,无数据返回空列表
"""
self.track_logger.write(f"get_daily_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = query_daily_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC",
)
return klines
def get_hour_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[HourKline]:
"""
获取指定日期范围内的股票小时线行情(按日期和时间升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: HourKline 列表,无数据返回空列表
"""
self.track_logger.write(f"get_hour_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
return query_hour_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC, time ASC",
)
def get_weekly_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[WeeklyKline]:
"""
获取指定日期范围内的股票周线行情(按日期升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: WeeklyKline 列表,无数据返回空列表
"""
self.track_logger.write(f"get_weekly_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
return query_weekly_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC",
)
def get_monthly_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[MonthlyKline]:
"""
获取指定日期范围内的股票月线行情(按日期升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: MonthlyKline 列表,无数据返回空列表
"""
self.track_logger.write(f"get_monthly_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
return query_monthly_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC",
)
def get_daily_close_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线收盘价列表(按日期升序)。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
收盘价列表
Example:
prices = api.get_daily_close_prices('600519.SH', '2026-01-01', '2026-03-01')
"""
self.track_logger.write(f"get_daily_close_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.close for k in klines]
def get_daily_open_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线开盘价列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线开盘价列表
"""
self.track_logger.write(f"get_daily_open_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.open for k in klines]
def get_daily_high_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线最高价列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线最高价列表
"""
self.track_logger.write(f"get_daily_high_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.high for k in klines]
def get_daily_low_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线最低价列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
最低价列表
"""
self.track_logger.write(f"get_daily_low_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.low for k in klines]
def get_daily_volumes(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线成交量列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线成交量列表
"""
self.track_logger.write(f"get_daily_volumes(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.volume for k in klines]
def get_daily_pct_chg(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线涨跌幅列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线涨跌幅列表(%)
"""
self.track_logger.write(f"get_daily_pct_chg(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.pctChg for k in klines]
# ============================================================
# 技术指标类接口(带缓存)
# ============================================================
def get_sma(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取简单移动平均SMA。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 周期,默认20
Returns:
SMA值,若数据不足返回None
Example:
sma = api.get_sma('600519.SH', '2026-03-01', 20)
"""
self.track_logger.write(f"get_sma(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_sma(code, date, period, use_adjusted)
def get_ema(self, code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""
获取指数移动平均EMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认12
Returns:
EMA值,若数据不足返回None
"""
self.track_logger.write(f"get_ema(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ema(code, date, period, use_adjusted)
def get_rsi(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取相对强弱指标RSI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
RSI值(0-100),若数据不足返回None
Example:
rsi = api.get_rsi('600519.SH', '2026-03-01', 14)
if rsi and rsi < 30:
print('超卖')
"""
self.track_logger.write(f"get_rsi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rsi(code, date, period, use_adjusted)
def get_bollinger_bands(self, code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取布林带指标。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
std_dev: 标准差倍数,默认2
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
Example:
bb = api.get_bollinger_bands('600519.SH', '2026-03-01')
if bb and close > bb['upper']:
print('突破上轨')
"""
self.track_logger.write(f"get_bollinger_bands(code={code!r}, date={date!r}, period={period!r}, std_dev={std_dev!r}, use_adjusted={use_adjusted!r})")
return get_bollinger_bands(code, date, period, std_dev, use_adjusted)
def get_macd(self, code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取MACD指标。
Args:
code: 股票代码
date: 计算日期
fast: 快线周期,默认12
slow: 慢线周期,默认26
signal: 信号线周期,默认9
Returns:
字典 {'macd': MACD线, 'signal': 信号线, 'histogram': 柱状图},若数据不足返回None
Example:
macd = api.get_macd('600519.SH', '2026-03-01')
if macd and macd['histogram'] > 0:
print('多头')
"""
self.track_logger.write(f"get_macd(code={code!r}, date={date!r}, fast={fast!r}, slow={slow!r}, signal={signal!r}, use_adjusted={use_adjusted!r})")
return get_macd(code, date, fast, slow, signal, use_adjusted)
def get_atr(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取平均真实波幅ATR。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
ATR值,若数据不足返回None
"""
self.track_logger.write(f"get_atr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_atr(code, date, period, use_adjusted)
def get_wma(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取加权移动平均WMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
WMA值,若数据不足返回None
"""
self.track_logger.write(f"get_wma(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_wma(code, date, period, use_adjusted)
def get_tema(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取三重指数移动平均TEMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
TEMA值,若数据不足返回None
"""
self.track_logger.write(f"get_tema(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_tema(code, date, period, use_adjusted)
def get_mom(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取动量指标MOM。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
MOM值,若数据不足返回None
"""
self.track_logger.write(f"get_mom(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_mom(code, date, period, use_adjusted)
def get_roc(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取变动率指标ROC(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROC值(%),若数据不足返回None
"""
self.track_logger.write(f"get_roc(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_roc(code, date, period, use_adjusted)
def get_cci(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取顺势指标CCI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
CCI值,若数据不足返回None
"""
self.track_logger.write(f"get_cci(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_cci(code, date, period, use_adjusted)
def get_obv(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取能量潮OBV。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
OBV值,若数据不足返回None
"""
self.track_logger.write(f"get_obv(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_obv(code, date, period, use_adjusted)
def get_volume(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取成交量指标。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
字典 {'current': 当前成交量, 'sma': 成交量均线},若数据不足返回None
"""
self.track_logger.write(f"get_volume(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_volume(code, date, period, use_adjusted)
def get_kdj(self, code: str, date: str, n: int = 9, m1: int = 3, m2: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取随机指标KDJ。
Args:
code: 股票代码
date: 计算日期
n: 周期,默认9
m1: 平滑参数1,默认3
m2: 平滑参数2,默认3
Returns:
字典 {'k': K值, 'd': D值, 'j': J值},若数据不足返回None
"""
self.track_logger.write(f"get_kdj(code={code!r}, date={date!r}, n={n!r}, m1={m1!r}, m2={m2!r}, use_adjusted={use_adjusted!r})")
return get_kdj(code, date, n, m1, m2, use_adjusted)
def get_dmi(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取趋向指标DMI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
字典 {'pdi': +DI, 'mdi': -DI, 'adx': ADX},若数据不足返回None
"""
self.track_logger.write(f"get_dmi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_dmi(code, date, period, use_adjusted)
def get_trix(self, code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""
获取三重指数平滑移动平均TRIX(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认12
Returns:
TRIX值(%),若数据不足返回None
"""
self.track_logger.write(f"get_trix(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_trix(code, date, period, use_adjusted)
def get_sar(self, code: str, date: str, af_start: float = 0.02, af_max: float = 0.2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取抛物线转向SAR。
Args:
code: 股票代码
date: 计算日期
af_start: 加速因子起始值,默认0.02
af_max: 加速因子最大值,默认0.2
Returns:
字典 {'sar': SAR值, 'trend': 趋势},若数据不足返回None
"""
self.track_logger.write(f"get_sar(code={code!r}, date={date!r}, af_start={af_start!r}, af_max={af_max!r}, use_adjusted={use_adjusted!r})")
return get_sar(code, date, af_start, af_max, use_adjusted)
def get_williams_r(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取威廉指标WR(0-100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
WR值(0-100),0表示超买,100表示超卖,若数据不足返回None
"""
self.track_logger.write(f"get_williams_r(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_williams_r(code, date, period, use_adjusted)
def get_psycho(self, code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""
获取心理线PSY(0-100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认12
Returns:
PSY值(0-100),若数据不足返回None
"""
self.track_logger.write(f"get_psycho(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_psycho(code, date, period, use_adjusted)
def get_bias(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取乖离率BIAS(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
BIAS值(%),若数据不足返回None
"""
self.track_logger.write(f"get_bias(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_bias(code, date, period, use_adjusted)
def get_tr(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取真实波幅TR。
Args:
code: 股票代码
date: 计算日期
Returns:
TR值,若数据不足返回None
"""
self.track_logger.write(f"get_tr(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_tr(code, date, use_adjusted)
def get_natr(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取归一化平均真实波幅NATR(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
NATR值(%),若数据不足返回None
"""
self.track_logger.write(f"get_natr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_natr(code, date, period, use_adjusted)
def get_vwap(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取成交量加权平均价VWAP。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
VWAP值,若数据不足返回None
"""
self.track_logger.write(f"get_vwap(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_vwap(code, date, period, use_adjusted)
def get_ad(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取累积/派发线AD。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
AD值,若数据不足返回None
"""
self.track_logger.write(f"get_ad(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ad(code, date, period, use_adjusted)
def get_adosc(self, code: str, date: str, fast: int = 3, slow: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取震荡指标ADOSC。
Args:
code: 股票代码
date: 计算日期
fast: 快线周期,默认3
slow: 慢线周期,默认10
Returns:
ADOSC值,若数据不足返回None
"""
self.track_logger.write(f"get_adosc(code={code!r}, date={date!r}, fast={fast!r}, slow={slow!r}, use_adjusted={use_adjusted!r})")
return get_adosc(code, date, fast, slow, use_adjusted)
def get_mfi(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取资金流量指标MFI(0-100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
MFI值(0-100),若数据不足返回None
"""
self.track_logger.write(f"get_mfi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_mfi(code, date, period, use_adjusted)
def get_cmo(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取钱德动量摆动指标CMO(-100 to 100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
CMO值(-100 to 100),若数据不足返回None
"""
self.track_logger.write(f"get_cmo(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_cmo(code, date, period, use_adjusted)
def get_rocp(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取价格变动率ROCP。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROCP值,若数据不足返回None
"""
self.track_logger.write(f"get_rocp(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rocp(code, date, period, use_adjusted)
def get_rocr(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取价格变动率比ROCR。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROCR值,若数据不足返回None
"""
self.track_logger.write(f"get_rocr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rocr(code, date, period, use_adjusted)
def get_aroon(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取阿隆指标AROON。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
字典 {'up': AROON_UP, 'down': AROON_DOWN, 'osc': AROON_OSC},若数据不足返回None
"""
self.track_logger.write(f"get_aroon(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_aroon(code, date, period, use_adjusted)
def get_ultosc(self, code: str, date: str, period1: int = 7, period2: int = 14, period3: int = 28, use_adjusted: bool = True) -> Optional[float]:
"""
获取终极振荡器ULTOSC(0-100)。
Args:
code: 股票代码
date: 计算日期
period1: 周期1,默认7
period2: 周期2,默认14
period3: 周期3,默认28
Returns:
ULTOSC值(0-100),若数据不足返回None
"""
self.track_logger.write(f"get_ultosc(code={code!r}, date={date!r}, period1={period1!r}, period2={period2!r}, period3={period3!r}, use_adjusted={use_adjusted!r})")
return get_ultosc(code, date, period1, period2, period3, use_adjusted)
def get_dema(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取双重指数移动平均DEMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
DEMA值,若数据不足返回None
"""
self.track_logger.write(f"get_dema(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_dema(code, date, period, use_adjusted)
def get_kama(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取考夫曼自适应移动平均KAMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
KAMA值,若数据不足返回None
"""
self.track_logger.write(f"get_kama(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_kama(code, date, period, use_adjusted)
def get_midpoint(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取中点价格MIDPOINT。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
MIDPOINT值,若数据不足返回None
"""
self.track_logger.write(f"get_midpoint(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_midpoint(code, date, period, use_adjusted)
def get_midprice(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取中点价格MIDPRICE。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
MIDPRICE值,若数据不足返回None
"""
self.track_logger.write(f"get_midprice(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_midprice(code, date, period, use_adjusted)
def get_pvi(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取正成交量指标PVI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
PVI值,若数据不足返回None
"""
self.track_logger.write(f"get_pvi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_pvi(code, date, period, use_adjusted)
def get_nvi(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取负成交量指标NVI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
NVI值,若数据不足返回None
"""
self.track_logger.write(f"get_nvi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_nvi(code, date, period, use_adjusted)
def get_ppo(self, code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取价格震荡指标PPO。
Args:
code: 股票代码
date: 计算日期
fast: 快线周期,默认12
slow: 慢线周期,默认26
signal: 信号线周期,默认9
Returns:
字典 {'ppo': PPO线, 'signal': 信号线, 'histogram': 柱状图},若数据不足返回None
"""
self.track_logger.write(f"get_ppo(code={code!r}, date={date!r}, fast={fast!r}, slow={slow!r}, signal={signal!r}, use_adjusted={use_adjusted!r})")
return get_ppo(code, date, fast, slow, signal, use_adjusted)
def get_roc_r(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取变动率ROC_R。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROC_R值,若数据不足返回None
"""
self.track_logger.write(f"get_roc_r(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_roc_r(code, date, period, use_adjusted)
def get_stoch(self, code: str, date: str, fastk_period: int = 14, slowk_period: int = 3, slowd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取随机指标STOCH。
Args:
code: 股票代码
date: 计算日期
fastk_period: 快速K周期,默认14
slowk_period: 慢速K周期,默认3
slowd_period: 慢速D周期,默认3
Returns:
字典 {'slowk': 慢速K, 'slowd': 慢速D},若数据不足返回None
"""
self.track_logger.write(f"get_stoch(code={code!r}, date={date!r}, fastk_period={fastk_period!r}, slowk_period={slowk_period!r}, slowd_period={slowd_period!r}, use_adjusted={use_adjusted!r})")
return get_stoch(code, date, fastk_period, slowk_period, slowd_period, use_adjusted)
def get_stochf(self, code: str, date: str, fastk_period: int = 14, fastd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取快速随机指标STOCHF。
Args:
code: 股票代码
date: 计算日期
fastk_period: 快速K周期,默认14
fastd_period: 快速D周期,默认3
Returns:
字典 {'fastk': 快速K, 'fastd': 快速D},若数据不足返回None
"""
self.track_logger.write(f"get_stochf(code={code!r}, date={date!r}, fastk_period={fastk_period!r}, fastd_period={fastd_period!r}, use_adjusted={use_adjusted!r})")
return get_stochf(code, date, fastk_period, fastd_period, use_adjusted)
def get_stochrsi(self, code: str, date: str, rsi_period: int = 14, stoch_period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取随机RSI指标STOCHRSI。
Args:
code: 股票代码
date: 计算日期
rsi_period: RSI周期,默认14
stoch_period: 随机周期,默认14
Returns:
字典 {'fastk': K, 'fastd': D},若数据不足返回None
"""
self.track_logger.write(f"get_stochrsi(code={code!r}, date={date!r}, rsi_period={rsi_period!r}, stoch_period={stoch_period!r}, use_adjusted={use_adjusted!r})")
return get_stochrsi(code, date, rsi_period, stoch_period, use_adjusted)
def get_trange(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取真实波幅TRANGE。
Args:
code: 股票代码
date: 计算日期
Returns:
TRANGE值,若数据不足返回None
"""
self.track_logger.write(f"get_trange(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_trange(code, date, use_adjusted)
def get_ma_channel(self, code: str, date: str, period: int = 20, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取移动平均通道。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
multiplier: 倍数,默认2.0
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
"""
self.track_logger.write(f"get_ma_channel(code={code!r}, date={date!r}, period={period!r}, multiplier={multiplier!r}, use_adjusted={use_adjusted!r})")
return get_ma_channel(code, date, period, multiplier, use_adjusted)
def get_donchian(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取唐奇安通道。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
"""
self.track_logger.write(f"get_donchian(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_donchian(code, date, period, use_adjusted)
def get_keltner(self, code: str, date: str, ma_period: int = 20, atr_period: int = 10, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取凯尔特纳通道。
Args:
code: 股票代码
date: 计算日期
ma_period: MA周期,默认20
atr_period: ATR周期,默认10
multiplier: 倍数,默认2.0
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
"""
self.track_logger.write(f"get_keltner(code={code!r}, date={date!r}, ma_period={ma_period!r}, atr_period={atr_period!r}, multiplier={multiplier!r}, use_adjusted={use_adjusted!r})")
return get_keltner(code, date, ma_period, atr_period, multiplier, use_adjusted)
def get_bbands_width(self, code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""
获取布林带宽度BBANDS_WIDTH(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
std_dev: 标准差倍数,默认2
Returns:
BBANDS_WIDTH值(%),若数据不足返回None
"""
self.track_logger.write(f"get_bbands_width(code={code!r}, date={date!r}, period={period!r}, std_dev={std_dev!r}, use_adjusted={use_adjusted!r})")
return get_bbands_width(code, date, period, std_dev, use_adjusted)
def get_bbands_pct(self, code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""
获取布林带百分比位置BBANDS_PCT(0-1)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
std_dev: 标准差倍数,默认2
Returns:
BBANDS_PCT值(0-1),若数据不足返回None
"""
self.track_logger.write(f"get_bbands_pct(code={code!r}, date={date!r}, period={period!r}, std_dev={std_dev!r}, use_adjusted={use_adjusted!r})")
return get_bbands_pct(code, date, period, std_dev, use_adjusted)
def get_linearreg(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归预测值LINEARREG。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg(code, date, period, use_adjusted)
def get_linearreg_angle(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归角度LINEARREG_ANGLE。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG_ANGLE值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg_angle(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg_angle(code, date, period, use_adjusted)
def get_linearreg_intercept(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归截距LINEARREG_INTERCEPT。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG_INTERCEPT值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg_intercept(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg_intercept(code, date, period, use_adjusted)
def get_linearreg_slope(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归斜率LINEARREG_SLOPE。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG_SLOPE值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg_slope(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg_slope(code, date, period, use_adjusted)
def get_stddev(self, code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""
获取标准差STDDEV。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
nbdev: 标准差倍数,默认1
Returns:
STDDEV值,若数据不足返回None
"""
self.track_logger.write(f"get_stddev(code={code!r}, date={date!r}, period={period!r}, nbdev={nbdev!r}, use_adjusted={use_adjusted!r})")
return get_stddev(code, date, period, nbdev, use_adjusted)
def get_tsf(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取时间序列预测TSF。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
TSF值,若数据不足返回None
"""
self.track_logger.write(f"get_tsf(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_tsf(code, date, period, use_adjusted)
def get_var(self, code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""
获取方差VAR。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
nbdev: 倍数,默认1
Returns:
VAR值,若数据不足返回None
"""
self.track_logger.write(f"get_var(code={code!r}, date={date!r}, period={period!r}, nbdev={nbdev!r}, use_adjusted={use_adjusted!r})")
return get_var(code, date, period, nbdev, use_adjusted)
def get_correl(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取相关系数CORREL(固定返回1.0)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
CORREL值(固定1.0)
"""
self.track_logger.write(f"get_correl(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_correl(code, date, period, use_adjusted)
def get_beta(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取贝塔系数BETA(固定返回1.0)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
BETA值(固定1.0)
"""
self.track_logger.write(f"get_beta(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_beta(code, date, period, use_adjusted)
def get_ht_dcperiod(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取希尔伯特变换-主导周期HT_DCPERIOD。
Args:
code: 股票代码
date: 计算日期
Returns:
HT_DCPERIOD值,若数据不足返回None
"""
self.track_logger.write(f"get_ht_dcperiod(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_dcperiod(code, date, use_adjusted)
def get_ht_dcphase(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取希尔伯特变换-主导相位HT_DCPHASE。
Args:
code: 股票代码
date: 计算日期
Returns:
HT_DCPHASE值,若数据不足返回None
"""
self.track_logger.write(f"get_ht_dcphase(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_dcphase(code, date, use_adjusted)
def get_ht_phasor(self, code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取希尔伯特变换-相位分量HT_PHASOR。
Args:
code: 股票代码
date: 计算日期
Returns:
字典 {'inphase': 同相, 'quadrature': 正交},若数据不足返回None
"""
self.track_logger.write(f"get_ht_phasor(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_phasor(code, date, use_adjusted)
def get_ht_sine(self, code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取希尔伯特变换-正弦波HT_SINE。
Args:
code: 股票代码
date: 计算日期
Returns:
字典 {'sine': 正弦, 'leadsine': 超前正弦},若数据不足返回None
"""
self.track_logger.write(f"get_ht_sine(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_sine(code, date, use_adjusted)
def get_ht_trendmode(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
获取希尔伯特变换-趋势模式HT_TRENDMODE。
Args:
code: 股票代码
date: 计算日期
Returns:
1=趋势, 0=周期,若数据不足返回None
"""
self.track_logger.write(f"get_ht_trendmode(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_trendmode(code, date, use_adjusted)
def get_typical_price(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取典型价格TP = (High + Low + Close) / 3。
Args:
code: 股票代码
date: 计算日期
Returns:
典型价格,若数据不足返回None
"""
self.track_logger.write(f"get_typical_price(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_typical_price(code, date, use_adjusted)
def get_median_price(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取中位数价格 = (High + Low) / 2。
Args:
code: 股票代码
date: 计算日期
Returns:
中位数价格,若数据不足返回None
"""
self.track_logger.write(f"get_median_price(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_median_price(code, date, use_adjusted)
def get_weighted_close(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取加权收盘价 = (High + Low + 2 * Close) / 4。
Args:
code: 股票代码
date: 计算日期
Returns:
加权收盘价,若数据不足返回None
"""
self.track_logger.write(f"get_weighted_close(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_weighted_close(code, date, use_adjusted)
def get_avgp(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取平均价格 = (Open + High + Low + Close) / 4。
Args:
code: 股票代码
date: 计算日期
Returns:
平均价格,若数据不足返回None
"""
self.track_logger.write(f"get_avgp(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_avgp(code, date, use_adjusted)
def get_asi(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取累积摆动指数 ASI(Accumulative Swing Index)。
ASI 基于开高低收四价构造,用于衡量价格摆动的累积强度,
值域无固定范围,正值表示多头动能积累,负值表示空头动能积累。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 历史K线根数,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
ASI值(float),数据不足时返回None
Example:
asi = api.get_asi('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_asi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_asi(code, date, period, use_adjusted)
def get_vr(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取成交量比率指标 VR(Volume Ratio)。
VR = (上涨日成交量之和 + 0.5 * 平盘日成交量) / (下跌日成交量之和 + 0.5 * 平盘日成交量)。
VR > 1 表示量价配合偏多,VR < 1 表示量价配合偏空,正常区间约 0.5 ~ 1.5。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
VR值(float),数据不足时返回None
Example:
vr = api.get_vr('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_vr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_vr(code, date, period, use_adjusted)
def get_ar(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取人气指标 AR(Atmosphere/Rally)。
AR = sum(High - Open) / sum(Open - Low) * 100,衡量多空双方相对强弱,
100 为均衡,> 100 多头占优,< 100 空头占优,一般正常范围 50 ~ 150。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
AR值(float),数据不足时返回None
Example:
ar = api.get_ar('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_ar(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ar(code, date, period, use_adjusted)
def get_br(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取意愿指标 BR(Buyer/Seller Ratio)。
BR = sum(High - Close_prev) / sum(Close_prev - Low) * 100,衡量多空力量对比,
与 AR 配合使用:BR > AR 多头强势,BR < AR 空头强势。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
BR值(float),数据不足时返回None
Example:
br = api.get_br('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_br(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_br(code, date, period, use_adjusted)
def get_brar(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
同时获取人气指标 AR 和意愿指标 BR。
BRAR 是 AR 与 BR 的组合指标,二者结合判断市场多空力量:
- AR 衡量当日多空(基于开盘价)
- BR 衡量跨日多空(基于前收价)
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
字典 {'ar': float, 'br': float},数据不足时返回None
Example:
brar = api.get_brar('600519.SH', '2026-03-01', 26)
ar, br = brar['ar'], brar['br']
"""
self.track_logger.write(f"get_brar(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_brar(code, date, period, use_adjusted)
def get_dpo(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取去趋势震荡指标 DPO(Detrended Price Oscillator)。
DPO = Close - SMA(Close, period)[-(period/2 + 1)],通过去除长期趋势
来识别价格的短中期周期性波动,穿越零轴可作为买卖参考信号。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
DPO值(float),数据不足时返回None
Example:
dpo = api.get_dpo('600519.SH', '2026-03-01', 20)
"""
self.track_logger.write(f"get_dpo(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_dpo(code, date, period, use_adjusted)
def get_bbi(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取多空指标 BBI(Bull and Bear Index)。
BBI = (MA3 + MA6 + MA12 + MA24) / 4,是四条均线的算术平均值,
价格上穿 BBI 为多头信号,下穿为空头信号。固定使用 3/6/12/24 周期。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
BBI值(float),数据不足时返回None
Example:
bbi = api.get_bbi('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_bbi(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_bbi(code, date, use_adjusted)
def get_mass(self, code: str, date: str, period: int = 25, use_adjusted: bool = True) -> Optional[float]:
"""
获取梅斯线 MASS Index(Mass Index)。
MASS = sum(EMA(H-L, p) / EMA(EMA(H-L, p), p), period),通过高低价差
的双重 EMA 比值累加来识别价格反转,值升破 27 后回落至 26.5 以下为
"反转鼓"信号。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
ema_period: EMA平滑周期,默认9
period: 累积周期,默认25
use_adjusted: 是否使用后复权价格,默认True
Returns:
MASS值(float),数据不足时返回None
Example:
mass = api.get_mass('600519.SH', '2026-03-01', 9, 25)
"""
self.track_logger.write(f"get_mass(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_mass(code, date, period, use_adjusted)
def get_xue_channel(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取雪球通道(薛斯通道)。
雪球通道由中轨(MA)、上轨(MA + k*ATR)、下轨(MA - k*ATR)构成,
价格突破上轨为超买,跌破下轨为超卖,常用于趋势跟踪和止损设置。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 均线和ATR周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
字典 {'upper': float, 'middle': float, 'lower': float},数据不足时返回None
Example:
ch = api.get_xue_channel('600519.SH', '2026-03-01', 20)
upper, middle, lower = ch['upper'], ch['middle'], ch['lower']
"""
self.track_logger.write(f"get_xue_channel(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_xue_channel(code, date, period, use_adjusted)
def get_consecutive_rise(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
获取截至指定日期连续上涨的天数。
从指定日期向前追溯,统计收盘价连续高于前一日的天数,
0 表示当日未上涨,1 表示仅当日上涨,依此类推。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
连续上涨天数(int ≥ 0),数据不足时返回None
Example:
n = api.get_consecutive_rise('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_consecutive_rise(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_consecutive_rise(code, date, use_adjusted)
def get_consecutive_fall(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
获取截至指定日期连续下跌的天数。
从指定日期向前追溯,统计收盘价连续低于前一日的天数,
0 表示当日未下跌,1 表示仅当日下跌,依此类推。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
连续下跌天数(int ≥ 0),数据不足时返回None
Example:
n = api.get_consecutive_fall('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_consecutive_fall(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_consecutive_fall(code, date, use_adjusted)
def get_bomb_board(self, code: str, date: str) -> Optional[int]:
"""
判断指定日期是否发生炸板(曾涨停但收盘未封板)。
炸板意味着多头动能不足,当日虽冲击涨停但尾盘筹码松动,
可作为规避追高或观察多空博弈的参考信号。
不使用复权价格——炸板基于市场实际涨停价判断。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
Returns:
1=当日炸板,0=当日未炸板,None=非交易日或数据缺失
Example:
is_bomb = api.get_bomb_board('000001.SZ', '2026-03-01')
"""
self.track_logger.write(f"get_bomb_board(code={code!r}, date={date!r})")
return get_bomb_board(code, date)
def get_bomb_board_count(self, code: str, date: str, period: int = 20) -> Optional[int]:
"""
统计近N个交易日内的炸板次数。
炸板频繁说明个股多次冲板失败,多头信心不足;高频炸板的股票追高风险较大,
可结合连板数综合评估封板质量。
不使用复权价格——炸板基于市场实际涨停价判断。
Args:
code: 股票代码
date: 统计截止日期,格式 YYYY-MM-DD
period: 回溯交易日天数,默认20
Returns:
近period个交易日内炸板次数(int ≥ 0),数据不足时返回None
Example:
cnt = api.get_bomb_board_count('000001.SZ', '2026-03-01', 20)
"""
self.track_logger.write(f"get_bomb_board_count(code={code!r}, date={date!r}, period={period!r})")
return get_bomb_board_count(code, date, period)
def get_consecutive_limit_up(self, code: str, date: str) -> Optional[int]:
"""
获取截至指定日期的连续涨停天数(连板数)。
直接读取数据源维护的 limit_streak 字段,含一字板等特殊情形,
比自行计算更准确。连板数 >= 3 通常视为强势股,高连板存在高位风险。
不使用复权价格——涨停判断基于市场实际价格。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
Returns:
连板数(int ≥ 0,0表示当日未涨停),非交易日或数据缺失返回None
Example:
streak = api.get_consecutive_limit_up('000001.SZ', '2026-03-01')
"""
self.track_logger.write(f"get_consecutive_limit_up(code={code!r}, date={date!r})")
return get_consecutive_limit_up(code, date)
# ============================================================
# 裸K形态信号类接口(带缓存)
# ============================================================
def get_morning_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测早晨之星(Morning Star)形态。
早晨之星是底部反转信号,由三根K线构成:第一根大阴线、第二根十字星
(开低收于阴线实体下方)、第三根大阳线并收回阴线实体一半以上。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_morning_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_morning_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_morning_star(code, date, use_adjusted)
def get_qiming_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测启明星形态(早晨之星别名)。
启明星即早晨之星(Morning Star),共享同一缓存键 MORNING_STAR,
结果与 get_morning_star 完全一致。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_qiming_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_qiming_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_qiming_star(code, date, use_adjusted)
def get_evening_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测黄昏之星(Evening Star)形态。
黄昏之星是顶部反转信号,与早晨之星相反:第一根大阳线、第二根十字星
(开高收于阳线实体上方)、第三根大阴线并收回阳线实体一半以上。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_evening_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_evening_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_evening_star(code, date, use_adjusted)
def get_huanghun_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测黄昏星形态(黄昏之星别名)。
黄昏星即黄昏之星(Evening Star),共享同一缓存键 EVENING_STAR,
结果与 get_evening_star 完全一致。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_huanghun_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_huanghun_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_huanghun_star(code, date, use_adjusted)
def get_three_white_soldiers(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测红三兵(Three White Soldiers)形态。
红三兵是强势上涨信号,由连续三根阳线构成,每根实体占比≥50%,
上影线≤20%,且每根K线的开盘价在前一根实体范围内(逐步跳空上行)。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_three_white_soldiers('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_three_white_soldiers(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_three_white_soldiers(code, date, use_adjusted)
def get_three_black_crows(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测三只乌鸦(Three Black Crows)形态。
三只乌鸦是强势下跌信号,由连续三根阴线构成,每根实体占比≥50%,
下影线≤20%,且每根K线的开盘价在前一根实体范围内(逐步跳空下行)。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_three_black_crows('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_three_black_crows(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_three_black_crows(code, date, use_adjusted)
def get_dark_cloud_cover(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测乌云盖顶(Dark Cloud Cover)形态。
乌云盖顶是顶部反转信号,由两根K线构成:第一根大阳线,第二根阴线
高开(开盘高于前收)后下跌,收盘深入阳线实体一半以上但不低于阳线开盘。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第二根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_dark_cloud_cover('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_dark_cloud_cover(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_dark_cloud_cover(code, date, use_adjusted)
def get_rounding_bottom(self, code: str, date: str, period: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""
检测圆弧底(Rounding Bottom / Saucer)形态。
圆弧底是长期底部反转形态,价格在 period 根K线内呈现 U 形走势:
左侧缓慢下跌,底部盘整,右侧缓慢回升,最低点出现在中间三分之一区段。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
period: 观察窗口(K线根数),默认60
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_rounding_bottom('600519.SH', '2026-03-01', 60)
"""
self.track_logger.write(f"get_rounding_bottom(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rounding_bottom(code, date, period, use_adjusted)
def get_ascending_triangle(self, code: str, date: str, period: int = 30, use_adjusted: bool = True) -> Optional[int]:
"""
检测上升三角形(Ascending Triangle)形态。
上升三角形是整理后向上突破的形态:水平阻力位保持不变,
支撑位持续上移(低点逐步抬高),是多头蓄力信号。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
period: 观察窗口(K线根数),默认30
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_ascending_triangle('600519.SH', '2026-03-01', 30)
"""
self.track_logger.write(f"get_ascending_triangle(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ascending_triangle(code, date, period, use_adjusted)
def get_top_pattern(self, code: str, date: str, period: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""
检测顶部形态(双顶 / M头)。
顶部形态由两个相近高点和中间颈线构成:两个高点高度接近(误差在容忍范围内),
颈线低点跌幅达到一定比例,当前价格已从第二高点回落,确认顶部。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
period: 观察窗口(K线根数),默认60
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_top_pattern('600519.SH', '2026-03-01', 60)
"""
self.track_logger.write(f"get_top_pattern(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_top_pattern(code, date, period, use_adjusted)
# ============================================================
# 复合筛选与分析类接口
# ============================================================
def get_stocks_by_industry_keyword(
self,
keyword: str,
market: Optional[str] = None,
limit: Optional[int] = None,
) -> List[StockBasic]:
"""
按行业关键词模糊搜索股票(industry LIKE %keyword%)。
支持不精确的行业名称,如"半导体"可匹配"半导体及元件"、"集成电路"等。
可叠加 market 过滤(主板/创业板/科创板)。
Args:
keyword: 行业关键词,如"半导体"、"芯片"、"银行"、"新能源"
market: 市场类型过滤,如"主板"、"创业板"、"科创板",None 表示不限
limit: 最大返回数量,None 表示不限
Returns:
匹配的 StockBasic 列表
Example:
chip = api.get_stocks_by_industry_keyword('半导体')
kechuang = api.get_stocks_by_industry_keyword('半导体', market='科创板')
"""
self.track_logger.write(f"get_stocks_by_industry_keyword(keyword={keyword!r}, market={market!r}, limit={limit!r})")
return query_stock_basic(industry_keyword=keyword, market=market, limit=limit)
def get_latest_income(self, code: str) -> Optional[Income]:
"""
获取股票最新一期财务数据(合并报表)。
从 income 表取 end_date 最新的一条合并报表记录,
包含 ROE、ROA、毛利率、净利率、净利润增速等常用财务指标。
Args:
code: 股票代码,如 '000001.SZ'
Returns:
最新一期 Income 对象,无数据返回 None
Example:
inc = api.get_latest_income('600519.SH')
print(f"ROE={inc.roe:.2f}% 毛利率={inc.gross_margin:.2f}%")
"""
self.track_logger.write(f"get_latest_income(code={code!r})")
records = query_income(ts_codes=[code], report_type="1",
order_by="end_date DESC", limit=1)
return records[0] if records else None
def filter_stocks_by_fundamentals(
self,
date: str,
pe_ttm_max: Optional[float] = None,
roe_min: Optional[float] = None,
mv_min_yi: Optional[float] = None,
top_n: Optional[int] = None,
order_by: str = "total_mv DESC",
) -> List[Dict]:
"""
按基本面条件多维筛选股票,支持 PE/ROE/市值三重过滤。
跨 daily_basic(PE、市值)与 income(ROE)两张表联合筛选,
全部过滤在 Python 侧完成,结果按指定字段排序后返回。
pe_ttm <= 0(亏损股)始终被排除在外。
Args:
date: 基本面数据日期,格式 YYYY-MM-DD(取当日 daily_basic 快照)
pe_ttm_max: PE(TTM) 上限(不含),None 表示不过滤
roe_min: ROE 下限(%,不含),None 表示不过滤
mv_min_yi: 总市值下限(亿元),None 表示不过滤
top_n: 最终返回数量,None 表示全部
order_by: 排序字段,可选 'total_mv DESC'/'pe_ttm ASC'/'roe DESC' 等
Returns:
字典列表,每项含 ts_code / pe_ttm / total_mv_yi / roe(亿元,roe 仅在
roe_min 不为 None 时出现),按 order_by 排序后取前 top_n 条
Example:
results = api.filter_stocks_by_fundamentals(
'2026-03-14', pe_ttm_max=20, roe_min=15, mv_min_yi=100, top_n=20
)
for r in results:
print(r['ts_code'], r['pe_ttm'], r['total_mv_yi'], r.get('roe'))
"""
self.track_logger.write(f"filter_stocks_by_fundamentals(date={date!r}, pe_ttm_max={pe_ttm_max!r}, roe_min={roe_min!r}, mv_min_yi={mv_min_yi!r}, top_n={top_n!r}, order_by={order_by!r})")
# ── Step 1: 全市场当日基本面快照 ───────────────────────────────────
basics = query_daily_basic(trade_date=date)
if not basics:
return []
# ── Step 2: PE + 市值过滤(数据来源 daily_basic)───────────────────
mv_min_wan = (mv_min_yi * 10000) if mv_min_yi is not None else None
candidates = []
for b in basics:
if b.pe_ttm <= 0: # 亏损股排除
continue
if pe_ttm_max is not None and b.pe_ttm > pe_ttm_max:
continue
if mv_min_wan is not None and b.total_mv < mv_min_wan:
continue
candidates.append(b)
# ── Step 3: ROE 过滤(数据来源 income,批量查询后 Python 聚合)─────
if roe_min is not None and candidates:
codes = [b.ts_code for b in candidates]
all_income = query_income(ts_codes=codes, report_type="1")
# 每只股票取 end_date 最新的一期
latest: Dict[str, Income] = {}
for inc in sorted(all_income, key=lambda x: x.end_date):
latest[inc.ts_code] = inc
result = []
for b in candidates:
inc = latest.get(b.ts_code)
if inc is None or inc.roe < roe_min:
continue
result.append({
'ts_code': b.ts_code,
'pe_ttm': b.pe_ttm,
'total_mv_yi': round(b.total_mv / 10000, 2),
'roe': inc.roe,
})
else:
result = [{
'ts_code': b.ts_code,
'pe_ttm': b.pe_ttm,
'total_mv_yi': round(b.total_mv / 10000, 2),
} for b in candidates]
# ── Step 4: 排序 + 截取 ────────────────────────────────────────────
field_map = {
'total_mv': 'total_mv_yi',
'total_mv_yi': 'total_mv_yi',
'pe_ttm': 'pe_ttm',
'roe': 'roe',
}
parts = order_by.strip().split()
sort_key = field_map.get(parts[0].lower(), 'total_mv_yi')
descending = len(parts) < 2 or parts[1].upper() == 'DESC'
result.sort(key=lambda x: x.get(sort_key) or 0, reverse=descending)
return result[:top_n] if top_n is not None else result
def scan_all_signals(
self,
signal_type: str,
date: str,
period: int = 0,
use_adjusted: bool = True,
codes: Optional[List[str]] = None,
) -> Dict[str, int]:
"""
全市场(或指定股票池)扫描指定裸K形态信号。
对每只股票调用对应信号函数,返回所有触发信号(值=1)的股票代码→信号值字典。
结果已缓存到 cached_signals 表,重复扫描同一日期无额外计算开销。
Args:
signal_type: 信号类型,不区分大小写,可选值:
MORNING_STAR / QIMING_STAR / EVENING_STAR / HUANGHUN_STAR /
THREE_WHITE_SOLDIERS / THREE_BLACK_CROWS / DARK_CLOUD_COVER /
ROUNDING_BOTTOM / ASCENDING_TRIANGLE / TOP_PATTERN
date: 扫描日期,格式 YYYY-MM-DD
period: 窗口周期(仅对 ROUNDING_BOTTOM/ASCENDING_TRIANGLE/TOP_PATTERN 有效),
0 表示使用各信号默认值(60/30/60)
use_adjusted: 是否使用后复权价格,默认 True
codes: 待扫描股票代码列表,None 表示全市场扫描
Returns:
Dict[str, int]:{股票代码: 1},仅包含信号值为 1 的股票;
若某股票数据不足(返回 None)则不计入结果
Example:
hits = api.scan_all_signals('EVENING_STAR', '2026-03-14')
print(f"黄昏之星: {list(hits.keys())}")
hits = api.scan_all_signals('ROUNDING_BOTTOM', '2026-03-14', period=60)
"""
self.track_logger.write(f"scan_all_signals(signal_type={signal_type!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r}, codes={codes!r})")
_DISPATCH: Dict[str, any] = {
'MORNING_STAR': lambda c: get_morning_star(c, date, use_adjusted),
'QIMING_STAR': lambda c: get_qiming_star(c, date, use_adjusted),
'EVENING_STAR': lambda c: get_evening_star(c, date, use_adjusted),
'HUANGHUN_STAR': lambda c: get_huanghun_star(c, date, use_adjusted),
'THREE_WHITE_SOLDIERS':lambda c: get_three_white_soldiers(c, date, use_adjusted),
'THREE_BLACK_CROWS': lambda c: get_three_black_crows(c, date, use_adjusted),
'DARK_CLOUD_COVER': lambda c: get_dark_cloud_cover(c, date, use_adjusted),
'ROUNDING_BOTTOM': lambda c: get_rounding_bottom(c, date, period or 60, use_adjusted),
'ASCENDING_TRIANGLE': lambda c: get_ascending_triangle(c, date, period or 30, use_adjusted),
'TOP_PATTERN': lambda c: get_top_pattern(c, date, period or 60, use_adjusted),
}
fn = _DISPATCH.get(signal_type.upper())
if fn is None:
raise ValueError(
f"未知 signal_type: {signal_type!r}。"
f"可选值: {list(_DISPATCH.keys())}"
)
scan_codes = codes if codes is not None else self.get_all_symbols()
return {code: 1 for code in scan_codes if fn(code) == 1}
def get_period_return(
self,
code: str,
start_date: str,
end_date: str,
use_adjusted: bool = True,
) -> Optional[float]:
"""
计算股票在指定区间内的期间收益率(%)。
取区间内第一个和最后一个有效交易日的收盘价,使用后复权价格消除分红/送股影响。
Args:
code: 股票代码,如 '600519.SH'
start_date: 区间起始日期,格式 YYYY-MM-DD(取当日或之后第一个交易日)
end_date: 区间结束日期,格式 YYYY-MM-DD(取当日或之前最后一个交易日)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
收益率(%,保留4位小数),区间内交易日不足2天返回 None
Example:
ret = api.get_period_return('600519.SH', '2026-01-01', '2026-03-14')
print(f"贵州茅台近3个月收益率: {ret:.2f}%")
"""
self.track_logger.write(f"get_period_return(code={code!r}, start_date={start_date!r}, end_date={end_date!r}, use_adjusted={use_adjusted!r})")
klines = query_daily_kline(
codes=[code], start_date=start_date, end_date=end_date,
order_by="date ASC",
)
if len(klines) < 2:
return None
if use_adjusted:
basics = query_daily_basic(
ts_codes=[code],
start_date=klines[0].date,
end_date=klines[-1].date,
)
adj_map = {b.trade_date: b.adj_factor for b in basics}
adj_s = adj_map.get(klines[0].date, 1.0)
adj_e = adj_map.get(klines[-1].date, 1.0)
close_s = klines[0].close * adj_s
close_e = klines[-1].close * adj_e
else:
close_s = klines[0].close
close_e = klines[-1].close
if close_s == 0:
return None
return round((close_e - close_s) / close_s * 100, 4)
# ============================================================
# 性能指标类接口
# ============================================================
def get_max_drawdown(self, equity_curve: List[float]) -> tuple:
"""
计算最大回撤。
Args:
equity_curve: 权益曲线,资产列表[初始值, ..., 最终值]
Returns:
元组 (最大回撤比例, 最高点索引, 最低点索引)
Example:
dd, peak_idx, drawdown_idx = api.get_max_drawdown([1000000, 1100000, 950000])
print(f'最大回撤: {dd:.2%}')
"""
self.track_logger.write(f"get_max_drawdown(equity_curve={equity_curve!r})")
return get_max_drawdown(equity_curve)
def get_max_drawdown_pct(self, equity_curve: List[float]) -> float:
"""
获取最大回撤百分比。
Args:
equity_curve: 权益曲线
Returns:
最大回撤比例,如 0.15 表示 15%
"""
self.track_logger.write(f"get_max_drawdown_pct(equity_curve={equity_curve!r})")
return get_max_drawdown_pct(equity_curve)
def get_annualized_return(self, total_return: float, days: int) -> float:
"""
计算年化收益率。
Args:
total_return: 总收益率,如 0.15 表示 15%
days: 交易天数
Returns:
年化收益率
Example:
annualized = api.get_annualized_return(0.15, 60)
"""
self.track_logger.write(f"get_annualized_return(total_return={total_return!r}, days={days!r})")
return get_annualized_return(total_return, days)
def get_total_return(self, initial_value: float, final_value: float) -> float:
"""
计算总收益率。
Args:
initial_value: 初始资金
final_value: 最终资金
Returns:
总收益率
"""
self.track_logger.write(f"get_total_return(initial_value={initial_value!r}, final_value={final_value!r})")
return get_total_return(initial_value, final_value)
def get_sharpe_ratio(self, equity_curve: List[float], risk_free_rate: float = 0.03) -> float:
"""
计算夏普比率。
Args:
equity_curve: 权益曲线
risk_free_rate: 无风险利率(年化),默认0.03
Returns:
夏普比率
Example:
sharpe = api.get_sharpe_ratio([1000000, 1050000, 1020000])
"""
self.track_logger.write(f"get_sharpe_ratio(equity_curve={equity_curve!r}, risk_free_rate={risk_free_rate!r})")
return get_sharpe_ratio(equity_curve, risk_free_rate)
def get_win_rate(self, trades: List[Dict]) -> float:
"""
计算胜率。
Args:
trades: 交易记录列表,每条包含 {'profit': 盈亏金额}
Returns:
胜率(0-100)
Example:
trades = [{'profit': 1000}, {'profit': -500}, {'profit': 800}]
win_rate = api.get_win_rate(trades)
"""
self.track_logger.write(f"get_win_rate(trades={trades!r})")
return get_win_rate(trades)
def get_profit_loss_ratio(self, trades: List[Dict]) -> float:
"""
计算盈亏比。
Args:
trades: 交易记录列表
Returns:
盈亏比(平均盈利/平均亏损)
"""
self.track_logger.write(f"get_profit_loss_ratio(trades={trades!r})")
return get_profit_loss_ratio(trades)
def get_calmar_ratio(self, equity_curve: List[float], days: int) -> float:
"""
计算卡尔玛比率(年化收益/最大回撤)。
Args:
equity_curve: 权益曲线
days: 交易天数
Returns:
卡尔玛比率
"""
self.track_logger.write(f"get_calmar_ratio(equity_curve={equity_curve!r}, days={days!r})")
return get_calmar_ratio(equity_curve, days)
def get_volatility(self, equity_curve: List[float]) -> float:
"""
计算收益波动率(年化)。
Args:
equity_curve: 权益曲线
Returns:
年化波动率
"""
self.track_logger.write(f"get_volatility(equity_curve={equity_curve!r})")
return get_volatility(equity_curve)
def get_trade_stats(self, trades: List[Dict]) -> Dict:
"""
获取交易统计信息。
Args:
trades: 交易记录列表
Returns:
统计信息字典,包含:
- total_trades: 总交易次数
- wins: 盈利次数
- losses: 亏损次数
- win_rate: 胜率
- profit_loss_ratio: 盈亏比
- total_profit: 总盈利
- total_loss: 总亏损
- avg_profit: 平均盈利
- avg_loss: 平均亏损
"""
self.track_logger.write(f"get_trade_stats(trades={trades!r})")
return get_trade_stats(trades)
def calculate_metrics(self, equity_curve: List[float], trades: List[Dict], initial_cash: float, days: int) -> Dict:
"""
生成完整的回测报告。
Args:
equity_curve: 权益曲线
trades: 交易记录列表
initial_cash: 初始资金
days: 交易天数
Returns:
回测报告字典,包含:
- initial_cash: 初始资金
- final_value: 最终资金
- total_return: 总收益率
- total_return_pct: 总收益率(%)
- annualized_return: 年化收益率
- annualized_return_pct: 年化收益率(%)
- max_drawdown: 最大回撤
- max_drawdown_pct: 最大回撤(%)
- sharpe_ratio: 夏普比率
- calmar_ratio: 卡尔玛比率
- volatility: 波动率
- trading_days: 交易天数
- trade_stats: 交易统计
Example:
equity = [1000000, 1050000, 1020000]
trades = [{'profit': 5000}, {'profit': -3000}]
report = api.calculate_metrics(equity, trades, 1000000, 30)
print(f"收益率: {report['total_return_pct']:.2f}%")
print(f"夏普比率: {report['sharpe_ratio']:.2f}")
"""
self.track_logger.write(f"calculate_metrics(equity_curve={equity_curve!r}, trades={trades!r}, initial_cash={initial_cash!r}, days={days!r})")
return generate_report(equity_curve, trades, initial_cash, days, self.track_logger)
# ============================================================
# 回测工具类接口
# ============================================================
def simulate_trade(self, action: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Dict:
"""
模拟单笔交易,计算成本和手续费。
Args:
action: 交易方向,'BUY' 或 'SELL'
price: 成交价格
quantity: 成交数量
fee_rate: 手续费率,默认0.0003(万三)
Returns:
字典 {'cost': 成本, 'fee': 手续费, 'net_proceeds': 净收款(卖出)}
Example:
result = api.simulate_trade('BUY', 100.0, 100)
print(f"成本: {result['cost']}, 手续费: {result['fee']}")
"""
self.track_logger.write(f"simulate_trade(action={action!r}, price={price!r}, quantity={quantity!r}, fee_rate={fee_rate!r})")
return simulate_trade(action, price, quantity, fee_rate)
def calculate_trade_cost(self, action: str, price: float, quantity: int, fee_rate: float = 0.0003, slippage: float = 0.0) -> float:
"""
计算交易成本(含手续费和滑点)。
Args:
action: 交易方向
price: 价格
quantity: 数量
fee_rate: 手续费率
slippage: 滑点比例
Returns:
交易成本
"""
self.track_logger.write(f"calculate_trade_cost(action={action!r}, price={price!r}, quantity={quantity!r}, fee_rate={fee_rate!r}, slippage={slippage!r})")
return calculate_trade_cost(action, price, quantity, fee_rate, slippage)
def create_position(self, code: str, shares: int, price: float, date: str) -> Position:
"""
创建持仓对象。
Args:
code: 股票代码
shares: 股数
price: 买入价格
date: 买入日期
Returns:
Position对象
Example:
pos = api.create_position('600519.SH', 100, 1800.0, '2026-01-01')
"""
self.track_logger.write(f"create_position(code={code!r}, shares={shares!r}, price={price!r}, date={date!r})")
return create_position(code, shares, price, date)
def get_position_value(self, position: Position, current_price: float) -> float:
"""
计算持仓市值。
Args:
position: Position对象
current_price: 当前价格
Returns:
市值
"""
self.track_logger.write(f"get_position_value(position={position!r}, current_price={current_price!r})")
return get_position_value(position, current_price)
def get_position_profit(self, position: Position, current_price: float) -> tuple:
"""
计算持仓盈亏。
Args:
position: Position对象
current_price: 当前价格
Returns:
元组 (盈亏金额, 盈亏比例)
Example:
profit, pct = api.get_position_profit(position, 2000.0)
print(f"盈利: {profit}, 比例: {pct:.2%}")
"""
self.track_logger.write(f"get_position_profit(position={position!r}, current_price={current_price!r})")
return get_position_profit(position, current_price)
def calculate_portfolio_value(self, cash: float, positions: Dict[str, Position], prices: Dict[str, float]) -> float:
"""
计算组合总价值。
Args:
cash: 现金
positions: 持仓字典 {code: Position}
prices: 当前价格字典 {code: price}
Returns:
总资产
Example:
value = api.calculate_portfolio_value(500000, positions, current_prices)
"""
self.track_logger.write(f"calculate_portfolio_value(cash={cash!r}, positions={positions!r}, Position={Position!r}, prices={prices!r}, float={float!r})")
return calculate_portfolio_value(cash, positions, prices)
def get_portfolio_positions(self, positions: Dict[str, Position]) -> List[Dict]:
"""
获取组合持仓详情列表。
Args:
positions: 持仓字典
Returns:
持仓详情列表
"""
self.track_logger.write(f"get_portfolio_positions(positions={positions!r}, Position]={Position!r})")
return get_portfolio_positions(positions)
def build_equity_curve(self, daily_values: List[tuple]) -> List[float]:
"""
从每日资产构建权益曲线。
Args:
daily_values: [(日期, 资产), ...] 按日期升序
Returns:
权益曲线列表
Example:
values = [('2026-01-01', 1000000), ('2026-01-02', 1005000)]
curve = api.build_equity_curve(values)
"""
self.track_logger.write(f"build_equity_curve(daily_values={daily_values!r})")
return build_equity_curve(daily_values)
def calculate_daily_returns(self, equity_curve: List[float]) -> List[float]:
"""
计算日收益率序列。
Args:
equity_curve: 权益曲线
Returns:
日收益率列表
"""
self.track_logger.write(f"calculate_daily_returns(equity_curve={equity_curve!r})")
return calculate_daily_returns(equity_curve)
def should_buy(self, current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_oversold: float = 30) -> bool:
"""
买入信号判断(MA金叉 + RSI超卖)。
Args:
current_price: 当前价格
ma_short: 短期均线
ma_long: 长期均线
rsi: RSI值
rsi_oversold: RSI超卖阈值
Returns:
是否买入
Example:
if api.should_buy(close, ma5, ma20, rsi, 30):
print('买入信号')
"""
self.track_logger.write(f"should_buy(current_price={current_price!r}, ma_short={ma_short!r}, ma_long={ma_long!r}, rsi={rsi!r}, rsi_oversold={rsi_oversold!r})")
return should_buy(current_price, ma_short, ma_long, rsi, rsi_oversold)
def should_sell(self, current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_overbought: float = 70) -> bool:
"""
卖出信号判断(MA死叉或RSI超买)。
Args:
current_price: 当前价格
ma_short: 短期均线
ma_long: 长期均线
rsi: RSI值
rsi_overbought: RSI超买阈值
Returns:
是否卖出
Example:
if api.should_sell(close, ma5, ma20, rsi, 70):
print('卖出信号')
"""
self.track_logger.write(f"should_sell(current_price={current_price!r}, ma_short={ma_short!r}, ma_long={ma_long!r}, rsi={rsi!r}, rsi_overbought={rsi_overbought!r})")
return should_sell(current_price, ma_short, ma_long, rsi, rsi_overbought)
def calculate_drawdown(self, equity_curve: List[float]) -> List[float]:
"""
计算回撤序列。
Args:
equity_curve: 权益曲线
Returns:
回撤序列列表
Example:
drawdowns = api.calculate_drawdown([1000000, 1100000, 950000])
"""
self.track_logger.write(f"calculate_drawdown(equity_curve={equity_curve!r})")
return calculate_drawdown(equity_curve)
# ============================================================
# Tick级数据接口(模拟级Tick)
# ============================================================
def get_tick_data(self, code: str, date: str) -> Optional[Dict]:
"""
获取指定日期的Tick级数据(模拟级)。
Args:
code: 股票代码
date: 日期,格式 YYYY-MM-DD
Returns:
Tick数据字典,包含:
- time: 时间
- open: 开盘价
- high: 最高价
- low: 最低价
- close: 收盘价
- volume: 成交量
- amount: 成交额
若无数据返回None
Example:
tick = api.get_tick_data('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_tick_data(code={code!r}, date={date!r})")
klines = query_daily_kline(codes=[code], start_date=date, end_date=date, order_by="date ASC")
if not klines:
return None
k = klines[0]
return {
'time': k.date,
'open': k.open,
'high': k.high,
'low': k.low,
'close': k.close,
'volume': k.volume,
'amount': k.amount,
}
def get_realtime_bar(self, code: str, date: str) -> Dict:
"""
获取实时Bar数据(用于实盘级Tick)。
Args:
code: 股票代码
date: 日期
Returns:
Bar数据字典
Example:
bar = api.get_realtime_bar('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_realtime_bar(code={code!r}, date={date!r})")
return self.get_tick_data(code, date)
# ============================================================
# 订单管理接口
# ============================================================
def create_order(self, code: str, action: str, price: float, quantity: int) -> Dict:
"""
创建订单(本地模拟,非真实下单)。
Args:
code: 股票代码
action: 'BUY' 或 'SELL'
price: 价格
quantity: 数量(股)
Returns:
订单字典,包含:
- order_id: 订单ID
- code: 股票代码
- action: 方向
- price: 价格
- quantity: 数量
- status: 状态 'PENDING'
- create_time: 创建时间
Example:
order = api.create_order('600519.SH', 'BUY', 1800.0, 100)
"""
self.track_logger.write(f"create_order(code={code!r}, action={action!r}, price={price!r}, quantity={quantity!r})")
import time
return {
'order_id': f"ORDER_{int(time.time()*1000)}",
'code': code,
'action': action.upper(),
'price': price,
'quantity': quantity,
'status': 'PENDING',
'create_time': time.strftime('%Y-%m-%d %H:%M:%S'),
}
def cancel_order(self, order: Dict) -> bool:
"""
取消订单。
Args:
order: 订单字典
Returns:
是否取消成功
Example:
api.cancel_order(order)
"""
self.track_logger.write(f"cancel_order(order={order!r})")
if order.get('status') == 'PENDING':
order['status'] = 'CANCELLED'
return True
return False
def get_order_status(self, order: Dict) -> str:
"""
获取订单状态。
Args:
order: 订单字典
Returns:
状态: PENDING, FILLED, CANCELLED, REJECTED
Example:
status = api.get_order_status(order)
"""
self.track_logger.write(f"get_order_status(order={order!r})")
return order.get('status', 'UNKNOWN')
def close_position(self, position: Position, price: float, date: str) -> Dict:
"""
平仓(卖出股票结束多头持仓)。
Args:
position: Position对象
price: 平仓价格
date: 平仓日期
Returns:
平仓结果字典,包含:
- profit: 盈亏金额
- profit_pct: 盈亏比例
- hold_days: 持有天数
Example:
result = api.close_position(position, 1900.0, '2026-01-15')
print(f"盈利: {result['profit']}")
"""
self.track_logger.write(f"close_position(position={position!r}, price={price!r}, date={date!r})")
profit, profit_pct = get_position_profit(position, price)
hold_days = (date_to_num(date) - date_to_num(position.entry_date))
return {
'profit': profit,
'profit_pct': profit_pct,
'hold_days': hold_days,
}
def update_position_price(self, position: Position, current_price: float) -> None:
"""
更新持仓的当前价格(用于市价计算)。
Args:
position: Position对象
current_price: 当前价格
"""
self.track_logger.write(f"update_position_price(position={position!r}, current_price={current_price!r})")
update_position(position, current_price)
# ============================================================
# 回测引擎控制接口
# ============================================================
def init_backtest(self, initial_cash: float = 1000000.0, fee_rate: float = 0.0003) -> Dict:
"""
初始化回测环境。
Args:
initial_cash: 初始资金,默认100万
fee_rate: 手续费率,默认万三
Returns:
回测环境字典
Example:
env = api.init_backtest(1000000, 0.0003)
"""
self.track_logger.write(f"init_backtest(initial_cash={initial_cash!r}, fee_rate={fee_rate!r})")
return {
'initial_cash': initial_cash,
'fee_rate': fee_rate,
'cash': initial_cash,
'positions': {},
'orders': [],
'trades': [],
'equity_curve': [],
}
def execute_buy(self, env: Dict, code: str, price: float, quantity: int, date: str) -> Dict:
"""
执行买入操作。
Args:
env: 回测环境字典
code: 股票代码
price: 价格
quantity: 数量
date: 交易日期
Returns:
执行结果字典
"""
self.track_logger.write(f"execute_buy(env={env!r}, code={code!r}, price={price!r}, quantity={quantity!r}, date={date!r})")
fee_rate = env.get('fee_rate', 0.0003)
new_cash, new_positions, result = buy(
env['cash'], env['positions'], code, price, quantity, date, fee_rate
)
env['cash'] = new_cash
env['positions'] = new_positions
if result.success:
env['trades'].append({
'code': code,
'action': 'BUY',
'price': price,
'quantity': quantity,
'cost': result.cost,
'fee': result.fee,
})
return {
'success': result.success,
'code': code,
'action': 'BUY',
'price': price,
'quantity': quantity,
'cost': result.cost if result.success else 0,
'fee': result.fee if result.success else 0,
'reason': result.reason,
}
def execute_sell(self, env: Dict, code: str, price: float, quantity: int) -> Dict:
"""
执行卖出操作。
Args:
env: 回测环境字典
code: 股票代码
price: 价格
quantity: 数量
Returns:
执行结果字典
"""
self.track_logger.write(f"execute_sell(env={env!r}, code={code!r}, price={price!r}, quantity={quantity!r})")
fee_rate = env.get('fee_rate', 0.0003)
new_cash, new_positions, result = sell(
env['cash'], env['positions'], code, price, quantity, fee_rate
)
env['cash'] = new_cash
env['positions'] = new_positions
if result.success:
env['trades'].append({
'code': code,
'action': 'SELL',
'price': price,
'quantity': quantity,
'net_proceeds': result.net_proceeds,
'fee': result.fee,
})
return {
'success': result.success,
'code': code,
'action': 'SELL',
'price': price,
'quantity': quantity,
'net_proceeds': result.net_proceeds if result.success else 0,
'fee': result.fee if result.success else 0,
'reason': result.reason,
}
def get_equity(self, env: Dict, current_prices: Dict[str, float]) -> float:
"""
获取当前权益(现金+持仓市值)。
Args:
env: 回测环境字典
current_prices: 当前价格字典 {code: price}
Returns:
总权益
"""
self.track_logger.write(f"get_equity(env={env!r}, current_prices={current_prices!r}, float]={float!r})")
return calculate_portfolio_value(env['cash'], env['positions'], current_prices)
def record_equity(self, env: Dict, date: str, current_prices: Dict[str, float]) -> None:
"""
记录每日权益到权益曲线。
Args:
env: 回测环境字典
date: 日期
current_prices: 当前价格字典
"""
self.track_logger.write(f"record_equity(env={env!r}, date={date!r}, current_prices={current_prices!r}, float]={float!r})")
equity = self.get_equity(env, current_prices)
env['equity_curve'].append((date, equity))
# ============================================================
# 策略辅助函数
# ============================================================
def get_price_change_rate(self, code: str, date: str, days: int = 3) -> Optional[float]:
"""
计算近N日平均涨幅。
Args:
code: 股票代码
date: 日期
days: 天数,默认3
Returns:
平均涨跌幅(%),若数据不足返回None
Example:
avg_change = api.get_price_change_rate('600519.SH', '2026-03-01', 3)
"""
self.track_logger.write(f"get_price_change_rate(code={code!r}, date={date!r}, days={days!r})")
import datetime
start_dt = datetime.datetime.strptime(date, '%Y-%m-%d')
end_dt = start_dt - datetime.timedelta(days=days * 2)
start = end_dt.strftime('%Y-%m-%d')
klines = query_daily_kline(codes=[code], start_date=start, end_date=date, order_by="date ASC")
if len(klines) < days:
return None
klines.sort(key=lambda x: x.date, reverse=True)
pct_sum = sum(k.pctChg for k in klines[:days])
return pct_sum / days
def get_top_performers(self, codes: List[str], date: str, days: int = 3, top_n: int = 3) -> List[tuple]:
"""
获取近N日涨幅最高的股票。
Args:
codes: 股票代码列表
date: 日期
days: 计算天数
top_n: 返回前N只
Returns:
[(股票代码, 平均涨幅), ...] 按涨幅降序
Example:
top_stocks = api.get_top_performers(codes, '2026-03-01', 3, 3)
"""
self.track_logger.write(f"get_top_performers(codes={codes!r}, date={date!r}, days={days!r}, top_n={top_n!r})")
results = []
for code in codes:
avg_change = self.get_price_change_rate(code, date, days)
if avg_change is not None:
results.append((code, avg_change))
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_n]
def get_price_at_date(self, code: str, date: str) -> Optional[float]:
"""
获取指定日期的收盘价。
Args:
code: 股票代码
date: 日期
Returns:
收盘价,若无数据返回None
Example:
price = api.get_price_at_date('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_price_at_date(code={code!r}, date={date!r})")
klines = query_daily_kline(codes=[code], start_date=date, end_date=date, order_by="date ASC")
return klines[0].close if klines else None
def get_prices_at_dates(self, code: str, dates: List[str]) -> List[Optional[float]]:
"""
获取多个日期的收盘价。
Args:
code: 股票代码
dates: 日期列表
Returns:
收盘价列表(按日期升序)
Example:
prices = api.get_prices_at_dates('600519.SH', ['2026-01-01', '2026-01-02'])
"""
self.track_logger.write(f"get_prices_at_dates(code={code!r}, dates={dates!r})")
if not dates:
return []
start = dates[0]
end = dates[-1]
klines = query_daily_kline(codes=[code], start_date=start, end_date=end, order_by="date ASC")
price_map = {k.date: k.close for k in klines}
return [price_map.get(d) for d in dates]
# ============================================================
# 数据库维护接口
# ============================================================
def init_databases(self) -> None:
"""
初始化所有数据库(指标库等)。
Example:
api.init_databases()
"""
self.track_logger.write("init_databases()")
init_indicators_db()
def clear_indicator_cache(self, code: str = None) -> None:
"""
清除技术指标缓存。
Args:
code: 股票代码,None表示清除所有
Example:
api.clear_indicator_cache('600519.SH') # 清除指定股票
api.clear_indicator_cache() # 清除所有
"""
self.track_logger.write(f"clear_indicator_cache(code={code!r})")
with getEngine().connect() as conn:
if code:
conn.execute(text("DELETE FROM cached_indicators WHERE code=:code"), {"code": code})
else:
conn.execute(text("DELETE FROM cached_indicators"))
conn.commit()
# ── Alpha101 因子接口 ────────────────────────────────────────────────────
def load_alpha_data(
self,
codes: List[str],
start_date: str,
end_date: str,
fill_method: str = "ffill",
) -> dict:
"""
加载 Alpha 因子计算所需的面板数据。
Args:
codes: 股票代码列表,如 ['000001.SZ', '600519.SH']
start_date: 起始日期,格式 'YYYY-MM-DD'
end_date: 截止日期,格式 'YYYY-MM-DD'
fill_method: 缺失值填充方式('ffill' / 'bfill' / None)
Returns:
字典,key 为字段名,value 为 DataFrame(行=日期,列=股票代码):
open, high, low, close, volume, amount, vwap, returns, ind
Example:
data = api.load_alpha_data(['000001.SZ', '600519.SH'], '2025-01-01', '2026-03-01')
"""
self.track_logger.write(f"load_alpha_data(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, fill_method={fill_method!r})")
loader = AlphaDataLoader()
return loader.load(codes=codes, start_date=start_date, end_date=end_date, fill_method=fill_method)
def compute_alpha(
self,
codes: List[str],
start_date: str,
end_date: str,
alpha_num: int,
fill_method: str = "ffill",
):
"""
计算单个 Alpha 因子面板。
Args:
codes: 股票代码列表
start_date: 起始日期
end_date: 截止日期
alpha_num: 因子编号(1~101)
fill_method: 缺失值填充方式
Returns:
pd.DataFrame(行=日期,列=股票代码),或 None(数据为空时)
Example:
df = api.compute_alpha(['000001.SZ', '600519.SH'], '2025-01-01', '2026-03-01', 1)
latest = df.iloc[-1].dropna().sort_values(ascending=False)
"""
self.track_logger.write(f"compute_alpha(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, alpha_num={alpha_num!r}, fill_method={fill_method!r})")
data = self.load_alpha_data(codes, start_date, end_date, fill_method)
if not data:
return None
a = Alpha101(data)
method_name = f"alpha{alpha_num:03d}"
method = getattr(a, method_name, None)
if method is None:
raise ValueError(f"Alpha101 不存在因子 {method_name}(编号需在 1~101 范围内)")
return method()
def compute_alphas(
self,
codes: List[str],
start_date: str,
end_date: str,
alphas: Optional[List[int]] = None,
fill_method: str = "ffill",
) -> Dict:
"""
批量计算多个 Alpha 因子面板。
Args:
codes: 股票代码列表
start_date: 起始日期
end_date: 截止日期
alphas: 因子编号列表(如 [1, 5, 12]),None 表示计算全部 101 个
fill_method: 缺失值填充方式
Returns:
Dict[str, pd.DataFrame],key 为 'alpha001' 等,value 为面板 DataFrame。
计算出错的因子会被跳过并打印警告,不影响其他因子。
Example:
results = api.compute_alphas(
['000001.SZ', '600519.SH'],
'2025-01-01', '2026-03-01',
alphas=[1, 5, 12, 101],
)
for name, df in results.items():
print(name, df.iloc[-1].describe())
"""
self.track_logger.write(f"compute_alphas(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, alphas={alphas!r}, fill_method={fill_method!r})")
data = self.load_alpha_data(codes, start_date, end_date, fill_method)
if not data:
return {}
a = Alpha101(data)
return a.compute_all(alphas=alphas)
def get_alpha_latest(
self,
codes: List[str],
start_date: str,
end_date: str,
alpha_num: int,
fill_method: str = "ffill",
):
"""
计算单个 Alpha 因子并返回最新一日横截面(已去 NaN,按因子值降序排列)。
Args:
codes: 股票代码列表
start_date: 起始日期(建议给出足够的历史数据,通常 1 年以上)
end_date: 截止日期(即"最新日")
alpha_num: 因子编号(1~101)
fill_method: 缺失值填充方式
Returns:
pd.Series(index=股票代码,values=因子值,降序排列),或 None(数据为空时)
Example:
latest = api.get_alpha_latest(
['000001.SZ', '600519.SH', '000858.SZ'],
'2025-01-01', '2026-03-14',
alpha_num=1,
)
print(latest.head(10)) # 因子值最高的 10 只股票
"""
self.track_logger.write(f"get_alpha_latest(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, alpha_num={alpha_num!r}, fill_method={fill_method!r})")
df = self.compute_alpha(codes, start_date, end_date, alpha_num, fill_method)
if df is None:
return None
return df.iloc[-1].dropna().sort_values(ascending=False)
def random_alpha_backtest(
self,
codes: Optional[List[str]] = None,
max_screen_factors: int = 3,
max_signal_factors: int = 3,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
initial_cash: float = 1_000_000.0,
warmup_days: int = 90,
random_seed: Optional[int] = None,
top_n_stocks: int = 5,
max_pool_size: int = 30,
max_holdings: int = 5,
) -> Dict:
"""
因子挖矿接口(两阶段:选股 + 交易信号)。
流程:
1. 随机抽取 k_screen 个选股因子 + k_signal 个信号因子
2. 选股阶段:以 start_date 为截面日,每个选股因子随机保留 5%~20% 的股票
3. 若过滤后股票数仍超过 max_pool_size,按各选股因子综合得分再截取前 max_pool_size 只
4. 信号阶段:逐日计算信号因子横截面分位排名,综合排名 >= buy_thresh 时买入,
<= sell_thresh 时卖出(阈值在合理范围内随机生成)
5. 每日最多同时持仓 max_holdings 只,优先买入综合排名最高的股票
6. 输出 Top N 个股的每笔交易时的具体因子值与排名
Args:
codes: 股票池;None 时取全市场
max_screen_factors: 选股因子最大数量(默认 5)
max_signal_factors: 信号因子最大数量(默认 7)
start_date: 回测起始日,None 取 end_date 前 90 天
end_date: 回测截止日,None 取今日
initial_cash: 初始资金(默认 100 万)
warmup_days: 因子预热天数(默认 90)
random_seed: 随机种子,None 不固定
top_n_stocks: 输出详细交易记录的个股数量(默认 5)
max_pool_size: 最终候选池上限,超过时按综合得分截取(默认 30)
max_holdings: 最大同时持仓数,优先持有综合排名最高的股票(默认 5)
"""
self.track_logger.write(f"random_alpha_backtest(codes={codes!r}, max_screen_factors={max_screen_factors!r}, max_signal_factors={max_signal_factors!r}, start_date={start_date!r}, end_date={end_date!r}, initial_cash={initial_cash!r}, warmup_days={warmup_days!r}, random_seed={random_seed!r}, top_n_stocks={top_n_stocks!r}, max_pool_size={max_pool_size!r}, max_holdings={max_holdings!r})")
from factor_mining import run_random_alpha_backtest
return run_random_alpha_backtest(
api=self,
codes=codes,
max_screen_factors=max_screen_factors,
max_signal_factors=max_signal_factors,
start_date=start_date,
end_date=end_date,
initial_cash=initial_cash,
warmup_days=warmup_days,
random_seed=random_seed,
top_n_stocks=top_n_stocks,
max_pool_size=max_pool_size,
max_holdings=max_holdings,
)
# ============================================================
# ★ MoE 混合专家买卖决策接口(Agent 调用优先级最高)
# ============================================================
def get_trade_signal(
self,
code: str,
date: str = None,
) -> Dict:
"""
★★★ 优先使用此接口判断股票买卖时机 ★★★
基于 MoE(混合专家模型)综合评估一只股票的买卖信号,整合以下四类专家的分析:
- 技术指标专家:80+ 个技术指标(均线、动量、振荡、成交量、通道等全覆盖)
- Alpha因子专家:WorldQuant 101个Alpha量化因子,基于全市场截面排名
- 基本面专家:PE_TTM、PB、换手率、量比、市销率等估值指标
- 量价行为专家:涨跌停、连板、炸板、龙虎榜净买入、近期涨跌幅
各专家权重从 moe_weights.json 动态加载,可通过 train_moe_weights() 跑回测优化。
当某类专家数据不足时自动降权,其余专家权重等比重新归一化。
⚠️ 调用场景:
- 用户询问某只股票"能不能买"、"该不该卖"、"现在适合持有吗"时,调用此接口
- 用户询问某只股票"当前信号"、"买卖时机"、"操作建议"时,调用此接口
- 多股票比较时,可多次调用后按 final_score 排序
Args:
code (str): 股票代码,格式如 '000001.SZ'、'600519.SH'
date (str, optional): 分析日期,格式 'YYYY-MM-DD'。
默认为今天。历史回溯时可指定过去日期。
Returns:
Dict,包含以下字段:
{
"code": "000001.SZ", # 股票代码
"date": "2026-03-18", # 分析日期
"signal": "BUY", # 信号:BUY=买入 / SELL=卖出 / HOLD=持有
"final_score": 0.72, # 综合评分 0~1,越高越看多
"confidence": "高", # 置信度:高/中/低(专家间分歧程度)
"reason": "技术面看多(0.71),Alpha因子看多(0.73),量价行为看多(0.68)",
"experts": {
"technical": {"score": 0.71, "weight": 0.41, "valid_count": 90},
"alpha": {"score": 0.73, "weight": 0.41, "valid_count": 98},
"fundamental": {"score": null, "weight": 0.0, "note": "数据不足"},
"behavior": {"score": 0.68, "weight": 0.18}
}
}
signal 取值说明:
- "BUY" → final_score >= buy_thresh(默认0.65),建议买入
- "SELL" → final_score <= sell_thresh(默认0.35),建议卖出
- "HOLD" → 介于两者之间,建议持有观望
使用示例:
api = StockApi()
result = api.get_trade_signal('000001.SZ')
if result['signal'] == 'BUY':
print(f"建议买入,综合评分 {result['final_score']}")
# 历史日期分析
result = api.get_trade_signal('600519.SH', date='2026-01-15')
"""
from datetime import datetime as _dt
_date = date or _dt.today().strftime('%Y-%m-%d')
# 延迟导入,避免循环依赖
import importlib, os as _os
_moe_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'moe_signal.py')
import importlib.util
_spec = importlib.util.spec_from_file_location('moe_signal', _moe_path)
_moe = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_moe)
init_indicators_db()
return _moe.analyze(code, _date)
def train_moe_weights(
self,
start_date: str = None,
end_date: str = None,
population_size: int = 20,
generations: int = 30,
train_stock_count: int = 30,
) -> Dict:
"""
通过遗传算法在指定历史区间跑回测,训练 MoE 各指标权重,目标:最大化平均持仓收益。
训练完成后自动将最优权重写入 moe_weights.json,下次调用 get_trade_signal() 时自动生效。
建议每半年重新训练一次,以适应最新行情风格。
⚠️ 调用场景:
- 用户说"优化权重"、"重新训练"、"适配最新行情"时调用
- 默认训练区间为最近半年(约180天)
Args:
start_date (str, optional): 训练开始日期 'YYYY-MM-DD',默认今天前180天
end_date (str, optional): 训练结束日期 'YYYY-MM-DD',默认今天
population_size (int): 遗传算法种群大小,默认20(越大越精准但越慢)
generations (int): 迭代代数,默认30
train_stock_count (int): 参与训练的随机采样股票数量,默认30
Returns:
Dict: 优化后的完整权重配置(同时已写入 moe_weights.json)
{
"expert_weights": {"technical": 0.38, "alpha": 0.32, ...},
"signal_thresholds": {"buy": 0.67, "sell": 0.33},
"technical": {"sma5": 1.2, "rsi14": 0.9, ...},
...
"_trained_at": "2026-03-18 12:00:00",
"_train_period": "2025-09-18~2026-03-18"
}
使用示例:
api = StockApi()
# 用最近半年数据训练(默认)
weights = api.train_moe_weights()
# 指定区间,快速训练(小种群+少代数)
weights = api.train_moe_weights(
start_date='2025-06-01', end_date='2025-12-31',
population_size=10, generations=15, train_stock_count=20
)
"""
from datetime import datetime as _dt, timedelta as _td
import importlib.util, os as _os
_end = end_date or _dt.today().strftime('%Y-%m-%d')
_start = start_date or (_dt.today() - _td(days=180)).strftime('%Y-%m-%d')
_moe_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'moe_signal.py')
_spec = importlib.util.spec_from_file_location('moe_signal', _moe_path)
_moe = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_moe)
init_indicators_db()
return _moe.train_weights(
start_date=_start,
end_date=_end,
population_size=population_size,
generations=generations,
train_stock_count=train_stock_count,
)
def date_to_num(date_str: str) -> int:
"""日期字符串转数字(用于计算天数差)"""
import datetime
try:
return int(datetime.datetime.strptime(date_str, '%Y-%m-%d').strftime('%Y%m%d'))
except:
return 0
FILE:scripts/stock_crawler.py
import requests
import time
import json
import random
import re
from typing import Dict, List, Optional, Union, Any
from dataclasses import dataclass
@dataclass
class RealtimeStockQuote:
"""
实时股票报价信息
"""
ts_code: str # 股票代码(如 000001.SZ)
name: str # 股票名称
open: float # 今日开盘价
pre_close: float # 昨日收盘价
price: float # 当前最新价
high: float # 今日最高价
low: float # 今日最低价
bid: float # 买一价
ask: float # 卖一价
volume: int # 成交量(股)
amount: float # 成交额(元)
date: str # 交易日期(YYYY-MM-DD)
time: str # 最新报价时间(HH:MM:SS)
amplitude: float # 振幅(%)
turnover_rate: Optional[float] # 换手率(%),可能为空
total_cap: Optional[float] # 总市值(元),可能为空
circ_cap: Optional[float] # 流通市值(元),可能为空
pb: Optional[float] # 市净率,可能为空
pe_ttm: Optional[float] # 市盈率(TTM),可能为空
total_shares: Optional[float] # 总股本(股),可能为空
circ_shares: Optional[float] # 流通股本(股),可能为空
status: str # 请求状态(success / error)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "RealtimeStockQuote":
return cls(**data)
class StockCrawler:
"""
StockCrawler provides interfaces to fetch real-time stock data from multiple sources:
- Sina Finance (新浪财经)
- East Money (东方财富)
- Tonghuashun (同花顺)
It handles rate limiting to avoid IP bans and standardizes the output format.
"""
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
self.last_request_time = {}
self.min_interval = 1.0 # Minimum interval between requests in seconds per source
def _wait_for_rate_limit(self, source: str):
current_time = time.time()
last_time = self.last_request_time.get(source, 0)
elapsed = current_time - last_time
if elapsed < self.min_interval:
sleep_time = self.min_interval - elapsed
time.sleep(sleep_time)
self.last_request_time[source] = time.time()
def _convert_to_sina_symbol(self, ts_code: str) -> str:
# 000001.SZ -> sz000001
# 600519.SH -> sh600519
code, market = ts_code.split('.')
return f"{market.lower()}{code}"
def _convert_to_eastmoney_secid(self, ts_code: str) -> str:
# 000001.SZ -> 0.000001
# 600519.SH -> 1.600519
code, market = ts_code.split('.')
if market == 'SH':
return f"1.{code}"
else:
return f"0.{code}"
def _convert_to_tonghuashun_code(self, ts_code: str) -> str:
# 000001.SZ -> 000001
# 600519.SH -> 600519
return ts_code.split('.')[0]
def fetch_sina(self, ts_code: str) -> Optional[RealtimeStockQuote]:
"""
Fetch real-time data from Sina Finance.
URL: http://hq.sinajs.cn/list={symbol}
"""
self._wait_for_rate_limit('sina')
symbol = self._convert_to_sina_symbol(ts_code)
url = f"http://hq.sinajs.cn/list={symbol}"
headers = self.headers.copy()
headers['Referer'] = 'https://finance.sina.com.cn/'
try:
response = requests.get(url, headers=headers, timeout=5)
response.raise_for_status()
# Format: var hq_str_sh601006="大秦铁路, 27.55, 27.25, 26.91, 27.55, 26.20, 26.91, 26.92, ...";
content = response.text
print(content)
match = re.search(r'="(.*)";', content)
if match:
data_str = match.group(1)
parts = data_str.split(',')
if len(parts) > 30:
pre_close = float(parts[2])
high = float(parts[4])
low = float(parts[5])
amplitude = 0.0
if pre_close > 0:
amplitude = (high - low) / pre_close * 100
return RealtimeStockQuote(
ts_code=ts_code,
name=parts[0],
open=float(parts[1]),
pre_close=pre_close,
price=float(parts[3]),
high=high,
low=low,
bid=float(parts[6]),
ask=float(parts[7]),
volume=int(parts[8]), # Sina returns shares
amount=float(parts[9]),
date=parts[30],
time=parts[31],
amplitude=round(amplitude, 2),
turnover_rate=None,
total_cap=None,
circ_cap=None,
pb=None,
pe_ttm=None,
total_shares=None,
circ_shares=None,
status="success"
)
return None
except Exception:
return None
def fetch_eastmoney(self, ts_code: str) -> Dict[str, Any]:
"""
Fetch real-time data from East Money.
"""
self._wait_for_rate_limit('eastmoney')
secid = self._convert_to_eastmoney_secid(ts_code)
url = "http://push2.eastmoney.com/api/qt/stock/get"
# f43:price, f44:high, f45:low, f46:open, f47:volume, f48:amount, f57:code, f58:name, f60:pre_close, f168:turnover, f170:change_pct
params = {
"secid": secid,
"fields": "f43,f44,f45,f46,f47,f48,f50,f51,f52,f57,f58,f60,f116,f117,f162,f167,f168,f169,f170,f7,f84,f85",
"invt": "2",
"fltt": "2",
"ut": "fa5fd1943c7b386f172d6893dbfba10b"
}
try:
response = requests.get(url, params=params, headers=self.headers, timeout=5)
response.raise_for_status()
data = response.json()
if data and data.get("data"):
d = data["data"]
# EastMoney volume is in 'shou' (100 shares), convert to shares
volume = d.get("f47")
if volume is not None:
volume = float(volume) * 100
# Calculate amplitude if not provided
amplitude = d.get("f7")
if amplitude is None:
try:
high = float(d.get("f44"))
low = float(d.get("f45"))
pre_close = float(d.get("f60"))
if pre_close > 0:
amplitude = (high - low) / pre_close * 100
amplitude = round(amplitude, 2)
except (TypeError, ValueError):
amplitude = None
return {
"source": "eastmoney",
"ts_code": ts_code,
"name": d.get("f58"),
"price": d.get("f43"),
"high": d.get("f44"),
"low": d.get("f45"),
"open": d.get("f46"),
"pre_close": d.get("f60"),
"volume": volume,
"amount": d.get("f48"),
"turnover_rate": d.get("f168"),
"change_pct": d.get("f170"),
"change_amount": d.get("f169"),
"pe_ttm": d.get("f162"),
"pb": d.get("f167"),
"total_cap": d.get("f116"),
"circ_cap": d.get("f117"),
"amplitude": amplitude,
"total_shares": d.get("f84"),
"circ_shares": d.get("f85"),
"status": "success"
}
return {"source": "eastmoney", "ts_code": ts_code, "status": "failed", "message": "No data"}
except Exception as e:
return {"source": "eastmoney", "ts_code": ts_code, "status": "failed", "message": str(e)}
def fetch_tonghuashun(self, ts_code: str) -> Dict[str, Any]:
"""
Fetch data from Tonghuashun (10jqka).
Using the 'last.js' endpoint which provides the latest kline data.
Note: This returns daily K-line data, which might be the latest available trading day.
"""
self._wait_for_rate_limit('tonghuashun')
code = self._convert_to_tonghuashun_code(ts_code)
url = f"http://d.10jqka.com.cn/v6/line/hs_{code}/01/last.js"
try:
response = requests.get(url, headers=self.headers, timeout=5)
response.raise_for_status()
content = response.text
# content format: quotebridge_v6_line_hs_000001_01_last({"rt":"...","data":"date,open,high,low,close,vol,amt,..."})
match = re.search(r'\((.*)\)', content)
if match:
json_str = match.group(1)
d = json.loads(json_str)
data_str = d.get("data", "")
if data_str:
lines = data_str.split(';')
if lines:
latest = lines[-1].split(',')
if len(latest) >= 7:
# Format: Date, Open, High, Low, Close, Volume, Amount, Turnover...
return {
"source": "tonghuashun",
"ts_code": ts_code,
"date": latest[0],
"open": float(latest[1]),
"high": float(latest[2]),
"low": float(latest[3]),
"price": float(latest[4]),
"volume": float(latest[5]), # Volume is likely in shares for kline data
"amount": float(latest[6]),
"turnover_rate": float(latest[7]) if len(latest) > 7 and latest[7] else None,
"pre_close": None, # Needs calculation from previous day or another source
"amplitude": None, # Needs pre_close
"total_cap": None,
"circ_cap": None,
"pb": None,
"pe_ttm": None,
"total_shares": None,
"circ_shares": None,
"status": "success"
}
return {"source": "tonghuashun", "ts_code": ts_code, "status": "failed", "message": "Parse error or no data"}
except Exception as e:
return {"source": "tonghuashun", "ts_code": ts_code, "status": "failed", "message": str(e)}
def fetch(self, ts_code: str, source: str = 'sina') -> Dict[str, Any]:
"""
Fetch stock data from specified source.
Args:
ts_code (str): Stock code (e.g., '000001.SZ')
source (str): Source name ('sina', 'eastmoney', 'tonghuashun')
Returns:
Dict: Stock data
"""
if source == 'sina':
return self.fetch_sina(ts_code)
elif source == 'eastmoney':
return self.fetch_eastmoney(ts_code)
elif source == 'tonghuashun':
return self.fetch_tonghuashun(ts_code)
else:
# Default to sina if unknown
return self.fetch_sina(ts_code)
if __name__ == "__main__":
crawler = StockCrawler()
# Test examples
print(crawler.fetch("000001.SZ", "sina"))
# print(crawler.fetch("600519.SH", "eastmoney"))
# print(crawler.fetch("000001.SZ", "tonghuashun"))
FILE:scripts/track_logger.py
import os
from time import time
from define import BASE_URL
import config
import requests
from logger import log
class TrackLogger:
def __init__(self, file_path):
self.logger_file = file_path
self.f = open(file_path, mode="a", encoding="utf-8")
def write(self, message:str):
self.f.write(f"{message}\n")
self.f.flush()
FILE:scripts/utils.py
import tempfile
import zipfile
import os
import shutil
import requests
def get_skill_work_dir():
"""获取/创建skill专属的自定义临时目录"""
# 1. 获取系统临时目录路径(原生方法,跨平台)
system_temp_dir = tempfile.gettempdir()
# 2. 创建skill专属子目录(命名如:BitSoulStockSkill)
skill_temp_dir = os.path.join(system_temp_dir, "BitSoulStockSkill")
# 目录不存在则创建
if not os.path.exists(skill_temp_dir):
os.makedirs(skill_temp_dir, exist_ok=True)
return skill_temp_dir
def get_skill_dir():
current_file_path = os.path.abspath(__file__)
dir = os.path.dirname(os.path.dirname(current_file_path))
return dir
def get_skill_assets_dir():
return os.path.join(get_skill_dir(), "assets")
def scan_files_in_dir(dir:str):
file_list = []
# scandir 返回可迭代的 DirEntry 对象,包含文件信息
with os.scandir(dir) as entries:
for entry in entries:
# is_file(follow_symlinks=False):排除符号链接,仅判断真实文件
if entry.is_file(follow_symlinks=False):
file_list.append(entry.path) # entry.path 直接返回完整路径
return file_list
def unzip_file(zip_file_path, target_dir):
"""
将zip文件解压到指定目录
Args:
zip_file_path (str): zip压缩文件的路径
target_dir (str): 解压目标目录
Returns:
bool: 解压成功返回True,失败返回False
"""
# 确保目标目录存在,如果不存在则创建
os.makedirs(target_dir, exist_ok=True)
try:
# 以只读模式打开zip文件
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
# 解压所有文件到指定目录
zip_ref.extractall(target_dir)
return True
except zipfile.BadZipFile:
return False
except FileNotFoundError:
return False
except PermissionError:
return False
except Exception as e:
return False
def compare_version(v1: str, v2: str) -> int:
"""
比较两个版本号的大小。
支持任意段数的版本号,如 1.0、1.0.1、1.19.0 等。
每段均按整数比较,不做字符串排序。
Args:
v1 (str): 版本号A
v2 (str): 版本号B
Returns:
int: 1 表示 v1 > v2
0 表示 v1 == v2
-1 表示 v1 < v2
"""
parts1 = [int(x) for x in v1.split(".")]
parts2 = [int(x) for x in v2.split(".")]
# 补齐短版本号,末尾补 0
length = max(len(parts1), len(parts2))
parts1 += [0] * (length - len(parts1))
parts2 += [0] * (length - len(parts2))
for a, b in zip(parts1, parts2):
if a > b:
return 1
if a < b:
return -1
return 0
def download_file(url: str, dest_path: str) -> bool:
"""
从指定URL下载文件到目标路径
Args:
url (str): 文件下载链接
dest_path (str): 保存文件的完整路径(含文件名)
Returns:
bool: 下载成功返回True,失败返回False
"""
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
try:
with requests.get(url, stream=True, timeout=60) as resp:
resp.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
return True
except Exception:
return False
if __name__ == "__main__":
print(get_skill_work_dir())
FILE:scripts/formulaicAlphas/alpha101.py
"""
alpha101.py — WorldQuant《101 Formulaic Alphas》完整实现
使用方法:
from formulaicAlphas.data_loader import AlphaDataLoader
from formulaicAlphas.alpha101 import Alpha101
loader = AlphaDataLoader()
data = loader.load(codes, start_date='2025-01-01', end_date='2026-03-14')
a = Alpha101(data)
alpha1 = a.alpha001() # DataFrame: 行=日期, 列=股票代码
alpha50 = a.alpha050()
# 取最新一日横截面
latest = alpha1.iloc[-1].dropna().sort_values()
设计说明:
- 每个 alpha 方法返回与输入面板同形的 DataFrame(行=日期,列=股票)
- 参数中的小数(如 3.65595)来自机器优化,实现时直接取整
- 带 IndNeutralize 的 alpha 需传入 ind(行业面板),否则跳过中性化直接使用原值
- sum / max / min 在公式中均指时间序列滚动操作(ts_sum / ts_max / ts_min)
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from typing import Optional
from .operators import (
rank, scale, ind_neutralize,
delay, delta, ts_sum, mean, stddev,
ts_max, ts_min, ts_rank, ts_argmax, ts_argmin,
correlation, covariance, decay_linear, product,
signedpower, log, sign, abs_, adv, where,
)
Panel = pd.DataFrame
class Alpha101:
"""
WorldQuant Alpha 101 因子库。
Args:
data: AlphaDataLoader.load() 返回的字段字典,需包含:
open, high, low, close, volume, vwap, returns
可选:ind(行业面板,带 IndNeutralize 的 alpha 所需)
"""
def __init__(self, data: dict[str, Panel]):
self.open = data["open"].astype(float)
self.high = data["high"].astype(float)
self.low = data["low"].astype(float)
self.close = data["close"].astype(float)
self.volume = data["volume"].astype(float)
self.vwap = data["vwap"].astype(float)
self.returns = data["returns"].astype(float)
self.ind: Optional[Panel] = data.get("ind")
self._adv_cache: dict[int, Panel] = {}
# ── 内部辅助 ─────────────────────────────────────────────────────────────
def _adv(self, n: int) -> Panel:
"""缓存各 n 日平均成交量。"""
if n not in self._adv_cache:
self._adv_cache[n] = adv(self.volume, n)
return self._adv_cache[n]
def _ind_neu(self, x: Panel) -> Panel:
"""如有行业数据则中性化,否则原样返回。"""
if self.ind is not None:
return ind_neutralize(x, self.ind)
return x
# ── Alpha 001 ── Alpha 010 ───────────────────────────────────────────────
def alpha001(self) -> Panel:
"""rank(ts_argmax(signedpower(where(returns<0,stddev(returns,20),close),2),5)) - 0.5"""
cond = self.returns < 0
base = where(cond, stddev(self.returns, 20), self.close)
return rank(ts_argmax(signedpower(base, 2), 5)) - 0.5
def alpha002(self) -> Panel:
"""(-1) * correlation(rank(delta(log(volume),2)), rank((close-open)/open), 6)"""
x = rank(delta(log(self.volume), 2))
y = rank((self.close - self.open) / self.open)
return -1 * correlation(x, y, 6)
def alpha003(self) -> Panel:
"""(-1) * correlation(rank(open), rank(volume), 10)"""
return -1 * correlation(rank(self.open), rank(self.volume), 10)
def alpha004(self) -> Panel:
"""(-1) * ts_rank(rank(low), 9)"""
return -1 * ts_rank(rank(self.low), 9)
def alpha005(self) -> Panel:
"""rank(open - ts_sum(vwap,10)/10) * (-abs(rank(close - vwap)))"""
return rank(self.open - ts_sum(self.vwap, 10) / 10) * (-abs_(rank(self.close - self.vwap)))
def alpha006(self) -> Panel:
"""(-1) * correlation(open, volume, 10)"""
return -1 * correlation(self.open, self.volume, 10)
def alpha007(self) -> Panel:
"""if adv20 < volume: (-1)*ts_rank(abs(delta(close,7)),60)*sign(delta(close,7)) else -1"""
adv20 = self._adv(20)
d7 = delta(self.close, 7)
hit = -1 * ts_rank(abs_(d7), 60) * sign(d7)
return where(adv20 < self.volume, hit, -1)
def alpha008(self) -> Panel:
"""(-1) * rank(ts_sum(open,5)*ts_sum(returns,5) - delay(ts_sum(open,5)*ts_sum(returns,5),10))"""
prod = ts_sum(self.open, 5) * ts_sum(self.returns, 5)
return -1 * rank(prod - delay(prod, 10))
def alpha009(self) -> Panel:
"""顺势/逆势:5日内单调上涨→顺势;单调下跌→顺势;震荡→逆势"""
d1 = delta(self.close, 1)
cond_up = ts_min(d1, 5) > 0
cond_down = ts_max(d1, 5) < 0
return where(cond_up, d1, where(cond_down, d1, -d1))
def alpha010(self) -> Panel:
"""rank(alpha009_logic_with_4day_window)"""
d1 = delta(self.close, 1)
cond_up = ts_min(d1, 4) > 0
cond_down = ts_max(d1, 4) < 0
return rank(where(cond_up, d1, where(cond_down, d1, -d1)))
# ── Alpha 011 ── Alpha 020 ───────────────────────────────────────────────
def alpha011(self) -> Panel:
"""(rank(ts_max(vwap-close,3)) + rank(ts_min(vwap-close,3))) * rank(delta(volume,3))"""
vc = self.vwap - self.close
return (rank(ts_max(vc, 3)) + rank(ts_min(vc, 3))) * rank(delta(self.volume, 3))
def alpha012(self) -> Panel:
"""sign(delta(volume,1)) * (-delta(close,1))"""
return sign(delta(self.volume, 1)) * (-delta(self.close, 1))
def alpha013(self) -> Panel:
"""(-1) * rank(covariance(rank(close), rank(volume), 5))"""
return -1 * rank(covariance(rank(self.close), rank(self.volume), 5))
def alpha014(self) -> Panel:
"""(-1)*rank(delta(returns,3)) * correlation(open, volume, 10)"""
return -1 * rank(delta(self.returns, 3)) * correlation(self.open, self.volume, 10)
def alpha015(self) -> Panel:
"""(-1) * ts_sum(rank(correlation(rank(high), rank(volume), 3)), 3)"""
return -1 * ts_sum(rank(correlation(rank(self.high), rank(self.volume), 3)), 3)
def alpha016(self) -> Panel:
"""(-1) * rank(covariance(rank(high), rank(volume), 5))"""
return -1 * rank(covariance(rank(self.high), rank(self.volume), 5))
def alpha017(self) -> Panel:
"""(-1)*rank(ts_rank(close,10)) * rank(delta(delta(close,1),1)) * rank(ts_rank(vol/adv20,5))"""
adv20 = self._adv(20)
return (
-1 * rank(ts_rank(self.close, 10))
* rank(delta(delta(self.close, 1), 1))
* rank(ts_rank(self.volume / adv20, 5))
)
def alpha018(self) -> Panel:
"""(-1)*rank(stddev(abs(close-open),5) + (close-open) + correlation(close,open,10))"""
return -1 * rank(
stddev(abs_(self.close - self.open), 5)
+ (self.close - self.open)
+ correlation(self.close, self.open, 10)
)
def alpha019(self) -> Panel:
"""(-1)*sign(2*delta(close,7)) * (1 + rank(1 - ts_sum(returns,250)))"""
# (close - delay(close,7)) + delta(close,7) = 2*delta(close,7)
return -1 * sign(delta(self.close, 7)) * (1 + rank(1 - ts_sum(self.returns, 250)))
def alpha020(self) -> Panel:
"""(-1)*rank(open-delay(high,1)) * rank(open-delay(close,1)) * rank(open-delay(low,1))"""
return (
-1
* rank(self.open - delay(self.high, 1))
* rank(self.open - delay(self.close, 1))
* rank(self.open - delay(self.low, 1))
)
# ── Alpha 021 ── Alpha 030 ───────────────────────────────────────────────
def alpha021(self) -> Panel:
"""布林带判断:价格突破上轨→看空,跌破下轨→看多,中间→排名。"""
ma8 = ts_sum(self.close, 8) / 8
std8 = stddev(self.close, 8)
ma2 = ts_sum(self.close, 2) / 2
cond_up = (ma8 + std8) < ma2
cond_down = ma2 < (ma8 - std8)
middle = 1 - rank(std8 + ts_max(self.close, 8))
return where(cond_up, -1, where(cond_down, 1, middle))
def alpha022(self) -> Panel:
"""(-1) * delta(correlation(high, volume, 5), 5) * rank(stddev(close, 20))"""
return -1 * delta(correlation(self.high, self.volume, 5), 5) * rank(stddev(self.close, 20))
def alpha023(self) -> Panel:
"""if ts_mean(high,20) < high: -delta(high,2) else 0"""
cond = ts_sum(self.high, 20) / 20 < self.high
return where(cond, -delta(self.high, 2), 0)
def alpha024(self) -> Panel:
"""价格斜率<=5%: -(close - ts_min(close,100)); 否则 -delta(close,3)"""
slope = delta(ts_sum(self.close, 100) / 100, 100) / delay(self.close, 100)
cond = slope <= 0.05
return where(cond, -(self.close - ts_min(self.close, 100)), -delta(self.close, 3))
def alpha025(self) -> Panel:
"""复杂量价位置因子,结合 adv40、高低价相关性和7日价格变化。"""
adv40 = self._adv(40)
price = (2 * self.close + self.low + self.high) / 4
part1 = rank(price * (adv40 / self.volume + 1))
part2 = (
(rank(correlation(self.high, self.close, 5)) + rank(correlation(self.low, self.close, 5))) / 2
+ rank(delta(self.close, 7))
)
return part1 * part2
def alpha026(self) -> Panel:
"""(-1) * (rank(ts_rank(exp(close),1)) - rank(rank(correlation(high,volume,5))))"""
return -1 * (rank(ts_rank(np.exp(self.close), 1)) - rank(rank(correlation(self.high, self.volume, 5))))
def alpha027(self) -> Panel:
"""if rank(mean(correlation(rank(volume),rank(vwap),6),2)) > 0.5: -1 else 1"""
cond = rank(mean(correlation(rank(self.volume), rank(self.vwap), 6), 2)) > 0.5
return where(cond, -1, 1)
def alpha028(self) -> Panel:
"""scale(correlation(adv20, low, 5) + (high+low)/2 - close)"""
adv20 = self._adv(20)
return scale(correlation(adv20, self.low, 5) + (self.high + self.low) / 2 - self.close)
def alpha029(self) -> Panel:
"""ts_min(rank(rank(scale(log(ts_sum(rank(rank(-rank(delta(close,5)))),2))))),5)
+ ts_rank(delay(-returns,6),5)"""
inner = -rank(delta(self.close, 5))
part1 = ts_min(rank(rank(scale(log(ts_sum(rank(rank(inner)), 2))))), 5)
part2 = ts_rank(delay(-self.returns, 6), 5)
return part1 + part2
def alpha030(self) -> Panel:
"""(1 - rank(sign_sum_3days)) * ts_sum(vol,5) / ts_sum(vol,20)"""
s = (
sign(self.close - delay(self.close, 1))
+ sign(delay(self.close, 1) - delay(self.close, 2))
+ sign(delay(self.close, 2) - delay(self.close, 3))
)
return (1 - rank(s)) * ts_sum(self.volume, 5) / ts_sum(self.volume, 20)
# ── Alpha 031 ── Alpha 040 ───────────────────────────────────────────────
def alpha031(self) -> Panel:
"""(-1)*(rank(correlation(close,adv10,5)) - rank(delta(delta(close,1),1)))
* rank(close - ts_min(close,10) + 0.001)"""
adv10 = self._adv(10)
return (
-1
* (rank(correlation(self.close, adv10, 5)) - rank(delta(delta(self.close, 1), 1)))
* rank(self.close - ts_min(self.close, 10) + 0.001)
)
def alpha032(self) -> Panel:
"""scale(rank((ts_sum(close,7)/7 - close))) + 2*rank(correlation(vwap,delay(close,5),230))"""
return (
scale(rank(ts_sum(self.close, 7) / 7 - self.close))
+ 2 * rank(correlation(self.vwap, delay(self.close, 5), 230))
)
def alpha033(self) -> Panel:
"""rank(-(1 - open/close)^2)"""
return rank(-signedpower(1 - self.open / self.close, 2))
def alpha034(self) -> Panel:
"""(-1)*((rank(open - ts_max(close,2)) + rank(open - ts_min(close,2)))
* rank((open - delay(close,1)) + (delay(close,1) - close)))"""
return -1 * (
(rank(self.open - ts_max(self.close, 2)) + rank(self.open - ts_min(self.close, 2)))
* rank((self.open - delay(self.close, 1)) + (delay(self.close, 1) - self.close))
)
def alpha035(self) -> Panel:
"""(-1)*(rank(volume)*(1-rank(close-low)) + rank(open-low)*(1-rank(returns)))"""
return -1 * (
rank(self.volume) * (1 - rank(self.close - self.low))
+ rank(self.open - self.low) * (1 - rank(self.returns))
)
def alpha036(self) -> Panel:
"""2.21*rank(correlation(open,volume,5)) - 0.01*rank(delta(returns,3))
+ 1.54*rank(open - low)"""
# Note: paper also includes vwap & low terms; using best-effort interpretation
adv20 = self._adv(20)
return (
2.21 * rank(correlation(self.open, self.volume, 5))
- 0.01 * rank(delta(self.returns, 3))
+ 1.54 * rank(self.open - self.low)
)
def alpha037(self) -> Panel:
"""rank(correlation(delay(open-close,1), close, 200)) + rank(open-close)"""
return rank(correlation(delay(self.open - self.close, 1), self.close, 200)) + rank(self.open - self.close)
def alpha038(self) -> Panel:
"""(-1)*rank(ts_rank(close,10)) * rank(close/open)"""
return -1 * rank(ts_rank(self.close, 10)) * rank(self.close / self.open)
def alpha039(self) -> Panel:
"""(-1)*rank(delta(close,7) * (1 - rank(decay_linear(volume/adv20,9))))
* (1 + rank(ts_sum(returns,250)))"""
adv20 = self._adv(20)
return (
-1 * rank(delta(self.close, 7) * (1 - rank(decay_linear(self.volume / adv20, 9))))
* (1 + rank(ts_sum(self.returns, 250)))
)
def alpha040(self) -> Panel:
"""(-1)*rank(stddev(high,10)) * correlation(high, volume, 10)"""
return -1 * rank(stddev(self.high, 10)) * correlation(self.high, self.volume, 10)
# ── Alpha 041 ── Alpha 050 ───────────────────────────────────────────────
def alpha041(self) -> Panel:
"""(high*low)^0.5 - vwap"""
return (self.high * self.low) ** 0.5 - self.vwap
def alpha042(self) -> Panel:
"""(vwap - close) / (vwap + close) * 0.5"""
return (self.vwap - self.close) / (self.vwap + self.close) * 0.5
def alpha043(self) -> Panel:
"""ts_rank(volume/adv20, 20) * ts_rank(-delta(close,7), 8)"""
adv20 = self._adv(20)
return ts_rank(self.volume / adv20, 20) * ts_rank(-delta(self.close, 7), 8)
def alpha044(self) -> Panel:
"""(-1) * correlation(high, rank(volume), 5)"""
return -1 * correlation(self.high, rank(self.volume), 5)
def alpha045(self) -> Panel:
"""(-1) * rank(ts_sum(delay(close,5),20)/20) * correlation(close,volume,2)
* rank(correlation(ts_sum(close,5), ts_sum(close,20), 2))"""
return (
-1
* rank(ts_sum(delay(self.close, 5), 20) / 20)
* correlation(self.close, self.volume, 2)
* rank(correlation(ts_sum(self.close, 5), ts_sum(self.close, 20), 2))
)
def alpha046(self) -> Panel:
"""比较两段价格斜率:(d20-d10)/10 vs (d10-now)/10"""
slope_far = (delay(self.close, 20) - delay(self.close, 10)) / 10
slope_near = (delay(self.close, 10) - self.close) / 10
diff = slope_far - slope_near
cond_sell = diff > 0.25
cond_buy = diff < 0
return where(cond_sell, -1, where(cond_buy, 1, -delta(self.close, 1)))
def alpha047(self) -> Panel:
"""复杂多因子:价格倒数×量/adv20 × 高价位置 / 5日均高 - rank(vwap变化)"""
adv20 = self._adv(20)
part1 = (
(1 / self.close)
* self.volume
/ adv20
* self.high
* rank(self.high - self.close)
/ (ts_sum(self.high, 5) / 5)
)
return rank(part1) - rank(delta(self.vwap, 5))
def alpha048(self) -> Panel:
"""类似alpha030但加行业中性化"""
s = (
sign(self.close - delay(self.close, 1))
+ sign(delay(self.close, 1) - delay(self.close, 2))
+ sign(delay(self.close, 2) - delay(self.close, 3))
)
return (
-1 * rank(s) * ts_sum(self.volume, 5) / ts_sum(self.volume, 20)
* self._ind_neu(self.close)
)
def alpha049(self) -> Panel:
"""价格斜率差 < -1: 1; 否则 -delta(close,1)"""
slope_far = (delay(self.close, 20) - delay(self.close, 10)) / 10
slope_near = (delay(self.close, 10) - self.close) / 10
cond = (slope_far - slope_near) < -1
return where(cond, 1, -delta(self.close, 1))
def alpha050(self) -> Panel:
"""(-1) * ts_max(rank(correlation(rank(volume), rank(vwap), 5)), 5)"""
return -1 * ts_max(rank(correlation(rank(self.volume), rank(self.vwap), 5)), 5)
# ── Alpha 051 ── Alpha 060 ───────────────────────────────────────────────
def alpha051(self) -> Panel:
"""价格斜率差 < -0.05: 1; 否则 -1"""
slope_far = (delay(self.close, 20) - delay(self.close, 10)) / 10
slope_near = (delay(self.close, 10) - self.close) / 10
cond = (slope_far - slope_near) < -0.05
return where(cond, 1, -1)
def alpha052(self) -> Panel:
"""(ts_min(low,5)变化量) * rank(长期收益加速) * ts_rank(volume,5)"""
low_chg = -ts_min(self.low, 5) + delay(ts_min(self.low, 5), 5)
ret_accel = rank((ts_sum(self.returns, 240) - ts_sum(self.returns, 20)) / 220)
return low_chg * ret_accel * ts_rank(self.volume, 5)
def alpha053(self) -> Panel:
"""(-1) * delta((close-low - (high-close)) / (close-low+1e-8), 9)"""
ratio = (self.close - self.low - (self.high - self.close)) / (self.close - self.low + 1e-8)
return -1 * delta(ratio, 9)
def alpha054(self) -> Panel:
"""(-1) * (low-close)*(open^5) / ((low-high)*(close^5) + 1e-8)"""
denom = (self.low - self.high) * (self.close ** 5) + 1e-8
return -1 * (self.low - self.close) * (self.open ** 5) / denom
def alpha055(self) -> Panel:
"""(-1) * correlation(rank((close-ts_min(low,12))/(ts_max(high,12)-ts_min(low,12)+1e-8)),
rank(volume), 6)"""
stoch = (self.close - ts_min(self.low, 12)) / (ts_max(self.high, 12) - ts_min(self.low, 12) + 1e-8)
return -1 * correlation(rank(stoch), rank(self.volume), 6)
def alpha056(self) -> Panel:
"""(-1)*(rank(ts_sum(returns,5)-ts_sum(returns,20)) + rank(close-ts_min(close,5)))
* IndNeutralize(close, ind)"""
part = rank(ts_sum(self.returns, 5) - ts_sum(self.returns, 20)) + rank(self.close - ts_min(self.close, 5))
return -1 * part * self._ind_neu(self.close)
def alpha057(self) -> Panel:
"""0 - (1 - rank((close-ts_min(close,5))/(ts_max(close,5)-ts_min(close,5)+1e-8)))"""
stoch = (self.close - ts_min(self.close, 5)) / (ts_max(self.close, 5) - ts_min(self.close, 5) + 1e-8)
return 0 - (1 - rank(stoch))
def alpha058(self) -> Panel:
"""(-1)*ts_rank(decay_linear(correlation(IndNeutralize(vwap,ind),volume,4),8),6)"""
vwap_n = self._ind_neu(self.vwap)
return -1 * ts_rank(decay_linear(correlation(vwap_n, self.volume, 4), 8), 6)
def alpha059(self) -> Panel:
"""类似alpha058,vwap经加权(0.728317)后中性化"""
vwap_w = self.vwap * 0.728317 + self.vwap * (1 - 0.728317) # == self.vwap
vwap_n = self._ind_neu(vwap_w)
return -1 * ts_rank(decay_linear(correlation(vwap_n, self.volume, 4), 16), 8)
def alpha060(self) -> Panel:
"""(-1)*rank(ts_rank(correlation(rank(high),rank(volume),9),14))
* rank(ts_rank(correlation(rank(low),rank(volume),7),8))"""
c1 = rank(ts_rank(correlation(rank(self.high), rank(self.volume), 9), 14))
c2 = rank(ts_rank(correlation(rank(self.low), rank(self.volume), 7), 8))
return -1 * c1 * c2
# ── Alpha 061 ── Alpha 070 ───────────────────────────────────────────────
def alpha061(self) -> Panel:
"""(-1)*rank(correlation(rank(vwap),rank(volume),4))
* rank(correlation(rank(low),rank(volume),5))"""
c1 = rank(correlation(rank(self.vwap), rank(self.volume), 4))
c2 = rank(correlation(rank(self.low), rank(self.volume), 5))
return -1 * c1 * c2
def alpha062(self) -> Panel:
"""if rank(corr(vwap,adv20,10)) < rank(corr(rank(low),rank(adv50),17)): -1 else 1"""
adv20 = self._adv(20)
adv50 = self._adv(50)
cond = rank(correlation(self.vwap, adv20, 10)) < rank(correlation(rank(self.low), rank(adv50), 17))
return where(cond, -1, 1)
def alpha063(self) -> Panel:
"""类似alpha062,vwap行业中性化后比较"""
adv50 = self._adv(50)
vwap_n = self._ind_neu(self.vwap * 0.724108 + self.vwap * (1 - 0.724108))
c1 = rank(correlation(vwap_n, rank(self.volume), 6))
c2 = rank(correlation(rank(self.low), rank(adv50), 20))
return where(c1 < c2, -1, 1)
def alpha064(self) -> Panel:
"""比较复合量的相关性排名"""
adv40 = self._adv(40)
adv50 = self._adv(50)
price = self.open * 0.178404 + self.low * (1 - 0.178404)
c1 = rank(correlation(ts_sum(price, 13), ts_sum(adv40, 13), 5))
c2 = rank(correlation(rank(self.low), rank(adv50), 12))
return where(c1 < c2, -1, 1)
def alpha065(self) -> Panel:
"""if rank(corr(open,vol,11)) < rank(corr(rank(low),rank(adv30),15)): -1 else 1"""
adv30 = self._adv(30)
c1 = rank(correlation(self.open, self.volume, 11))
c2 = rank(correlation(rank(self.low), rank(adv30), 15))
return where(c1 < c2, -1, 1)
def alpha066(self) -> Panel:
"""if rank(corr(open,vol,5)) < rank(corr(rank(vwap),rank(vol),6)): -1 else 1"""
c1 = rank(correlation(self.open, self.volume, 5))
c2 = rank(correlation(rank(self.vwap), rank(self.volume), 6))
return where(c1 < c2, -1, 1)
def alpha067(self) -> Panel:
"""if rank(corr(IndNeutralize(high,ind),vol,8)) < rank(corr(rank(low),rank(adv30),18)): -1 else 1"""
adv30 = self._adv(30)
high_n = self._ind_neu(self.high)
c1 = rank(correlation(high_n, self.volume, 8))
c2 = rank(correlation(rank(self.low), rank(adv30), 18))
return where(c1 < c2, -1, 1)
def alpha068(self) -> Panel:
"""比较两个ts_rank后相关性的排名"""
adv20 = self._adv(20)
c1 = rank(correlation(ts_rank(self.high, 3), ts_rank(self.volume, 3), 4))
c2 = rank(correlation(rank(self.low), rank(adv20), 14))
return where(c1 < c2, -1, 1)
def alpha069(self) -> Panel:
"""if rank(corr(rank(vwap),rank(vol),12)) < rank(corr(rank(low),adv40,19)): -1 else 1"""
adv40 = self._adv(40)
c1 = rank(correlation(rank(self.vwap), rank(self.volume), 12))
c2 = rank(correlation(rank(self.low), adv40, 19))
return where(c1 < c2, -1, 1)
def alpha070(self) -> Panel:
"""(-1) * rank(delta(vwap,1))^ts_rank(corr(IndNeutralize(close,ind),adv50,18),18)"""
adv50 = self._adv(50)
close_n = self._ind_neu(self.close)
exp = ts_rank(correlation(close_n, adv50, 18), 18)
base = rank(delta(self.vwap, 1))
return -1 * base ** exp
# ── Alpha 071 ── Alpha 080 ───────────────────────────────────────────────
def alpha071(self) -> Panel:
"""max(rank(decay_linear(corr(ts_rank(close,3),ts_rank(adv50,3),12),7)),
rank(decay_linear((h+l)/2+open-close, 15)))"""
adv50 = self._adv(50)
p1 = rank(decay_linear(correlation(ts_rank(self.close, 3), ts_rank(adv50, 3), 12), 7))
p2 = rank(decay_linear((self.high + self.low) / 2 + self.open - self.close, 15))
return p1.combine(p2, np.maximum)
def alpha072(self) -> Panel:
"""rank(decay_linear(corr(rank(high),rank(adv30),9),4))
- rank(decay_linear(corr(rank(low),rank(adv30),10),8))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(rank(self.high), rank(adv30), 9), 4))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 10), 8))
return p1 - p2
def alpha073(self) -> Panel:
"""rank(decay_linear(delta(vwap,3),2)) + rank(decay_linear((-low+open)/(high-low+1e-8),1))"""
ratio = (-self.low + self.open) / (self.high - self.low + 1e-8)
return rank(decay_linear(delta(self.vwap, 3), 2)) + rank(decay_linear(ratio, 1))
def alpha074(self) -> Panel:
"""(-1)*(rank(corr(low,adv30,17)) + rank(delay(rank(open+close),12))
- rank(decay_linear(corr(rank(vwap),rank(adv50),7),18)))"""
adv30 = self._adv(30)
adv50 = self._adv(50)
p1 = rank(correlation(self.low, adv30, 17))
p2 = rank(delay(rank(self.open + self.close), 12))
p3 = rank(decay_linear(correlation(rank(self.vwap), rank(adv50), 7), 18))
return -1 * (p1 + p2 - p3)
def alpha075(self) -> Panel:
"""(rank(corr(rank(vwap),rank(vol),4)) + rank(corr(rank(low),rank(adv50),19)))
- rank(decay_linear((high+low)/2, 12))"""
adv50 = self._adv(50)
p1 = rank(correlation(rank(self.vwap), rank(self.volume), 4))
p2 = rank(correlation(rank(self.low), rank(adv50), 19))
p3 = rank(decay_linear((self.high + self.low) / 2, 12))
return p1 + p2 - p3
def alpha076(self) -> Panel:
"""(-rank(decay_linear(delta(vwap,2),16))
- rank(decay_linear(-(rank(close+open-low)-rank(vwap)+rank(close-vwap)),17)))
* IndNeutralize(close,ind)"""
inner = -(rank(self.close + self.open - self.low) - rank(self.vwap) + rank(self.close - self.vwap))
p1 = -rank(decay_linear(delta(self.vwap, 2), 16))
p2 = -rank(decay_linear(inner, 17))
return (p1 + p2) * self._ind_neu(self.close)
def alpha077(self) -> Panel:
"""(-1)*rank(decay_linear((h+l)/2+high-vwap-high,2))
+ rank(decay_linear(corr(rank(low),rank(adv50),20),9))"""
adv50 = self._adv(50)
ratio = (self.high + self.low) / 2 + self.high - self.vwap - self.high
p1 = -rank(decay_linear(ratio, 2))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv50), 20), 9))
return p1 + p2
def alpha078(self) -> Panel:
"""(-1)*rank(decay_linear(corr(rank(vwap),rank(adv20),4),5))
- rank(decay_linear(-low,9)) + rank(decay_linear(open,16))"""
adv20 = self._adv(20)
p1 = -rank(decay_linear(correlation(rank(self.vwap), rank(adv20), 4), 5))
p2 = -rank(decay_linear(-self.low, 9))
p3 = rank(decay_linear(self.open, 16))
return p1 + p2 + p3
def alpha079(self) -> Panel:
"""rank(delta(IndNeutralize(close,ind),2)) * rank(corr(IndNeutralize(vwap,ind),adv50,15))
- rank(decay_linear(-low,4))"""
adv50 = self._adv(50)
close_n = self._ind_neu(self.close)
vwap_n = self._ind_neu(self.vwap)
p1 = rank(delta(close_n, 2)) * rank(correlation(vwap_n, adv50, 15))
p2 = rank(decay_linear(-self.low, 4))
return p1 - p2
def alpha080(self) -> Panel:
"""(-1)*(rank(sign(delta(IndNeutralize(open,ind),2))*sign(delta(IndNeutralize(close,ind),3)))
+ rank(ts_rank(volume,5)))"""
open_n = self._ind_neu(self.open)
close_n = self._ind_neu(self.close)
p1 = rank(sign(delta(open_n, 2)) * sign(delta(close_n, 3)))
p2 = rank(ts_rank(self.volume, 5))
return -1 * (p1 + p2)
# ── Alpha 081 ── Alpha 090 ───────────────────────────────────────────────
def alpha081(self) -> Panel:
"""(-1)*(rank(log(product(rank(corr(rank(high),rank(adv10),8)),2))) - 0.5)"""
adv10 = self._adv(10)
return -1 * (rank(log(product(rank(correlation(rank(self.high), rank(adv10), 8)), 2))) - 0.5)
def alpha082(self) -> Panel:
"""(-1)*(rank(log(rank(corr(rank(high),rank(adv10),8))))
+ rank(ts_rank(vol,6)) - rank(corr(open,vol,6)))"""
adv10 = self._adv(10)
p1 = rank(log(rank(correlation(rank(self.high), rank(adv10), 8))))
p2 = rank(ts_rank(self.volume, 6))
p3 = rank(correlation(self.open, self.volume, 6))
return -1 * (p1 + p2 - p3)
def alpha083(self) -> Panel:
"""rank(delay(high,5)*sign(delta(close,5))) + rank(vwap-close)
- rank(corr(open,vol,13))"""
p1 = rank(delay(self.high, 5) * sign(delta(self.close, 5)))
p2 = rank(self.vwap - self.close)
p3 = rank(correlation(self.open, self.volume, 13))
return p1 + p2 - p3
def alpha084(self) -> Panel:
"""rank(corr(vwap,adv20,6)) + rank(corr(open,vol,15))"""
adv20 = self._adv(20)
return rank(correlation(self.vwap, adv20, 6)) + rank(correlation(self.open, self.volume, 15))
def alpha085(self) -> Panel:
"""(rank(corr(close,adv15,9)) + rank(corr(rank(high),rank(vol),11)))
- rank(delta(close,6))"""
adv15 = self._adv(15)
p1 = rank(correlation(self.close, adv15, 9))
p2 = rank(correlation(rank(self.high), rank(self.volume), 11))
p3 = rank(delta(self.close, 6))
return p1 + p2 - p3
def alpha086(self) -> Panel:
"""(rank(corr(close,adv30,7)) + rank(corr(open,vol,13))) - rank(delta(close,6))"""
adv30 = self._adv(30)
p1 = rank(correlation(self.close, adv30, 7))
p2 = rank(correlation(self.open, self.volume, 13))
p3 = rank(delta(self.close, 6))
return p1 + p2 - p3
def alpha087(self) -> Panel:
"""max(rank(delta(close,3)),rank(ts_rank(vol*0.47,5)))
- min(rank(delta(adv50,2)),rank(corr(IndNeutralize(vwap,ind),vol,11)))"""
adv50 = self._adv(50)
vwap_n = self._ind_neu(self.vwap)
hi = (rank(delta(self.close, 3))).combine(rank(ts_rank(self.volume * 0.47, 5)), np.maximum)
lo = (rank(delta(adv50, 2))).combine(rank(correlation(vwap_n, self.volume, 11)), np.minimum)
return hi - lo
def alpha088(self) -> Panel:
"""min(rank(decay_linear(delta(open,2),3)), rank(corr(close,vol,20)))
+ rank(decay_linear(open-low,2))"""
p_min = (rank(decay_linear(delta(self.open, 2), 3))).combine(
rank(correlation(self.close, self.volume, 20)), np.minimum
)
return p_min + rank(decay_linear(self.open - self.low, 2))
def alpha089(self) -> Panel:
"""(-1)*(rank(ts_rank(decay_linear(corr(IndNeutralize(vwap,ind),vol,4),8),7))
- rank(decay_linear(ts_rank(corr(rank(low),rank(adv30),12),19),16))"""
adv30 = self._adv(30)
vwap_n = self._ind_neu(self.vwap)
p1 = rank(ts_rank(decay_linear(correlation(vwap_n, self.volume, 4), 8), 7))
p2 = rank(decay_linear(ts_rank(correlation(rank(self.low), rank(adv30), 12), 19), 16))
return -1 * (p1 - p2)
def alpha090(self) -> Panel:
"""rank(decay_linear(corr(rank(vwap),rank(vol),14),5))
- rank(decay_linear(ts_rank(ts_min(low,5),19),13))
+ rank(corr(IndNeutralize(close,ind),vol,16))"""
close_n = self._ind_neu(self.close)
p1 = rank(decay_linear(correlation(rank(self.vwap), rank(self.volume), 14), 5))
p2 = rank(decay_linear(ts_rank(ts_min(self.low, 5), 19), 13))
p3 = rank(correlation(close_n, self.volume, 16))
return p1 - p2 + p3
# ── Alpha 091 ── Alpha 101 ───────────────────────────────────────────────
def alpha091(self) -> Panel:
"""(-1)*(rank(ts_rank(decay_linear(decay_linear(corr(IndNeutralize(close,ind),vol,3),7),6),4))
- rank(decay_linear(ts_rank(corr(rank(vwap),rank(adv30),19),12),20))"""
adv30 = self._adv(30)
close_n = self._ind_neu(self.close)
inner = decay_linear(decay_linear(correlation(close_n, self.volume, 3), 7), 6)
p1 = rank(ts_rank(inner, 4))
p2 = rank(decay_linear(ts_rank(correlation(rank(self.vwap), rank(adv30), 19), 12), 20))
return -1 * (p1 - p2)
def alpha092(self) -> Panel:
"""min(rank(decay_linear(cond_close_lt_lo_open, 15)),
rank(decay_linear(corr(rank(high),rank(adv30),14),16)))"""
adv30 = self._adv(30)
cond = (self.high + self.low) / 2 + self.close < self.low + self.open
p1 = rank(decay_linear(cond.astype(float), 15))
p2 = rank(decay_linear(correlation(rank(self.high), rank(adv30), 14), 16))
return p1.combine(p2, np.minimum)
def alpha093(self) -> Panel:
"""(5/6)*ts_rank(decay_linear(corr(IndNeutralize(vwap,ind),adv50,19),8),6)
+ (1/6)*rank(decay_linear(corr(rank(vwap),rank(vol),20),4))"""
adv50 = self._adv(50)
vwap_n = self._ind_neu(self.vwap)
p1 = ts_rank(decay_linear(correlation(vwap_n, adv50, 19), 8), 6)
p2 = rank(decay_linear(correlation(rank(self.vwap), rank(self.volume), 20), 4))
return (5 / 6) * p1 + (1 / 6) * p2
def alpha094(self) -> Panel:
"""(rank(decay_linear(corr(vwap,low,18),12))
- rank(decay_linear(corr(rank(low),rank(adv30),14),16)))
+ rank(decay_linear(delta(vwap,3),16))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(self.vwap, self.low, 18), 12))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 14), 16))
p3 = rank(decay_linear(delta(self.vwap, 3), 16))
return p1 - p2 + p3
def alpha095(self) -> Panel:
"""(rank(decay_linear(corr(open,vol,13),18))
- rank(decay_linear(corr(rank(low),rank(adv30),16),12)))
+ rank(decay_linear(delta(vwap,2),17))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(self.open, self.volume, 13), 18))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 16), 12))
p3 = rank(decay_linear(delta(self.vwap, 2), 17))
return p1 - p2 + p3
def alpha096(self) -> Panel:
"""(rank(decay_linear(corr(vwap,vol,17),18))
- rank(decay_linear(corr(rank(low),rank(adv30),18),20)))
+ rank(decay_linear(delta(vwap,3),20))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(self.vwap, self.volume, 17), 18))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 18), 20))
p3 = rank(decay_linear(delta(self.vwap, 3), 20))
return p1 - p2 + p3
def alpha097(self) -> Panel:
"""(-1)*(rank(decay_linear(delta(IndNeutralize(close,ind),3),16))
- rank(decay_linear(corr(IndNeutralize(vwap,ind),vol,20),18)))"""
close_n = self._ind_neu(self.close)
vwap_n = self._ind_neu(self.vwap)
p1 = rank(decay_linear(delta(close_n, 3), 16))
p2 = rank(decay_linear(correlation(vwap_n, self.volume, 20), 18))
return -1 * (p1 - p2)
def alpha098(self) -> Panel:
"""rank(decay_linear(corr(vwap,ts_sum(adv5,26),5),8))
- rank(decay_linear(ts_rank(ts_argmin(corr(rank(open),rank(adv15),21),9),7),8))"""
adv5 = self._adv(5)
adv15 = self._adv(15)
p1 = rank(decay_linear(correlation(self.vwap, ts_sum(adv5, 26), 5), 8))
p2 = rank(decay_linear(ts_rank(ts_argmin(correlation(rank(self.open), rank(adv15), 21), 9), 7), 8))
return p1 - p2
def alpha099(self) -> Panel:
"""(rank(decay_linear(corr(high,vol,20),18))
- rank(decay_linear(corr(low,vol,20),20)))
+ rank(decay_linear(corr(low,vol,9),1))"""
p1 = rank(decay_linear(correlation(self.high, self.volume, 20), 18))
p2 = rank(decay_linear(correlation(self.low, self.volume, 20), 20))
p3 = rank(decay_linear(correlation(self.low, self.volume, 9), 1))
return p1 - p2 + p3
def alpha100(self) -> Panel:
"""(rank(decay_linear(delta(vwap,2),20))
+ rank(decay_linear(corr(IndNeutralize(vwap,ind),vol,20),18)))
- rank(decay_linear(delta(close,2),5))"""
vwap_n = self._ind_neu(self.vwap)
p1 = rank(decay_linear(delta(self.vwap, 2), 20))
p2 = rank(decay_linear(correlation(vwap_n, self.volume, 20), 18))
p3 = rank(decay_linear(delta(self.close, 2), 5))
return p1 + p2 - p3
def alpha101(self) -> Panel:
"""(close - open) / (high - low + 0.001)
当日K线实体长度与日内振幅的比值,衡量动量效率。"""
return (self.close - self.open) / (self.high - self.low + 0.001)
# ── 批量计算 ─────────────────────────────────────────────────────────────
def compute_all(self, alphas: list[int] | None = None) -> dict[str, Panel]:
if alphas is None:
alphas = list(range(1, 102))
results: dict[str, Panel] = {}
for n in alphas:
name = f"alpha{n:03d}"
method = getattr(self, name, None)
if method is None:
continue
try:
results[name] = method()
except Exception as exc:
results[name] = pd.DataFrame(
np.nan,
index=self.close.index,
columns=self.close.columns,
)
import warnings
warnings.warn(f"{name} computation failed: {exc}")
return results
# ── 因子说明字典(供外部调用)────────────────────────────────────────────────
ALPHA_DESCRIPTIONS: dict[str, str] = {
"alpha001": "收益率为负时用波动率替代价格,对5日最大argmax排名,捕捉下行风险中的波动偏好。值越高→预期下跌(反向因子),高值表示股票在恐慌时期仍被追捧,短期过热",
"alpha002": "成交量二阶差分排名与收益排名的负相关,捕捉量价背离。值越高→预期下跌(反向因子),高值表示量增但价格未跟上,上涨动能衰竭",
"alpha003": "开盘价横截面排名与成交量排名的负相关。值越高→预期下跌(反向因子),高值表示开盘价在市场中偏高而成交量相对偏小,高价低量超买",
"alpha004": "低价时序排名取反,衡量低价的相对历史强度。值越高→预期下跌(反向因子),高值表示当前低价相对历史偏高,下行空间较大",
"alpha005": "开盘与10日均价偏离 × 收盘与均价偏离的乘积。值越高→预期下跌(反向因子),高值表示开盘和收盘双双偏离均价,价格虚高",
"alpha006": "开盘价与成交量的10日负相关,捕捉放量高开的反转信号。值越高→预期下跌(反向因子),高值表示开盘价与量呈强负相关,放量高开后易回落",
"alpha007": "量超均量时取价差方向×波动排名,否则取反,量能突破动量。值越高→预期上涨(正向因子),高值表示放量上涨,量能支撑价格动量",
"alpha008": "5日开盘×收益积的10日变化取反,捕捉短期量价加速反转。值越高→预期下跌(反向因子),高值表示近期开盘收益积加速上升,短期超买",
"alpha009": "5日价格单调涨跌时顺势,震荡时逆势,趋势与均值回归切换。值越高→预期上涨(正向因子),高值在趋势行情中表示强势,震荡中表示超卖反弹",
"alpha010": "alpha009的4日排名版,短周期趋势识别。值越高→预期上涨(正向因子),高值表示短期趋势或超卖反弹机会",
"alpha011": "vwap与收盘差极值排名之和 × 成交量变化,量价极值动量。值越高→预期上涨(正向因子),高值表示vwap偏离大且成交量放大,多头动能强",
"alpha012": "成交量变化方向 × 价格反向变化,量增价跌反转信号。值越高→预期上涨(反转因子),高值表示量增价跌,短期超卖后反弹概率高",
"alpha013": "收盘价与成交量排名协方差取反,量价协同性逆向因子。值越高→预期下跌(反向因子),高值表示量价协同上涨强烈,短期超买",
"alpha014": "收益3日变化排名取反 × 开盘量相关,动量衰减与量价信号。值越高→预期下跌(反向因子),高值表示近期涨速快且量价背离,动量衰竭",
"alpha015": "高价量3日相关排名的3日累加取反,量价相关持续性反转。值越高→预期下跌(反向因子),高值表示高价伴随高量持续,短期过热后回调",
"alpha016": "高价与成交量排名协方差取反,高位量价协同反转。值越高→预期下跌(反向因子),高值表示高价高量协同强,超买反转信号",
"alpha017": "价格趋势排名 × 价格加速度 × 成交量比排名三重乘积。值越高→预期下跌(反向因子),三重趋势叠加后过热,均值回归",
"alpha018": "振幅标准差+实体方向+量价相关综合取反,波动与动量综合反转。值越高→预期下跌(反向因子),高值表示波动大、阳线多且量价同向,热点股超买",
"alpha019": "7日价格方向 × 长期累计收益排名取反,趋势延续性反转。值越高→预期下跌(反向因子),高值表示近期上涨且长期涨幅大,强势股均值回归",
"alpha020": "开盘与前日高收低之差三乘积取反,跳空反转信号。值越高→预期下跌(反向因子),高值表示向上跳空动能强,缺口易被回补",
"alpha021": "布林带位置判断:突破上轨=−1看空,跌破下轨=+1看多,中间取均量排名。值越高→预期上涨(条件正向因子),高值(+1)表示跌破下轨超卖,反弹概率高",
"alpha022": "高价量5日相关的5日变化 × 收盘波动率,量价相关动量衰减。值越高→预期下跌(反向因子),高值表示高价量相关性近期大幅增加,短期热度过高",
"alpha023": "当日高价高于20日均高时,取2日高价变化的反向排名。值越高→预期下跌(反向因子),高值表示在高价突破后近期高价仍在上升,超买信号",
"alpha024": "斜率平缓时距最低点距离反转,斜率陡时3日变化反转。值越高→预期下跌(反向因子),高值表示价格高于近期低点或近期涨速过快,回调信号",
"alpha025": "量比调整价格排名 × 高低价相关排名 × 7日价格变化。值越高→预期上涨(正向因子),高值表示量比支撑下价格动量强,多因子共振看涨",
"alpha026": "价格时序排名 vs 高价量双重排名之差,价格趋势与量价分歧。值越高→预期上涨(正向因子),高值表示价格趋势强于量价相关性,纯价格动量信号",
"alpha027": "量与vwap 6日相关排名超0.5则返回−1看空,量价中期相关判断。值越高→预期下跌(反向因子),高值(>−1)时量vwap相关弱,但−1时量价同步上涨后反转",
"alpha028": "均量与低价5日相关 + 中间价偏离收盘标准化,量价位置综合。值越高→预期上涨(正向因子),高值表示均量支撑下价格位置偏低,具有上涨空间",
"alpha029": "多层嵌套价格排名 + 延迟收益时序排名,复合价格动量衰减。值越高→预期下跌(反向因子),高值表示多周期价格排名叠加高位,综合超买",
"alpha030": "3日价格方向符号排名 × 短期/长期量比,量价方向与量比结合。值越高→预期上涨(正向因子),高值表示近期上涨且短期放量,量价配合看涨",
"alpha031": "(价格均量相关−价格加速度) × 价格离低点距离排名。值越高→预期上涨(正向因子),高值表示量能支撑价格且当前远离低点,多头格局",
"alpha032": "7日均价偏离标准化 + 2×vwap与延迟价格230日相关排名。值越高→预期上涨(正向因子),高值表示均价偏低且vwap长期趋势向上,价值低估信号",
"alpha033": "开收比反向幂次排名,日内阴线(跌)放大信号。值越高→预期上涨(反转因子),高值表示收盘低于开盘幅度大,超卖后反弹概率高",
"alpha034": "开盘与近期高低价差三重排名之积取反,开盘跳空反转。值越高→预期下跌(反向因子),高值表示开盘价在高低价中位置偏高,跳空高开反转",
"alpha035": "量排名×收盘离低反向 + 开收方向×收益,综合量价反转。值越高→预期下跌(反向因子),高值表示高量且价格偏高,量价双重超买",
"alpha036": "开量相关×2.21 − 收益变化×0.01 + 开低差×1.54 线性组合。值越高→预期上涨(正向因子),高值表示开盘量能配合且开盘接近低价,支撑强",
"alpha037": "前日开收差与今日收盘200日相关 + 开收差排名,隔日价格传导。值越高→预期上涨(正向因子),高值表示前日阳线动量通过量价相关传导,延续性强",
"alpha038": "价格趋势排名 × 收开比排名取反,趋势中相对强度反转。值越高→预期下跌(反向因子),高值表示价格趋势向上但收盘相对开盘偏弱,动能衰减",
"alpha039": "7日价格变化×量比因子排名 × 长期累计收益取反。值越高→预期下跌(反向因子),高值表示短期和长期均强势,长牛股均值回归风险",
"alpha040": "高价10日波动率排名 × 高价量10日相关取反,高价波动量价背离。值越高→预期下跌(反向因子),高值表示高价波动大且量价同向,波动放大后超买",
"alpha041": "高低价几何均值 − vwap,价格重心与均价偏离。值越高→预期上涨(正向因子),高值表示高低价均值高于vwap,买方在均价之上交易,多头强势",
"alpha042": "(vwap − 收盘) / (vwap + 收盘),vwap相对收盘溢价率。值越高→预期上涨(正向因子),高值表示vwap高于收盘价,机构平均成本在收盘之上,支撑反弹",
"alpha043": "成交量比时序排名 × 7日价格逆变化时序排名,量能与价格反转共振。值越高→预期上涨(反转因子),高值表示放量且价格近期回调,量增价跌反弹信号",
"alpha044": "高价与成交量排名负相关取反,高位放量反转。值越高→预期下跌(反向因子),高值表示高价伴随大量,量价同步超买后回调",
"alpha045": "延迟均价排名 × 量价短期相关 × 均价多周期相关排名取反。值越高→预期下跌(反向因子),高值表示均价多周期均强且量价同向,综合超买",
"alpha046": "远近斜率差>0.25返回−1看空,<0返回1看多,否则取价格变化。值越高→预期上涨(条件正向因子),高值(+1)表示短期斜率转正,趋势反转向上",
"alpha047": "价格倒数×超额量×高价位置/均高 − vwap变化排名,量价位置偏离。值越高→预期上涨(正向因子),高值表示低价格高量能,vwap上升,价值洼地放量",
"alpha048": "3日价格方向排名×量比(行业中性化),行业内量价方向信号。值越高→预期上涨(正向因子),高值表示在同行业中近期涨势配合放量,相对强势",
"alpha049": "价格斜率差<−1时返回1看多,否则取价格变化反转。值越高→预期上涨(条件正向因子),高值(+1)表示短期斜率骤降,极度超卖后强力反弹",
"alpha050": "量与vwap相关排名的5日最大值取反,量价相关极值反转。值越高→预期下跌(反向因子),高值取反后实为量vwap相关性弱,量价不同向时反而易跌",
"alpha051": "远近价格斜率差<−0.05返回1看多,否则返回−1看空,二值趋势判断。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示短期急跌后斜率逆转,超卖反弹",
"alpha052": "5日低价变化量 × 长期收益加速排名 × 成交量时序排名,低价突破动量。值越高→预期上涨(正向因子),高值表示低价突破且长期动量加速放量,强势突破",
"alpha053": "价格在高低间位置比的9日变化取反,位置动量反转。值越高→预期下跌(反向因子),高值表示价格在高低区间位置近期快速上升,超买后回调",
"alpha054": "低收差×开盘5次方 / 低高差×收盘5次方 取反,高次方放大价格位置信号。值越高→预期下跌(反向因子),高值表示收盘接近最高且开盘价高,高位阳线超买",
"alpha055": "12日随机指标与成交量排名的负相关,超买区放量反转。值越高→预期下跌(反向因子),高值表示超买区成交量大,高位接盘多后回调概率高",
"alpha056": "短长期收益差排名+收盘离低排名 × 行业中性化收盘,行业内价格位置。值越高→预期上涨(正向因子),高值表示行业内近期超跌且收盘接近低位,相对低估",
"alpha057": "5日随机指标排名取反,价格在近期区间相对位置。值越高→预期上涨(反转因子),高值表示收盘接近5日低点,超卖区间内反弹概率高",
"alpha058": "行业中性化vwap量相关衰减时序排名取反,行业内量价趋势。值越高→预期下跌(反向因子),高值取反表示行业内量价相关趋势弱化,热度消退",
"alpha059": "alpha058加权版,16日衰减更长周期,行业内量价中期趋势。值越高→预期下跌(反向因子),高值取反表示中期量价同向信号衰减,中线热度减退",
"alpha060": "高价量9日相关×14日时序排名 × 低价量7日相关×8日时序排名取反。值越高→预期下跌(反向因子),高值表示高低价均与量相关性强,全面超买",
"alpha061": "vwap量4日相关排名 × 低价量5日相关排名取反,量价双重相关反转。值越高→预期下跌(反向因子),高值表示均价和低价均与量同向,双重超买信号",
"alpha062": "vwap均量相关 vs 低价大均量相关的条件判断,流动性对比信号。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价流动性优于vwap流动性,低位承接强",
"alpha063": "行业中性化vwap量相关 vs 低价大均量相关,行业内流动性判断。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示行业内低位流动性好,相对强势",
"alpha064": "复合价格均量相关 vs 低价大均量相关,价量结构对比。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价区流动性结构更优,低位筹码稳定",
"alpha065": "开盘量11日相关 vs 低价均量15日相关,开盘流动性信号。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示开盘位置流动性强于低价区,开盘承接好",
"alpha066": "开盘量5日相关 vs vwap量排名6日相关,vwap流动性结构判断。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示开盘量能优于vwap区间,短期开盘动力强",
"alpha067": "行业中性化高价量8日相关 vs 低价均量18日相关,行业高低量能对比。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价区量能相对行业内更强,低位有支撑",
"alpha068": "高价量ts_rank×4日相关 vs 低价均量14日相关,趋势量价结构对比。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价成交更活跃,底部筑牢",
"alpha069": "vwap量排名12日相关 vs 低价大均量19日相关,量价趋势与流动性。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价区长期量能充沛,底部支撑强",
"alpha070": "vwap日变化排名的幂次(指数=量价相关时序排名)取反,非线性量价动量。值越高→预期下跌(反向因子),高值表示vwap非线性上升且量价相关强,过热后回调",
"alpha071": "收盘趋势衰减相关 vs 中间价开收差衰减的逐元素最大值。值越高→预期上涨(正向因子),高值表示收盘趋势与日内实体均强,多头持续",
"alpha072": "高价量相关衰减 − 低价量相关衰减,高低价量能不对称。值越高→预期上涨(正向因子),高值表示高价区量能强于低价区,上涨时放量下跌时缩量,多头健康",
"alpha073": "vwap 3日变化衰减排名 + 开盘离低比衰减排名,价格趋势与开盘位置。值越高→预期上涨(正向因子),高值表示vwap上升且开盘接近低价,空间大动能足",
"alpha074": "低价量相关排名 + 延迟开收排名 − vwap均量衰减相关排名取反。值越高→预期下跌(反向因子),高值表示低价量能和前期阳线共同过热,综合超买",
"alpha075": "vwap量相关 + 低价大均量相关 − 中间价衰减排名,流动性综合因子。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示量价流动性强,多方主导",
"alpha076": "vwap变化衰减+价格结构衰减 × 行业中性化收盘,行业内价格趋势。值越高→预期上涨(正向因子),高值表示行业内vwap和价格结构均向上,相对强势",
"alpha077": "中间价高价差衰减排名取反 + 低价大均量相关衰减排名,价格位置偏离。值越高→预期上涨(正向因子),高值表示日内价格偏向下沿且低价量能强,逢低做多",
"alpha078": "vwap均量相关衰减取反 + 低价衰减取反 + 开盘衰减,多因子价格偏离。值越高→预期上涨(正向因子),高值表示vwap量价背离且价格低位,反转看涨",
"alpha079": "行业中性化收盘变化×vwap均量相关排名 − 低价衰减排名,行业内趋势。值越高→预期上涨(正向因子),高值表示行业内价格上涨量价配合,低价区相对强势",
"alpha080": "行业中性化开收变化方向 + 成交量时序排名取反,行业内价格动量反转。值越高→预期下跌(反向因子),高值表示行业内阳线多且成交量偏大,超买后均值回归",
"alpha081": "高价均量8日相关乘积log排名−0.5取反,量价相关乘积信号。值越高→预期下跌(反向因子),高值取反表示量价相关乘积低,放量但高价配合差,看空",
"alpha082": "高量相关log排名 + 量时序排名 − 开量相关排名取反,多因子量价。值越高→预期下跌(反向因子),高值取反表示量排名虽高但开盘量价相关强,高开后易跌",
"alpha083": "延迟高价×价格方向 + vwap收盘差 − 开量13日相关排名,综合量价。值越高→预期上涨(正向因子),高值表示前期高价延续且vwap高于收盘,价值支撑",
"alpha084": "vwap均量6日相关 + 开盘量15日相关,双重流动性正向信号。值越高→预期上涨(正向因子),高值表示vwap和开盘均与量同向,量价双重共振看涨",
"alpha085": "收盘均量相关 + 高价量排名相关 − 价格6日变化,量价与动量综合。值越高→预期上涨(正向因子),高值表示量价相关强但近期价格涨幅有限,上涨空间大",
"alpha086": "收盘均量相关 + 开盘量相关 − 价格6日变化,类alpha085版本。值越高→预期上涨(正向因子),高值表示开盘收盘量能充沛但涨价不多,后续补涨潜力",
"alpha087": "max(价格3日变化, 量47%时序) − min(均量变化, 行业vwap量相关),极值差。值越高→预期上涨(正向因子),高值表示价格或量的上行极值强,下行极值弱,多头偏强",
"alpha088": "min(开盘2日变化衰减, 收盘量相关) + 开低差衰减,价格开盘综合。值越高→预期上涨(正向因子),高值表示开盘动能量价配合且开盘接近低价,稳健看涨",
"alpha089": "行业中性化vwap量相关双重衰减时序 − 低价均量相关衰减,量价行业中性。值越高→预期上涨(正向因子),高值表示行业内vwap量价趋势强于低价量能,中高位放量",
"alpha090": "vwap量相关衰减 − 低价ts_min衰减 + 行业中性化收盘量相关。值越高→预期上涨(正向因子),高值表示vwap量价配合且非近期最低,行业内收盘也强",
"alpha091": "行业中性化收盘量相关三重衰减时序 − vwap均量相关衰减取反。值越高→预期上涨(正向因子),高值表示行业内收盘量价持续配合,长期多头结构",
"alpha092": "min(条件衰减, 高价量相关衰减),价格位置与量能最小值信号。值越高→预期上涨(正向因子),高值表示价格位置和高价量能双双强,保守估计看涨",
"alpha093": "行业中性化vwap均量相关衰减时序×5/6 + 量价相关衰减排名×1/6。值越高→预期上涨(正向因子),高值表示行业内量价综合趋势强,主力资金配合",
"alpha094": "vwap低价相关衰减 − 低价均量相关衰减 + vwap变化衰减,三因子vwap偏离。值越高→预期上涨(正向因子),高值表示vwap趋势向上且高于低价水平,多头结构",
"alpha095": "开盘量相关衰减 − 低价均量相关衰减 + vwap变化衰减,类alpha094版本。值越高→预期上涨(正向因子),高值表示开盘量能及vwap趋势强,开盘多头",
"alpha096": "vwap量相关衰减 − 低价均量相关衰减 + vwap 3日变化,vwap量价综合。值越高→预期上涨(正向因子),高值表示vwap量价结构优于低价,且vwap近期上升",
"alpha097": "行业中性化收盘变化衰减 − 行业中性化vwap量相关衰减取反。值越高→预期下跌(反向因子),高值取反表示行业内收盘涨幅强但量价相关弱,价升量缩警惕",
"alpha098": "vwap均量相关衰减 − 开盘均量相关argmin时序衰减,vwap趋势与开盘底部。值越高→预期上涨(正向因子),高值表示vwap量价强且开盘远离近期低量时刻,多头延续",
"alpha099": "高价量相关衰减 − 低价量相关衰减 + 低价量9日短期相关,高低量能不对称。值越高→预期上涨(正向因子),高值表示上涨时放量下跌时缩量,高低量能分化利多",
"alpha100": "vwap变化衰减 + 行业中性化vwap量相关衰减 − 收盘变化衰减,量价趋势综合。值越高→预期上涨(正向因子),高值表示vwap趋势和量价强而收盘涨幅有限,补涨空间",
"alpha101": "当日K线实体长度与日内振幅之比,即(收盘−开盘)/(最高−最低+0.001)。值越高→预期上涨(正向因子),高正值表示收盘远高于开盘、阳线强势,惯性动量延续看涨",
}
FILE:scripts/formulaicAlphas/data_loader.py
"""
data_loader.py — Alpha 101 面板数据加载器
将数据库中的个股日线数据转换为 Alpha 计算所需的面板格式:
- 行(index) = 交易日期(pd.Timestamp)
- 列(columns) = 股票代码
返回字段:open / high / low / close / volume / amount / vwap / returns / ind
vwap 计算:amount / (volume * 100),volume 单位为手(100股/手),amount 单位为元。
若某日某股 vwap 计算结果异常(<=0 或 NaN),回退为 (open+high+low+close)/4。
ind(行业):从 stock_basic.industry 获取,广播到面板同形。
"""
from __future__ import annotations
import os
import sys
import numpy as np
import pandas as pd
# 允许直接运行或被上级包导入
_scripts_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _scripts_dir not in sys.path:
sys.path.insert(0, _scripts_dir)
from data_fetcher import query_daily_kline, query_stock_basic
class AlphaDataLoader:
"""加载 Alpha 因子计算所需的跨截面面板数据。"""
def load(
self,
codes: list[str],
start_date: str,
end_date: str,
fill_method: str = "ffill",
) -> dict[str, pd.DataFrame]:
"""
加载指定股票池和日期范围的面板数据。
Args:
codes: 股票代码列表,如 ['000001.SZ', '600519.SH']
start_date: 起始日期,格式 'YYYY-MM-DD'
end_date: 截止日期,格式 'YYYY-MM-DD'
fill_method: NaN 填充方式('ffill' / 'bfill' / None)
Returns:
字典,key 为字段名,value 为 DataFrame(行=日期,列=股票代码):
open, high, low, close, volume, amount, vwap, returns, ind
"""
klines = query_daily_kline(
codes=codes,
start_date=start_date,
end_date=end_date,
order_by="date ASC",
)
if not klines:
return {}
records = [
{
"date": k.date,
"code": k.code,
"open": k.open,
"high": k.high,
"low": k.low,
"close": k.close,
"volume": k.volume,
"amount": k.amount,
}
for k in klines
]
df = pd.DataFrame(records)
df["date"] = pd.to_datetime(df["date"])
df = df.set_index(["date", "code"])
panels: dict[str, pd.DataFrame] = {}
for field in ("open", "high", "low", "close", "volume", "amount"):
panels[field] = df[field].unstack(level="code").astype(float)
# 填充缺失值
if fill_method:
for key in list(panels.keys()):
panels[key] = getattr(panels[key], fill_method)()
# ── vwap ──────────────────────────────────────────────────────────
vol_safe = panels["volume"].replace(0, np.nan)
vwap = panels["amount"] / (vol_safe * 100) # volume 单位:手
typical = (panels["open"] + panels["high"] + panels["low"] + panels["close"]) / 4
bad = (vwap <= 0) | vwap.isna()
panels["vwap"] = vwap.where(~bad, typical)
# ── returns ───────────────────────────────────────────────────────
panels["returns"] = panels["close"].pct_change()
# ── industry ──────────────────────────────────────────────────────
basics = query_stock_basic()
ind_map = {b.ts_code: (b.industry or "Unknown") for b in basics}
ind_panel = pd.DataFrame(
{
code: ind_map.get(code, "Unknown")
for code in panels["close"].columns
},
index=panels["close"].index,
)
panels["ind"] = ind_panel
return panels
FILE:scripts/formulaicAlphas/operators.py
"""
operators.py — Alpha 101 基础运算符
所有运算符均以 pandas DataFrame 为工作单元:
- 行(index) = 交易日期
- 列(columns) = 股票代码
横截面运算(rank / scale / ind_neutralize)在"股票"轴(axis=1)上操作;
时间序列运算(ts_* / delay / delta / correlation 等)在"时间"轴(axis=0)上操作。
运算符命名与论文保持一致;Python 内建函数冲突时加下划线后缀(如 abs_)。
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from typing import Union
# 类型别名:行=日期,列=股票代码
Panel = pd.DataFrame
# ─────────────────────────────────────────────────────────────────────────────
# 横截面运算
# ─────────────────────────────────────────────────────────────────────────────
def rank(x: Panel) -> Panel:
"""横截面百分位排名:每个交易日内,股票间的相对排名(0~1)。"""
return x.rank(axis=1, pct=True, na_option='keep')
def scale(x: Panel, a: float = 1.0) -> Panel:
"""横截面标准化:使每日各股绝对值之和等于 a。"""
abs_sum = x.abs().sum(axis=1).replace(0, np.nan)
return x.div(abs_sum, axis=0).mul(a)
def ind_neutralize(x: Panel, ind: Panel) -> Panel:
"""行业中性化:减去同日同行业均值。ind 与 x 同形,值为行业代码字符串。"""
result = x.copy().astype(float)
for date in x.index:
row = x.loc[date]
ind_row = ind.loc[date] if date in ind.index else pd.Series(dtype=str)
if ind_row.empty:
continue
means = row.groupby(ind_row).transform('mean')
result.loc[date] = row.values - means.values
return result
# ─────────────────────────────────────────────────────────────────────────────
# 时间序列运算
# ─────────────────────────────────────────────────────────────────────────────
def delay(x: Panel, d: int) -> Panel:
"""d 期前的值。"""
return x.shift(int(d))
def delta(x: Panel, d: int) -> Panel:
"""x 与 d 期前的差:x - delay(x, d)。"""
return x.diff(int(d))
def ts_sum(x: Panel, d: int) -> Panel:
"""过去 d 期滚动求和(含当期)。"""
return x.rolling(int(d), min_periods=int(d)).sum()
def mean(x: Panel, d: int) -> Panel:
"""过去 d 期滚动均值。"""
return x.rolling(int(d), min_periods=int(d)).mean()
def stddev(x: Panel, d: int) -> Panel:
"""过去 d 期滚动标准差(样本标准差,ddof=1)。"""
return x.rolling(int(d), min_periods=int(d)).std(ddof=1)
def ts_max(x: Panel, d: int) -> Panel:
"""过去 d 期滚动最大值。"""
return x.rolling(int(d), min_periods=int(d)).max()
def ts_min(x: Panel, d: int) -> Panel:
"""过去 d 期滚动最小值。"""
return x.rolling(int(d), min_periods=int(d)).min()
def ts_rank(x: Panel, d: int) -> Panel:
"""时间序列百分位排名:当期值在过去 d 期中的排名(0~1)。"""
d = int(d)
def _rank_last(arr: np.ndarray) -> float:
if np.all(np.isnan(arr)):
return np.nan
s = pd.Series(arr)
return float(s.rank(pct=True).iloc[-1])
return x.rolling(d, min_periods=d).apply(_rank_last, raw=False)
def ts_argmax(x: Panel, d: int) -> Panel:
"""过去 d 期内最大值所在位置(1 = 最老,d = 最新)。"""
d = int(d)
return x.rolling(d, min_periods=d).apply(
lambda a: int(np.argmax(a)) + 1, raw=True
)
def ts_argmin(x: Panel, d: int) -> Panel:
"""过去 d 期内最小值所在位置(1 = 最老,d = 最新)。"""
d = int(d)
return x.rolling(d, min_periods=d).apply(
lambda a: int(np.argmin(a)) + 1, raw=True
)
def correlation(x: Panel, y: Panel, d: int) -> Panel:
"""各股票列的滚动 d 期皮尔逊相关系数(x 与 y 对应列)。
当某列方差为 0(常量序列)时,相关系数未定义,填充为 0(中性值)。"""
d = int(d)
# pandas rolling.corr on aligned DataFrames returns element-wise corr
result = x.rolling(d, min_periods=d).corr(y)
# 裁剪异常值,并将未定义的 NaN(零方差)填充为 0
return result.clip(-1, 1).fillna(0)
def covariance(x: Panel, y: Panel, d: int) -> Panel:
"""各股票列的滚动 d 期协方差。"""
d = int(d)
return x.rolling(d, min_periods=d).cov(y)
def decay_linear(x: Panel, d: int) -> Panel:
"""线性衰减加权移动平均:权重为 1,2,...,d(归一化后较新权重更大)。"""
d = int(max(1, round(d)))
weights = np.arange(1, d + 1, dtype=float)
weights /= weights.sum()
def _wavg(arr: np.ndarray) -> float:
mask = ~np.isnan(arr)
if not mask.any():
return np.nan
w = weights[mask]
return float(np.dot(arr[mask], w / w.sum()))
return x.rolling(d, min_periods=d).apply(_wavg, raw=True)
def product(x: Panel, d: int) -> Panel:
"""过去 d 期滚动乘积。"""
return x.rolling(int(d), min_periods=int(d)).apply(np.prod, raw=True)
# ─────────────────────────────────────────────────────────────────────────────
# 逐元素运算
# ─────────────────────────────────────────────────────────────────────────────
def signedpower(x: Panel, t: float) -> Panel:
"""保号指数:sign(x) * |x|^t。"""
return np.sign(x) * (np.abs(x) ** t)
def log(x: Panel) -> Panel:
"""自然对数(对非正值做保护性裁剪)。"""
return np.log(x.clip(lower=1e-10))
def sign(x: Panel) -> Panel:
return np.sign(x)
def abs_(x: Panel) -> Panel:
return x.abs()
def adv(volume: Panel, d: int) -> Panel:
"""过去 d 日平均成交量(Average Daily Volume)。"""
return volume.rolling(int(d), min_periods=int(d)).mean()
# ─────────────────────────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────────────────────────
def where(cond: Panel, a: Union[Panel, float], b: Union[Panel, float]) -> Panel:
"""三元条件:cond 为 True 时取 a,否则取 b(等价于论文中的 ? :)。"""
if isinstance(cond, pd.DataFrame):
result = pd.DataFrame(np.where(cond, a, b),
index=cond.index, columns=cond.columns)
else:
result = pd.DataFrame(np.where(cond, a, b))
return result
FILE:scripts/formulaicAlphas/__init__.py
"""
formulaicAlphas — WorldQuant《101 Formulaic Alphas》实现包
快速入门:
from formulaicAlphas import AlphaDataLoader, Alpha101
# 1. 加载面板数据
loader = AlphaDataLoader()
data = loader.load(
codes=['000001.SZ', '600519.SH', '000858.SZ'],
start_date='2025-01-01',
end_date='2026-03-14',
)
# 2. 计算 alpha 因子
a = Alpha101(data)
alpha1 = a.alpha001() # DataFrame:行=日期,列=股票代码
alpha50 = a.alpha050()
# 3. 取最新一日横截面排名(越大越看多)
latest = alpha1.iloc[-1].dropna().sort_values(ascending=False)
print(latest.head(10))
# 4. 批量计算指定 alpha
results = a.compute_all(alphas=[1, 5, 12, 41, 101])
for name, df in results.items():
print(name, df.iloc[-1].describe())
"""
from .data_loader import AlphaDataLoader
from .alpha101 import Alpha101, ALPHA_DESCRIPTIONS
from . import operators
__all__ = ["AlphaDataLoader", "Alpha101", "ALPHA_DESCRIPTIONS", "operators"]
FILE:references/API_FOR_LLM.md
# StockApi 接口文档
`StockApi` 是项目对外提供的唯一数据与回测接口,封装了股票基础信息查询、K线数据获取、技术指标计算、性能指标计算和回测工具函数。
```python
from stock_api import StockApi
api = StockApi()
```
---
## 目录
1. [初始化](#初始化)
2. [股票基础信息](#股票基础信息)
3. [价格行情](#价格行情)
4. [技术指标(带缓存)](#技术指标带缓存)
5. [性能指标](#性能指标)
6. [回测工具](#回测工具)
7. [回测引擎控制](#回测引擎控制)
8. [策略辅助函数](#策略辅助函数)
9. [数据库维护](#数据库维护)
10. [实时行情 (爬虫)](#实时行情-爬虫)
---
## 初始化
### 初始化 StockApi,自动初始化技术指标缓存数据库
```python
api = StockApi()
# __init__(self)
```
---
### 更新本地数据库,获取最新增量数据
```python
api.update_data()
# update_data() -> None
```
调用此接口会对比服务器上的 patch 列表,下载并导入缺失的数据。
---
## 股票基础信息
### 获取所有股票代码列表
```python
symbols = api.get_all_symbols()
# get_all_symbols() -> List[str]
```
| 返回 | 说明 |
|------|------|
| `List[str]` | 股票代码列表,格式如 `['000001.SZ', '600519.SH', ...]` |
---
### 根据股票代码获取股票基础信息
```python
info = api.get_symbol_basic_infomation('600519.SH')
# get_symbol_basic_infomation(ts_code: str) -> Optional[StockBasic]
```
| 参数 | 类型 | 说明 |
|------|------|------|
| `ts_code` | `str` | 股票代码,如 `000001.SZ` |
| 返回 | 说明 |
|------|------|
| `StockBasic` \| `None` | 股票基础信息,未查询到返回 `None` |
---
## 价格行情
### 查询每日基本面指标列表,支持按股票、日期、分页过滤
```python
# 查询某只股票全部历史基本面数据
basics = api.get_daily_basic(ts_codes=["000001.SZ"])
# 查询某天全市场基本面数据
basics = api.get_daily_basic(trade_date="2024-06-03")
# get_daily_basic(ts_codes=[], trade_date=None, start_date=None,
# end_date=None, limit=None, offset=0,
# order_by="trade_date ASC") -> List[DailyBasic]
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `ts_codes` | `List[str]` | `[]` | 按股票代码列表过滤,空表示不过滤 |
| `trade_date` | `str \| None` | `None` | 精确过滤交易日期,格式 `YYYY-MM-DD` |
| `start_date` | `str \| None` | `None` | 日期范围下限(含),格式 `YYYY-MM-DD` |
| `end_date` | `str \| None` | `None` | 日期范围上限(含),格式 `YYYY-MM-DD` |
| `limit` | `int \| None` | `None` | 返回最大记录数,`None` 表示不限 |
| `offset` | `int` | `0` | 分页偏移量 |
| `order_by` | `str` | `"trade_date ASC"` | 排序表达式 |
---
### 获取股票日线行情,按日期升序
```python
klines = api.get_daily_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# get_daily_kline(symbols: List[str], start_date: str, end_date: str) -> List[DailyKline]
```
| 参数 | 类型 | 说明 |
|------|------|------|
| `symbols` | `List[str]` | 股票代码列表,空表示获取所有股票 |
| `start_date` | `str` | 起始日期,格式 `YYYY-MM-DD` |
| `end_date` | `str` | 结束日期,格式 `YYYY-MM-DD` |
---
### 获取股票周线行情,按日期升序
```python
klines = api.get_weekly_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# get_weekly_kline(symbols: List[str], start_date: str, end_date: str) -> List[WeeklyKline]
```
---
### 获取股票月线行情,按日期升序
```python
klines = api.get_monthly_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# get_monthly_kline(symbols: List[str], start_date: str, end_date: str) -> List[MonthlyKline]
```
---
### 获取指定股票的日线收盘价列表,按日期升序
```python
prices = api.get_daily_close_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_close_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线开盘价列表
```python
prices = api.get_daily_open_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_open_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线最高价列表
```python
prices = api.get_daily_high_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_high_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线最低价列表
```python
prices = api.get_daily_low_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_low_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线成交量列表
```python
volumes = api.get_daily_volumes('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_volumes(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线涨跌幅列表(单位:%)
```python
pct = api.get_daily_pct_chg('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_pct_chg(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定日期的 Tick 级数据(模拟级),包含开高低收量额
```python
tick = api.get_tick_data('600519.SH', '2026-03-01')
# get_tick_data(code: str, date: str) -> Optional[Dict]
```
| 返回字段 | 说明 |
|----------|------|
| `time` | 时间 |
| `open` | 开盘价 |
| `high` | 最高价 |
| `low` | 最低价 |
| `close` | 收盘价 |
| `volume` | 成交量 |
| `amount` | 成交额 |
---
### 获取实时 Bar 数据,与 get_tick_data 等价,用于实盘级接口
```python
bar = api.get_realtime_bar('600519.SH', '2026-03-01')
# get_realtime_bar(code: str, date: str) -> Dict
```
---
## 技术指标(带缓存)
### 获取简单移动平均 SMA
```python
sma = api.get_sma('600519.SH', '2026-03-01', 20)
# get_sma(code: str, date: str, period: int = 20) -> Optional[float]
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `period` | `20` | 计算周期 |
---
### 获取指数移动平均 EMA
```python
ema = api.get_ema('600519.SH', '2026-03-01', 12)
# get_ema(code: str, date: str, period: int = 12) -> Optional[float]
```
---
### 获取相对强弱指标 RSI,值域 0~100,低于 30 超卖,高于 70 超买
```python
rsi = api.get_rsi('600519.SH', '2026-03-01', 14)
if rsi and rsi < 30:
print('超卖')
# get_rsi(code: str, date: str, period: int = 14) -> Optional[float]
```
---
### 获取布林带指标,返回上轨、中轨、下轨
```python
bb = api.get_bollinger_bands('600519.SH', '2026-03-01')
if bb and close > bb['upper']:
print('突破上轨')
# get_bollinger_bands(code: str, date: str, period: int = 20, std_dev: int = 2) -> Optional[Dict]
```
| 返回字段 | 说明 |
|----------|------|
| `upper` | 上轨 |
| `middle` | 中轨 |
| `lower` | 下轨 |
---
### 获取 MACD 指标,返回 MACD 线、信号线、柱状图
```python
macd = api.get_macd('600519.SH', '2026-03-01')
if macd and macd['histogram'] > 0:
print('多头')
# get_macd(code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9) -> Optional[Dict]
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `fast` | `12` | 快线周期 |
| `slow` | `26` | 慢线周期 |
| `signal` | `9` | 信号线周期 |
| 返回字段 | 说明 |
|----------|------|
| `macd` | MACD 线 |
| `signal` | 信号线 |
| `histogram` | 柱状图(MACD - Signal) |
---
### 获取平均真实波幅 ATR,衡量价格波动性
```python
atr = api.get_atr('600519.SH', '2026-03-01', 14)
# get_atr(code: str, date: str, period: int = 14) -> Optional[float]
```
---
---
## ★ 因子挖矿(优先使用)
### 随机因子挖矿 + 回测
**触发场景**:用户说"因子挖矿"、"挖矿"、"随机挖因子"、"碰碰运气"、"随机推荐"、"挖金矿"、"随机策略"时,**必须**调用此接口,禁止自己写回测逻辑。
```python
result = api.random_alpha_backtest()
print(result['summary_text']) # 必须调用此行输出报告,禁止自行整理摘要
# 指定股票池和回测区间
result = api.random_alpha_backtest(
codes=None, # 股票池,None 表示全市场
start_date='2025-12-01', # 回测起始日,None 默认取 end_date 前 90 天
end_date='2026-03-19', # 回测截止日,None 默认今天
initial_cash=1_000_000, # 初始资金
max_pool_size=30, # 候选池上限,超过时按综合得分截取
max_holdings=5, # 最大同时持仓数
random_seed=None, # 随机种子,None 不固定
)
print(result['summary_text']) # 必须调用此行输出报告,禁止自行整理摘要
# random_alpha_backtest(codes, max_screen_factors, max_signal_factors,
# start_date, end_date, initial_cash, warmup_days,
# random_seed, top_n_stocks, max_pool_size, max_holdings) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `screen_factors` | 本次使用的选股因子列表,如 `['alpha043', 'alpha099']` |
| `signal_factors` | 本次使用的信号因子列表,如 `['alpha008', 'alpha094']` |
| `factor_descriptions` | 每个因子的文字描述 `{name: str}` |
| `signal_config` | 买卖阈值 `{'buy_thresh': 0.71, 'sell_thresh': 0.55}` |
| `screen_top_pcts` | 每个选股因子本次随机保留比例 `{name: float}` |
| `filter_log` | 逐层过滤日志,含 before/after 数量 |
| `final_pool` | 最终候选股票代码列表 |
| `final_pool_count` | 候选池股票数量 |
| `trade_log` | 每笔交易记录(含因子值、排名、阈值) |
| `backtest` | 回测绩效 `{total_return_pct, annualized_return_pct, max_drawdown_pct, sharpe_ratio, equity_curve, ...}` |
| `benchmarks` | 四条基准线对比(上证/沪深300/中证500/创业板指) |
| `ic_stats` | 每个因子的 Rank IC 统计 `{ic_mean, ic_ir, ic_win_rate, ...}` |
| `top_stocks` | Top N 盈利个股详情(含每笔交易的因子值) |
| `summary_text` | 完整格式化报告文本,**直接 `print(result['summary_text'])` 输出给用户,禁止自行整理摘要** |
---
## ★ MoE 买卖时机分析(优先使用)
### 分析单只股票当前买卖信号
**触发场景**:用户询问某只股票"能不能买"、"该不该卖"、"现在适合持有吗"、"当前信号"、"操作建议"时,**必须**调用此接口。
```python
result = api.get_trade_signal('000001.SZ')
result = api.get_trade_signal('600519.SH', date='2026-01-15')
# get_trade_signal(code: str, date: str = None) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `signal` | `"BUY"` 买入 / `"SELL"` 卖出 / `"HOLD"` 持有 |
| `final_score` | 综合评分 0~1,越高越看多 |
| `confidence` | 置信度:`"高"` / `"中"` / `"低"` |
| `reason` | 各专家评分描述,如 `"技术面看多(0.71),Alpha因子看多(0.73)"` |
| `experts` | 四个专家详情:`technical` / `alpha` / `fundamental` / `behavior` |
| `code` | 股票代码 |
| `date` | 分析日期 |
---
## 性能指标
### 计算最大回撤,返回回撤比例及对应的峰值、谷值索引
```python
dd, peak_idx, drawdown_idx = api.get_max_drawdown([1000000, 1100000, 950000])
print(f'最大回撤: {dd:.2%}')
# get_max_drawdown(equity_curve: List[float]) -> tuple
```
---
### 获取最大回撤百分比,如 0.15 表示 15%
```python
pct = api.get_max_drawdown_pct([1000000, 1100000, 950000])
# get_max_drawdown_pct(equity_curve: List[float]) -> float
```
---
### 计算年化收益率
```python
annualized = api.get_annualized_return(0.15, 60)
# get_annualized_return(total_return: float, days: int) -> float
```
| 参数 | 说明 |
|------|------|
| `total_return` | 总收益率,如 `0.15` 表示 15% |
| `days` | 交易天数 |
---
### 计算总收益率
```python
ret = api.get_total_return(1000000, 1150000)
# get_total_return(initial_value: float, final_value: float) -> float
```
---
### 计算夏普比率,衡量单位风险的超额收益
```python
sharpe = api.get_sharpe_ratio([1000000, 1050000, 1020000])
# get_sharpe_ratio(equity_curve: List[float], risk_free_rate: float = 0.03) -> float
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `risk_free_rate` | `0.03` | 无风险利率(年化) |
---
### 计算胜率(0~100),盈利交易次数占比
```python
trades = [{'profit': 1000}, {'profit': -500}, {'profit': 800}]
win_rate = api.get_win_rate(trades)
# get_win_rate(trades: List[Dict]) -> float
```
---
### 计算盈亏比,平均盈利 / 平均亏损
```python
ratio = api.get_profit_loss_ratio(trades)
# get_profit_loss_ratio(trades: List[Dict]) -> float
```
---
### 计算卡尔玛比率,年化收益 / 最大回撤
```python
calmar = api.get_calmar_ratio(equity_curve, 252)
# get_calmar_ratio(equity_curve: List[float], days: int) -> float
```
---
### 计算年化波动率,衡量收益稳定性
```python
vol = api.get_volatility(equity_curve)
# get_volatility(equity_curve: List[float]) -> float
```
---
### 获取完整交易统计信息
```python
stats = api.get_trade_stats(trades)
# get_trade_stats(trades: List[Dict]) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `total_trades` | 总交易次数 |
| `wins` | 盈利次数 |
| `losses` | 亏损次数 |
| `win_rate` | 胜率 |
| `profit_loss_ratio` | 盈亏比 |
| `total_profit` | 总盈利 |
| `total_loss` | 总亏损 |
| `avg_profit` | 平均盈利 |
| `avg_loss` | 平均亏损 |
---
### 生成完整回测报告,汇总所有关键绩效指标
```python
equity = [1000000, 1050000, 1020000]
trades = [{'profit': 5000}, {'profit': -3000}]
report = api.calculate_metrics(equity, trades, 1000000, 30)
print(f"收益率: {report['total_return_pct']:.2f}%")
print(f"夏普比率: {report['sharpe_ratio']:.2f}")
# calculate_metrics(equity_curve: List[float], trades: List[Dict],
# initial_cash: float, days: int) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `initial_cash` | 初始资金 |
| `final_value` | 最终资金 |
| `total_return` | 总收益率 |
| `total_return_pct` | 总收益率(%) |
| `annualized_return` | 年化收益率 |
| `annualized_return_pct` | 年化收益率(%) |
| `max_drawdown` | 最大回撤 |
| `max_drawdown_pct` | 最大回撤(%) |
| `sharpe_ratio` | 夏普比率 |
| `calmar_ratio` | 卡尔玛比率 |
| `volatility` | 波动率 |
| `trading_days` | 交易天数 |
| `trade_stats` | 交易统计(同 `get_trade_stats`) |
---
## 回测工具
### 模拟单笔交易,计算成本、手续费和净收款
```python
result = api.simulate_trade('BUY', 100.0, 100)
print(f"成本: {result['cost']}, 手续费: {result['fee']}")
# simulate_trade(action: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `cost` | 成本 |
| `fee` | 手续费 |
| `net_proceeds` | 净收款(卖出时) |
---
### 计算交易成本,含手续费和滑点
```python
cost = api.calculate_trade_cost('BUY', 100.0, 100, 0.0003, 0.001)
# calculate_trade_cost(action, price, quantity, fee_rate=0.0003, slippage=0.0) -> float
```
---
### 创建持仓对象,记录股票、股数、买入价和日期
```python
pos = api.create_position('600519.SH', 100, 1800.0, '2026-01-01')
# create_position(code: str, shares: int, price: float, date: str) -> Position
```
---
### 计算持仓市值
```python
value = api.get_position_value(pos, 1900.0)
# get_position_value(position: Position, current_price: float) -> float
```
---
### 计算持仓盈亏,返回盈亏金额和比例
```python
profit, pct = api.get_position_profit(position, 2000.0)
print(f"盈利: {profit}, 比例: {pct:.2%}")
# get_position_profit(position: Position, current_price: float) -> tuple
```
---
### 计算组合总价值(现金 + 所有持仓市值)
```python
value = api.calculate_portfolio_value(500000, positions, current_prices)
# calculate_portfolio_value(cash: float, positions: Dict[str, Position],
# prices: Dict[str, float]) -> float
```
---
### 获取组合持仓详情列表
```python
details = api.get_portfolio_positions(positions)
# get_portfolio_positions(positions: Dict[str, Position]) -> List[Dict]
```
---
### 从每日资产列表构建权益曲线
```python
values = [('2026-01-01', 1000000), ('2026-01-02', 1005000)]
curve = api.build_equity_curve(values)
# build_equity_curve(daily_values: List[tuple]) -> List[float]
```
---
### 计算日收益率序列
```python
returns = api.calculate_daily_returns(equity_curve)
# calculate_daily_returns(equity_curve: List[float]) -> List[float]
```
---
### 买入信号判断:MA 金叉且 RSI 超卖时返回 True
```python
if api.should_buy(close, ma5, ma20, rsi, 30):
print('买入信号')
# should_buy(current_price, ma_short, ma_long, rsi=50, rsi_oversold=30) -> bool
```
---
### 卖出信号判断:MA 死叉或 RSI 超买时返回 True
```python
if api.should_sell(close, ma5, ma20, rsi, 70):
print('卖出信号')
# should_sell(current_price, ma_short, ma_long, rsi=50, rsi_overbought=70) -> bool
```
---
### 计算权益曲线的逐日回撤序列
```python
drawdowns = api.calculate_drawdown([1000000, 1100000, 950000])
# calculate_drawdown(equity_curve: List[float]) -> List[float]
```
---
## 回测引擎控制
### 初始化回测环境,返回含现金、持仓、订单、交易记录的状态字典
```python
env = api.init_backtest(1000000, 0.0003)
# init_backtest(initial_cash: float = 1000000.0, fee_rate: float = 0.0003) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `initial_cash` | 初始资金 |
| `fee_rate` | 手续费率 |
| `cash` | 当前现金 |
| `positions` | 持仓字典 |
| `orders` | 订单列表 |
| `trades` | 交易记录 |
| `equity_curve` | 权益曲线 |
---
### 执行买入操作,自动更新 env 中的现金、持仓和交易记录
```python
result = api.execute_buy(env, '600519.SH', 1800.0, 100, '2026-01-01')
# execute_buy(env, code, price, quantity, date) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `success` | 是否成功 |
| `cost` | 成本 |
| `fee` | 手续费 |
| `reason` | 失败原因(失败时) |
---
### 执行卖出操作,自动更新 env
```python
result = api.execute_sell(env, '600519.SH', 1900.0, 100)
# execute_sell(env, code, price, quantity) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `success` | 是否成功 |
| `net_proceeds` | 净收款 |
| `fee` | 手续费 |
| `reason` | 失败原因(失败时) |
---
### 获取当前总权益(现金 + 持仓市值)
```python
equity = api.get_equity(env, current_prices)
# get_equity(env: Dict, current_prices: Dict[str, float]) -> float
```
---
### 将当日权益追加记录到 env['equity_curve']
```python
api.record_equity(env, '2026-03-01', current_prices)
# record_equity(env, date, current_prices) -> None
```
---
### 平仓,卖出结束多头持仓,返回盈亏和持有天数
```python
result = api.close_position(position, 1900.0, '2026-01-15')
print(f"盈利: {result['profit']}")
# close_position(position, price, date) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `profit` | 盈亏金额 |
| `profit_pct` | 盈亏比例 |
| `hold_days` | 持有天数 |
---
### 更新持仓的当前价格,用于实时市值计算
```python
api.update_position_price(position, 1900.0)
# update_position_price(position, current_price) -> None
```
---
### 创建本地模拟订单(非真实下单)
```python
order = api.create_order('600519.SH', 'BUY', 1800.0, 100)
# create_order(code, action, price, quantity) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `order_id` | 订单 ID |
| `status` | 状态(`PENDING`) |
| `create_time` | 创建时间 |
---
### 取消订单,仅 PENDING 状态可取消
```python
success = api.cancel_order(order)
# cancel_order(order: Dict) -> bool
```
---
### 获取订单状态:PENDING / FILLED / CANCELLED / REJECTED
```python
status = api.get_order_status(order)
# get_order_status(order: Dict) -> str
```
---
## 策略辅助函数
### 计算指定股票近 N 日平均涨幅(%)
```python
avg_change = api.get_price_change_rate('600519.SH', '2026-03-01', 3)
# get_price_change_rate(code, date, days=3) -> Optional[float]
```
---
### 从股票列表中筛选出近 N 日涨幅最高的前 N 只,按涨幅降序返回
```python
top_stocks = api.get_top_performers(codes, '2026-03-01', 3, 3)
# [(code, avg_pct), ...]
# get_top_performers(codes, date, days=3, top_n=3) -> List[tuple]
```
---
### 获取指定日期的收盘价,无数据返回 None
```python
price = api.get_price_at_date('600519.SH', '2026-03-01')
# get_price_at_date(code, date) -> Optional[float]
```
---
### 批量获取多个日期的收盘价,按日期升序对齐
```python
prices = api.get_prices_at_dates('600519.SH', ['2026-01-01', '2026-01-02'])
# get_prices_at_dates(code, dates) -> List[Optional[float]]
```
---
### 训练 MoE 权重(遗传算法优化)
**触发场景**:用户说"优化权重"、"重新训练"、"适配最新行情"时调用。
```python
weights = api.train_moe_weights()
# 指定区间和参数
weights = api.train_moe_weights(
start_date='2025-09-01',
end_date='2026-03-01',
population_size=20, # 种群大小,越大越精准但越慢
generations=30, # 迭代代数
train_stock_count=30, # 参与训练的随机采样股票数量
)
# train_moe_weights(start_date, end_date, population_size, generations, train_stock_count) -> Dict
```
训练完成后自动将最优权重写入 `moe_weights.json`,下次调用 `get_trade_signal()` 时自动生效。
---
## 数据库维护
### 初始化所有数据库(指标缓存库等)
```python
api.init_databases()
# init_databases() -> None
```
---
### 清除技术指标缓存,可指定股票或清除全部
```python
api.clear_indicator_cache('600519.SH') # 清除指定股票
api.clear_indicator_cache() # 清除所有
# clear_indicator_cache(code: str = None) -> None
```
---
## 实时行情 (爬虫)
### 初始化爬虫
```python
from stock_crawler import StockCrawler
crawler = StockCrawler()
```
### 获取实时数据
支持从新浪财经、东方财富、同花顺获取数据。
```python
data = crawler.fetch('000001.SZ', source='sina')
# fetch(ts_code: str, source: str = 'sina') -> Dict
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `ts_code` | `str` | - | 股票代码,如 `000001.SZ` |
| `source` | `str` | `'sina'` | 数据源:`'sina'` (新浪), `'eastmoney'` (东方财富), `'tonghuashun'` (同花顺) |
| 返回 | 说明 |
|------|------|
| `Dict` | 包含股票实时数据的字典,字段如下 |
**返回字段说明:**
| 字段 | 类型 | 说明 | 数据源支持 |
|------|------|------|------------|
| `source` | `str` | 数据源名称 | All |
| `ts_code` | `str` | 股票代码 | All |
| `status` | `str` | 状态 (`success`/`failed`) | All |
| `name` | `str` | 股票名称 | Sina, EastMoney |
| `price` | `float` | 当前价格 | All |
| `open` | `float` | 开盘价 | All |
| `high` | `float` | 最高价 | All |
| `low` | `float` | 最低价 | All |
| `volume` | `float` | 成交量 (股) | All |
| `amount` | `float` | 成交额 (元) | All |
| `pre_close` | `float` | 昨收价 | Sina, EastMoney |
| `date` | `str` | 日期 | Sina, Tonghuashun |
| `time` | `str` | 时间 | Sina |
| `turnover_rate` | `float` | 换手率 (%) | EastMoney, Tonghuashun |
| `change_pct` | `float` | 涨跌幅 (%) | EastMoney |
| `amplitude` | `float` | 振幅 (%) | Sina, EastMoney |
| `pe_ttm` | `float` | 市盈率(TTM) | EastMoney |
| `pb` | `float` | 市净率 | EastMoney |
| `total_cap` | `float` | 总市值 (元) | EastMoney |
| `circ_cap` | `float` | 流通市值 (元) | EastMoney |
| `total_shares` | `float` | 总股本 (股) | EastMoney |
| `circ_shares` | `float` | 流通股 (股) | EastMoney |
FILE:assets/config.json
{
"version": "1.0.0",
"base_url": "http://info.aicodingyard.com",
"http_timeout": 30
}
FILE:assets/requirements.txt
pandas>=1.3.5
requests>=2.31.0
SQLAlchemy>=2.0.48
numpy>=1.21.0
BitSoul旗下all-in-one的A股市场综合skill,提供股票筛选策略,内置上百种行业常见量化指标, 基于MOE混合因子专家模型的股票买卖点计算判断,个股风险判定,关键指标计算,数据回测,提供准确全面且免费的股票价格与股票历史信息,板块信息与相关交易数据,提供大v交易观察等信息聚合功能
---
name: BitSoulStockSkill
description: BitSoul旗下all-in-one的A股市场综合skill,提供股票筛选策略,内置上百种行业常见量化指标, 基于MOE混合因子专家模型的股票买卖点计算判断,个股风险判定,关键指标计算,数据回测,提供准确全面且免费的股票价格与股票历史信息,板块信息与相关交易数据,提供大v交易观察等信息聚合功能
version: 1.0.0
metadata:
openclaw:
emoji: "📈"
homepage: https://www.aicodingyard.com
requires:
env:
- BITSOUL_TOKEN
bins:
- python3
optional:
env:
- BITSOUL_TOKEN_ENV_FILE
- BITSOUL_CACHE_DIR
pythonPackages:
- pandas
- numpy
- requests
- sqlalchemy
network:
- info.aicodingyard.com
- https://finance.sina.com.cn/
primaryEnv: BITSOUL_TOKEN
---
# 简介
炒股龙虾的最佳搭档,best stock partner forever
## 核心优势
1. 免费稳定且每周更新的A股交易数据:为个股分析、买卖点计算、收益/回撤计算提供坚实的数据基础
2. 基于MOE混合因子专家模型的股票买卖点计算判断
3. 个股风险判定
4. 关键指标计算
5. 数据回测
6. 提供准确全面且免费的股票价格与股票历史信息
7. 板块信息与相关交易数据
8. 提供大V交易观察等信息聚合功能
# Token 配置
本 skill 需要有效的 `BITSOUL_TOKEN` 才能使用功能
token 可前往 <https://www.aicodingyard.com> 免费注册申请,并配置在外部运行环境中
## 必需的环境变量
* `BITSOUL_TOKEN`:用户令牌,用于远程服务器权限验证
## 可选的环境变量
* `BITSOUL_TOKEN_ENV_FILE`:指向包含 `BITSOUL_TOKEN` 的 env 文件
## 配置方式
1. **方式一:直接设置环境变量**
```bash
export BITSOUL_TOKEN="你的令牌"
```
2. **方式二:使用 env 文件**
```bash
export BITSOUL_TOKEN_ENV_FILE="/path/to/token.env"
```
其中 `token.env` 文件内容格式为:
```
BITSOUL_TOKEN=你的令牌
```
**注意**:如果同时设置了环境变量和 env 文件,环境变量优先。
## 运行时描述:
- 从环境变量读取 `BITSOUL_TOKEN`
- 只有在显式提供 `BITSOUL_TOKEN_ENV_FILE` 时,才会从文件中读取 `BITSOUL_TOKEN`
- 根据用户的自然语言,参考references/API_FOR_LLM.md 调用对应接口
- 对“分析 / 估值 / 基本面 / 趋势 / 风险”等请求自动切到综合分析, 需要moe因子计算,返回详细信息
- 对“交易观察 / 技术分析 / 均线 / 动量 / RSI / KDJ / 布林线 / MACD”等请求需要进行moe因子计算,同时需要调用calculate_metrics进行数据回测
- 返回结构化 JSON;查询场景优先给原始数据,分析场景给结论和支撑数据
- 任何返回的股票数据,都应包括个股的完整信息,不应遗漏任何字段
## 安全与运行边界
- 技能所需环境变量已经在本文件 frontmatter 中显式声明
- 策略回测、因子挖矿、实时行情查询等功能会访问 `info.aicodingyard.com` 服务器
- 技能只读取声明过的 token 相关环境变量,以及显式指定的 env 文件路径
- 技能不会主动扫描其他本地凭证文件,也不会写入 token 缓存文件
## 安装
使用前先安装 Python 依赖,依赖参考assets/requirements
首次安装需要执行初始化操作,在设置好BITSOUL_TOKEN后,请进行初始化操作,可参考scripts/data_fetcher
# 注意事项
* api接口文档主要参考 references/API_FOR_LLM.md 对应的代码文件是scripts/stock_api.py 和 scripts/define.py
* **凭证说明**:本skill需要用户Token用于数据访问权限验证。Token通过环境变量 `BITSOUL_TOKEN` 或 `BITSOUL_TOKEN_ENV_FILE` 传入。Token在数据访问时需要保持有效(请自行确保token未过期)。
* **缓存目录**:`BITSOUL_CACHE_DIR`,可选,用于指定缓存目录和数据存储路径。默认值为系统临时目录下的 `BitSoulStockSkill` 子目录
* **因子挖矿**:用户说"因子挖矿"、"挖矿"、"随机挖因子"、"碰碰运气"、"随机推荐"、"挖金矿"、"随机策略"时,直接调用 `api.random_alpha_backtest()`,禁止自己写回测逻辑。返回结果调用 `print(result['summary_text'])` 输出,禁止自行整理摘要。
* **因子挖矿结束后**:在 `print(result['summary_text'])` 之后,用自然语言向用户逐一解释本次使用的每个因子是什么含义、在策略中起什么作用。解释来源是 `result['factor_descriptions']`,格式示例:`alpha022:高价量5日相关的5日变化 × 收盘波动率,用于衡量量价相关动量的衰减程度,在本次策略中作为选股因子使用。`
* **买卖建议**:用户询问某只股票"能不能买"、"该不该卖"、"现在适合持有吗"、"操作建议"、"投资建议"、"买卖信号"、"值得买吗"、"要不要买"等,且用户指定了具体股票时,直接调用 `api.get_trade_signal(code)`,禁止自己计算指标做判断。
* **股票显示格式**:任何场景下输出股票代码时,必须同时附上股票名称,使用 `api.get_symbol_basic_infomation(code).name` 获取,格式如 `600519.SH(贵州茅台)`,禁止只输出代码。
* **买卖信号输出格式(强制执行)**:调用 `get_trade_signal()` 后,必须按以下结构完整输出,禁止简化:
1. **汇总表**:信号、综合评分、置信度、分析日期
2. **专家评分明细表**:列出 `result['experts']` 中所有专家(technical/alpha/fundamental/behavior),每个专家显示:评分、权重、有效指标数(valid_count/total_count)、note(若数据不足)
3. **各专家关键细节**(从 details 中挑重要的展示,不需要逐项列举):
- `technical`:说明看多/看空/中性指标各多少个,点出最关键的 2~3 个指标信号
- `behavior`:列出近5日涨跌幅、涨跌停次数等关键字段
- `fundamental` / `alpha`:若有数据则简要说明核心结论
4. **结论与建议**:引用 `reason` 字段,说明综合评分与阈值关系,给出操作建议
5. 免责声明
## 输出行为
- 默认使用简体中文,以报告的形式输出
- 尽量充分利用接口返回的所有数据,不要随意删减,尽可能多呈现结果内容
- 分析类请求默认返回结论、关键指标、风险提示与支撑摘要
## 示例请求
- `请整理东方财富过去10个交易日的股价信息,并输出成表格`
- `帮我看看同花顺近期最佳买点和卖点分别是多少,并给我些建议`
- `整理中国石油过去半年的财务数据,帮我分析是否具备投资价值`
- `过去一个月上龙虎榜最多的股票是哪只?`
- `请帮我因子挖矿,看看挖出的收益率和最大回撤是多少`
- `调用moe方法,帮我分析工业富联的买入点`
- `最近资金流入最快和涨幅最大的板块是哪些,有什么推荐`
- `给我做一份沪电股份的技术分析报告`
## 参考资料
- 机器可读目录:`references/API_FOR_LLM.dm`
FILE:scripts/backtest_tools.py
"""
backtest_tools.py - 回测工具模块
功能:
1. 交易模拟
2. 持仓管理
3. 组合价值计算
设计原则:
- 函数功能单一、最小粒度
- 纯函数,无副作用
"""
from typing import Dict, List, Tuple
from dataclasses import dataclass
@dataclass
class Position:
"""持仓记录
Attributes:
code: 股票代码,如 '000001.SZ'
shares: 持仓股数(股)
entry_price: 建仓均价(元),加仓时自动更新为加权均价
entry_date: 初次建仓日期,格式 'YYYY-MM-DD'
hold_days: 已持有天数,默认 0
"""
code: str
shares: int
entry_price: float
entry_date: str
hold_days: int = 0
def __repr__(self) -> str:
return (f"Position(code={self.code!r}, shares={self.shares}, "
f"entry_price={self.entry_price}, entry_date={self.entry_date!r}, "
f"hold_days={self.hold_days})")
@dataclass
class TradeResult:
"""单笔交易执行结果
Attributes:
success: 是否成交,False 时 reason 字段说明原因
code: 股票代码
action: 交易方向,'BUY' 或 'SELL'
price: 成交价格(元)
quantity: 成交数量(股)
cost: 买入总成本,含手续费(元);卖出时为 0
fee: 本次手续费(元)
net_proceeds: 卖出净收款,已扣手续费(元);买入时为 0
reason: 失败原因描述;成功时为空字符串
position: 交易后该股票的最新持仓;清仓后为 None
"""
success: bool
code: str
action: str
price: float
quantity: int
cost: float = 0.0
fee: float = 0.0
net_proceeds: float = 0.0
reason: str = ""
position: Position = None
def __repr__(self) -> str:
return (f"TradeResult(success={self.success}, code={self.code!r}, "
f"action={self.action!r}, price={self.price}, quantity={self.quantity}, "
f"cost={self.cost}, fee={self.fee}, net_proceeds={self.net_proceeds}, "
f"reason={self.reason!r}, position={self.position!r})")
def simulate_trade(action: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Dict:
"""模拟单笔交易,计算成本与手续费
买入:总成本 = 价格 × 数量 × (1 + 手续费率)
卖出:净收款 = 价格 × 数量 × (1 - 手续费率)
Args:
action: 交易方向,'BUY' 或 'SELL'(大小写不敏感)
price: 成交价格(元)
quantity: 成交数量(股)
fee_rate: 手续费率,默认 0.0003(即万三)
Returns:
Dict:
- cost (float): 买入总成本(元);卖出时为 0
- fee (float): 手续费(元)
- net_proceeds (float): 卖出净收款(元);买入时为 0
"""
action = action.upper()
fee = price * quantity * fee_rate
if action == 'BUY':
cost = price * quantity * (1 + fee_rate)
return {'cost': cost, 'fee': fee, 'net_proceeds': 0}
else:
net = price * quantity * (1 - fee_rate)
return {'cost': 0, 'fee': fee, 'net_proceeds': net}
def calculate_trade_cost(action: str, price: float, quantity: int, fee_rate: float = 0.0003, slippage: float = 0.0) -> float:
"""计算含滑点的交易总成本
在手续费基础上叠加滑点:买入价上浮、卖出价下浮。
仅返回总成本金额,不区分手续费与滑点。
Args:
action: 交易方向,'BUY' 或 'SELL'(大小写不敏感)
price: 名义价格(元)
quantity: 成交数量(股)
fee_rate: 手续费率,默认 0.0003(万三)
slippage: 滑点比例,默认 0.0;如 0.001 表示 0.1% 的价格偏移
Returns:
float: 含滑点与手续费的总成本(元)
"""
if action.upper() == 'BUY':
actual_price = price * (1 + slippage)
else:
actual_price = price * (1 - slippage)
return actual_price * quantity * (1 + fee_rate)
def create_position(code: str, shares: int, price: float, date: str) -> Position:
"""创建新持仓记录
Args:
code: 股票代码,如 '000001.SZ'
shares: 持仓股数(股)
price: 建仓价格(元)
date: 建仓日期,格式 'YYYY-MM-DD'
Returns:
Position: hold_days=0 的新持仓对象
"""
return Position(code=code, shares=shares, entry_price=price, entry_date=date, hold_days=0)
def update_position(position: Position, days: int = 1) -> Position:
"""更新持仓持有天数
Args:
position: 待更新的持仓对象
days: 本次新增天数,默认 1
Returns:
Position: 已更新 hold_days 的同一持仓对象(原地修改后返回)
"""
position.hold_days += days
return position
def get_position_value(position: Position, current_price: float) -> float:
"""计算持仓当前市值
Args:
position: 持仓对象
current_price: 当前市场价格(元)
Returns:
float: 持仓市值 = 持股数 × 当前价(元)
"""
return position.shares * current_price
def get_position_profit(position: Position, current_price: float) -> Tuple[float, float]:
"""计算持仓浮动盈亏
Args:
position: 持仓对象
current_price: 当前市场价格(元)
Returns:
Tuple[float, float]:
- float: 浮动盈亏金额(元),正数盈利,负数亏损
- float: 浮动盈亏比例(小数),如 0.1 表示盈利 10%
"""
profit = (current_price - position.entry_price) * position.shares
profit_pct = (current_price - position.entry_price) / position.entry_price
return profit, profit_pct
def calculate_portfolio_value(cash: float, positions: Dict[str, Position], prices: Dict[str, float]) -> float:
"""计算组合总价值
总价值 = 现金 + 各持仓市值之和。
若某股票当日无行情,则以建仓价代替当前价。
Args:
cash: 当前现金余额(元)
positions: 持仓字典 {股票代码: Position}
prices: 当日收盘价字典 {股票代码: 价格(元)}
Returns:
float: 组合总价值(元)
"""
position_value = sum(
p.shares * prices.get(p.code, p.entry_price)
for p in positions.values()
)
return cash + position_value
def get_portfolio_positions(positions: Dict[str, Position]) -> List[Dict]:
"""获取组合持仓详情列表
Args:
positions: 持仓字典 {股票代码: Position}
Returns:
List[Dict]: 每个元素包含以下字段:
- code (str): 股票代码
- shares (int): 持仓股数
- entry_price (float): 建仓均价(元)
- entry_date (str): 建仓日期
- hold_days (int): 已持有天数
"""
return [
{
'code': p.code,
'shares': p.shares,
'entry_price': p.entry_price,
'entry_date': p.entry_date,
'hold_days': p.hold_days,
}
for p in positions.values()
]
def build_equity_curve(daily_values: List[Tuple[str, float]]) -> List[float]:
"""从日期-价值序列提取权益曲线
Args:
daily_values: 每日 (日期, 账户总价值) 的有序列表
Returns:
List[float]: 仅含账户总价值的权益曲线,与 metrics.py 中各函数兼容
"""
return [value for _, value in daily_values]
def calculate_daily_returns(equity_curve: List[float]) -> List[float]:
"""计算逐日收益率序列
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
List[float]: 日收益率列表(小数形式),长度比 equity_curve 少 1;
equity_curve 不足 2 条时返回空列表
"""
if len(equity_curve) < 2:
return []
return [
(equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
for i in range(1, len(equity_curve))
if equity_curve[i-1] > 0
]
def should_buy(current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_oversold: float = 30) -> bool:
"""买入信号判断:均线金叉且 RSI 超卖
同时满足以下两个条件时触发买入:
1. 短期均线 > 长期均线(金叉,趋势向上)
2. RSI < rsi_oversold(超卖,价格低估)
Args:
current_price: 当前价格(元,本函数暂未使用,预留扩展)
ma_short: 短期均线值
ma_long: 长期均线值
rsi: 当前 RSI 值(0~100)
rsi_oversold: 超卖阈值,默认 30
Returns:
bool: True 表示触发买入信号
"""
return ma_short > ma_long and rsi < rsi_oversold
def should_sell(current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_overbought: float = 70) -> bool:
"""卖出信号判断:均线死叉或 RSI 超买
满足以下任意一个条件时触发卖出:
1. 短期均线 < 长期均线(死叉,趋势向下)
2. RSI > rsi_overbought(超买,价格高估)
Args:
current_price: 当前价格(元,本函数暂未使用,预留扩展)
ma_short: 短期均线值
ma_long: 长期均线值
rsi: 当前 RSI 值(0~100)
rsi_overbought: 超买阈值,默认 70
Returns:
bool: True 表示触发卖出信号
"""
return ma_short < ma_long or rsi > rsi_overbought
def calculate_drawdown(equity_curve: List[float]) -> List[float]:
"""计算逐日回撤序列
对每个时间点,计算从此前最高点到当日的回撤比例。
与 metrics.get_max_drawdown 的区别:该函数返回完整序列,可用于绘图。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
List[float]: 与 equity_curve 等长的回撤比例序列(0~1);
equity_curve 为空时返回空列表
"""
if not equity_curve:
return []
drawdowns = []
peak = equity_curve[0]
for value in equity_curve:
if value > peak:
peak = value
dd = (peak - value) / peak if peak > 0 else 0
drawdowns.append(dd)
return drawdowns
def buy(cash: float, positions: Dict[str, Position], code: str, price: float, quantity: int, date: str, fee_rate: float = 0.0003) -> Tuple[float, Dict[str, Position], TradeResult]:
"""买入股票
扣除买入成本后更新现金和持仓。若该股票已有持仓,以加权均价合并。
现金不足时不成交,返回失败的 TradeResult。
Args:
cash: 当前现金余额(元)
positions: 持仓字典 {股票代码: Position},会被原地修改
code: 股票代码,如 '000001.SZ'
price: 买入价格(元)
quantity: 买入数量(股)
date: 交易日期,格式 'YYYY-MM-DD'
fee_rate: 手续费率,默认 0.0003(万三)
Returns:
Tuple[float, Dict[str, Position], TradeResult]:
- float: 交易后现金余额(元)
- Dict[str, Position]: 交易后持仓字典
- TradeResult: 交易结果,success=False 时 reason='资金不足'
"""
trade = simulate_trade('BUY', price, quantity, fee_rate)
cost = trade['cost']
if cash < cost:
return cash, positions, TradeResult(
success=False,
code=code,
action='BUY',
price=price,
quantity=quantity,
reason='资金不足',
)
new_cash = cash - cost
if code in positions:
pos = positions[code]
total_cost = pos.entry_price * pos.shares + cost
new_shares = pos.shares + quantity
new_entry_price = total_cost / new_shares
positions[code] = Position(
code=code,
shares=new_shares,
entry_price=new_entry_price,
entry_date=pos.entry_date,
hold_days=0,
)
else:
positions[code] = Position(
code=code,
shares=quantity,
entry_price=price,
entry_date=date,
hold_days=0,
)
return new_cash, positions, TradeResult(
success=True,
code=code,
action='BUY',
price=price,
quantity=quantity,
cost=cost,
fee=trade['fee'],
position=positions[code],
)
def sell(cash: float, positions: Dict[str, Position], code: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Tuple[float, Dict[str, Position], TradeResult]:
"""卖出股票
将卖出净收款加回现金,并减少对应持仓股数。全部卖出时自动删除持仓记录。
无持仓或持仓不足时不成交,返回失败的 TradeResult。
Args:
cash: 当前现金余额(元)
positions: 持仓字典 {股票代码: Position},会被原地修改
code: 股票代码,如 '000001.SZ'
price: 卖出价格(元)
quantity: 卖出数量(股)
fee_rate: 手续费率,默认 0.0003(万三)
Returns:
Tuple[float, Dict[str, Position], TradeResult]:
- float: 交易后现金余额(元)
- Dict[str, Position]: 交易后持仓字典(清仓后该 code 键被删除)
- TradeResult: 交易结果,success=False 时 reason 说明原因
"""
if code not in positions:
return cash, positions, TradeResult(
success=False,
code=code,
action='SELL',
price=price,
quantity=quantity,
reason='无持仓',
)
pos = positions[code]
if pos.shares < quantity:
return cash, positions, TradeResult(
success=False,
code=code,
action='SELL',
price=price,
quantity=quantity,
reason=f'持仓不足(持有{pos.shares}股)',
)
trade = simulate_trade('SELL', price, quantity, fee_rate)
new_cash = cash + trade['net_proceeds']
new_shares = pos.shares - quantity
if new_shares == 0:
del positions[code]
else:
positions[code] = Position(
code=code,
shares=new_shares,
entry_price=pos.entry_price,
entry_date=pos.entry_date,
hold_days=pos.hold_days,
)
return new_cash, positions, TradeResult(
success=True,
code=code,
action='SELL',
price=price,
quantity=quantity,
fee=trade['fee'],
net_proceeds=trade['net_proceeds'],
position=positions.get(code),
)
FILE:scripts/config.py
import json
import utils
import os
from pathlib import Path
from typing import Optional
g_tmp_logic_path: str = ""
def _parse_dotenv_value(env_file: Path, key: str) -> Optional[str]:
if not env_file.exists():
return None
for line in env_file.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
if k.strip() == key:
return v.strip()
return None
def get_token() -> str:
token = os.environ.get("BITSOUL_TOKEN")
if token:
return token
env_file = os.environ.get("BITSOUL_TOKEN_ENV_FILE")
if env_file:
token_from_file = _parse_dotenv_value(Path(env_file).expanduser(), "BITSOUL_TOKEN")
if token_from_file:
return token_from_file
return ""
def get_cache_dir() -> str:
cache_dir = os.environ.get("BITSOUL_CACHE_DIR")
if cache_dir:
return cache_dir
env_file = os.environ.get("BITSOUL_TOKEN_ENV_FILE")
if env_file:
cache_from_file = _parse_dotenv_value(Path(env_file).expanduser(), "BITSOUL_CACHE_DIR")
if cache_from_file:
return cache_from_file
return utils.get_skill_work_dir()
def get_local_version() -> str:
with open(os.path.join(utils.get_skill_assets_dir(), "config.json"), "r", encoding="utf-8") as f:
data = json.load(f)
return data["version"]
def set_tmp_logic_path(fpath: str):
global g_tmp_logic_path
g_tmp_logic_path = fpath
def get_tmp_logic_path() -> str:
global g_tmp_logic_path
return g_tmp_logic_path
FILE:scripts/data_fetcher.py
"""
data_fetcher.py
===============
封装 HTTP 数据获取接口,将远程数据拉取并持久化到本地 SQLite 数据库,
同时提供本地数据库的查询接口。
接口规范参考: API_REFERENCE.md § "通用数据查询接口"
表结构参考: DATABASE_DOCUMENTATION.md
不依赖任何第三方库,仅使用 Python 3 标准库。
"""
import json
import datetime
import sqlite3
import urllib.error
import urllib.parse
import urllib.request
import shutil
import pandas as pd
import requests
import os
import decrypt_patch
from sqlalchemy import create_engine, text, Engine
from typing import List, Optional
from define import BASE_URL, HTTP_TIMEOUT, DB_PATH, StockBasic, DailyKline, HourKline, WeeklyKline, MonthlyKline, DailyBasic, Income, StockLimit, DailyLimitList, DailyBombList, SectorStockMap, TopList, TopInst, SectorFlowDaily, IndexBasic, IndexDaily, IndexWeekly, IndexMonthly
import utils
from logger import log
import config
import remote_api
from remote_api import PatchItem
from urllib.parse import urlparse
_g_engine: Engine = None
g_table_name_to_pk = {
"stock_basic" : ["ts_code"],
"hour_kline" : ["code","date", "time"],
"daily_kline" : ["code","date"],
"weekly_kline" : ["code","date"],
"monthly_kline" : ["code","date"],
"daily_basic": ["ts_code", "trade_date"],
"income": ["ts_code", "report_type", "end_date"],
"stock_limit": ["trade_date", "ts_code"],
"daily_limit_list": ["trade_date", "ts_code"],
"daily_bomb_list": ["trade_date", "ts_code"],
"sector_stock_map": ["sector_code", "stock_code"],
"top_list": ["id"],
"top_inst": ["id"],
"sector_flow_daily": ["trade_date", "ts_code"],
"index_basic": ["ts_code"],
"index_daily": ["trade_date", "ts_code"],
"index_weekly": ["trade_date", "ts_code"],
"index_monthly": ["trade_date", "ts_code"],
}
def getEngine() -> Engine:
global _g_engine
if not _g_engine:
_g_engine = create_engine(f"sqlite:///{DB_PATH}")
return _g_engine
class TablePatch:
"""
各个表目前应用的是哪个补丁的数据
字段说明:
patch 当前表数据是用的哪个patch,patch格式patch0、patch1等
"""
__slots__ = ("patch")
def __init__(self, patch: str):
self.patch = patch
@classmethod
def from_dict(cls, d: dict) -> "TablePatch":
"""从字典(API 响应或数据库行)构造 TablePatch 对象。"""
return cls(
patch=d.get("patch") or "",
)
def __repr__(self) -> str:
return f"TablePatch(patch={self.patch!r})"
# ============================================================
# 本地 SQLite 数据库管理
# ============================================================
def init_db() -> None:
"""
初始化本地 SQLite 数据库,创建 stock_basic 和 daily_kline 表(若不存在)。
若检测到旧版本表结构(字段不匹配),自动删除旧库重建。
"""
assets_dir = utils.get_skill_assets_dir()
base_data_file = os.path.join(assets_dir, "data_1.0.bin")
if not os.path.exists(base_data_file):
log(f"本地基础数据包 data_1.0.bin 不存在,正在从服务器下载...")
if not download_data_file("data_1.0.bin", base_data_file, max_retries=3):
log("错误:基础数据包下载失败,数据库初始化中止")
return
with getEngine().connect() as conn:
conn.execute(text("""
CREATE TABLE IF NOT EXISTS stock_basic (
ts_code TEXT NOT NULL,
symbol TEXT,
name TEXT,
area TEXT,
industry TEXT,
fullname TEXT,
enname TEXT,
cnspell TEXT,
market TEXT,
exchange TEXT,
curr_type TEXT,
list_status TEXT,
list_date TEXT,
delist_date TEXT,
is_hs TEXT,
PRIMARY KEY (ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS hour_kline (
date TEXT NOT NULL,
time TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
code TEXT NOT NULL,
PRIMARY KEY (code,date,time)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_kline (
date TEXT NOT NULL,
code TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
adjustflag TEXT,
turn REAL,
pctChg REAL,
pre_close REAL,
change REAL,
PRIMARY KEY (code,date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS weekly_kline (
date TEXT NOT NULL,
code TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
pctChg REAL,
PRIMARY KEY (code,date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS monthly_kline (
date TEXT NOT NULL,
code TEXT NOT NULL,
open REAL,
high REAL,
low REAL,
close REAL,
volume REAL,
amount REAL,
pctChg REAL,
PRIMARY KEY (code,date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_basic (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
turnover_rate REAL,
turnover_rate_f REAL,
volume_ratio REAL,
pe REAL,
pe_ttm REAL,
pb REAL,
ps REAL,
ps_ttm REAL,
dv_ratio REAL,
dv_ttm REAL,
total_share REAL,
float_share REAL,
free_share REAL,
total_mv REAL,
circ_mv REAL,
adj_factor REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS income (
ts_code TEXT NOT NULL,
end_date TEXT NOT NULL,
ann_date TEXT,
report_type TEXT NOT NULL,
comp_type TEXT,
basic_eps REAL,
diluted_eps REAL,
total_revenue REAL,
revenue REAL,
total_cogs REAL,
oper_cost REAL,
sell_exp REAL,
admin_exp REAL,
fin_exp REAL,
total_profit REAL,
income_tax REAL,
n_income REAL,
n_income_attr_p REAL,
minority_gain REAL,
oth_compr_income REAL,
t_compr_income REAL,
compr_inc_attr_p REAL,
ebit REAL,
ebitda REAL,
roe REAL,
roa REAL,
gross_margin REAL,
net_profit_margin REAL,
net_profit_yoy REAL,
revenue_yoy REAL,
equity_yoy REAL,
pcf REAL,
free_circ_mv REAL,
PRIMARY KEY (ts_code, report_type, end_date)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS stock_limit (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
pre_close REAL,
up_limit REAL,
down_limit REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_limit_list (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
name TEXT,
limit_type TEXT,
limit_price REAL,
pct_chg REAL,
volume REAL,
amount REAL,
limit_streak INTEGER,
sector TEXT,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS daily_bomb_list (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
name TEXT,
bomb_type TEXT,
limit_price REAL,
pct_chg REAL,
volume REAL,
amount REAL,
sector TEXT,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS sector_stock_map (
sector_code TEXT NOT NULL,
stock_code TEXT NOT NULL,
sector_name TEXT,
source TEXT,
PRIMARY KEY (sector_code, stock_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS top_list (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT,
ts_code TEXT,
name TEXT,
close REAL,
pct_change REAL,
turnover_rate REAL,
amount REAL,
l_sell REAL,
l_buy REAL,
l_amount REAL,
net_amount REAL,
net_rate REAL,
amount_rate REAL,
float_values REAL,
reason TEXT
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS top_inst (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trade_date TEXT,
ts_code TEXT,
exalter TEXT,
side TEXT,
buy REAL,
buy_rate REAL,
sell REAL,
sell_rate REAL,
net_buy REAL,
reason TEXT
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS sector_flow_daily (
trade_date TEXT NOT NULL,
content_type TEXT,
ts_code TEXT NOT NULL,
name TEXT,
pct_change REAL,
close REAL,
net_amount REAL,
net_amount_rate REAL,
buy_elg_amount REAL,
buy_elg_amount_rate REAL,
buy_lg_amount REAL,
buy_lg_amount_rate REAL,
buy_md_amount REAL,
buy_md_amount_rate REAL,
buy_sm_amount REAL,
buy_sm_amount_rate REAL,
buy_sm_amount_stock TEXT,
rank INTEGER,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_basic (
ts_code TEXT NOT NULL,
name TEXT,
fullname TEXT,
market TEXT,
publisher TEXT,
index_type TEXT,
category TEXT,
base_date TEXT,
base_point REAL,
list_date TEXT,
weight_rule TEXT,
desc TEXT,
exp_date TEXT,
PRIMARY KEY (ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_daily (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
open REAL,
high REAL,
low REAL,
pre_close REAL,
change REAL,
pct_chg REAL,
vol REAL,
amount REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_weekly (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
open REAL,
high REAL,
low REAL,
pre_close REAL,
change REAL,
pct_chg REAL,
vol REAL,
amount REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS index_monthly (
trade_date TEXT NOT NULL,
ts_code TEXT NOT NULL,
close REAL,
open REAL,
high REAL,
low REAL,
pre_close REAL,
change REAL,
pct_chg REAL,
vol REAL,
amount REAL,
PRIMARY KEY (trade_date, ts_code)
)
"""))
conn.execute(text("""
CREATE TABLE IF NOT EXISTS table_patch (
patch TEXT NOT NULL
)
"""))
conn.commit()
# ============================================================
# 本地 SQLite 查询接口
# ============================================================
def query_stock_basic(
ts_code: Optional[str] = None,
industry: Optional[str] = None,
industry_keyword: Optional[str] = None,
area: Optional[str] = None,
market: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[StockBasic]:
"""
从本地 SQLite 数据库查询 stock_basic 表,返回 StockBasic 对象列表。
参数:
ts_code 按股票代码精确过滤
industry 按行业名称精确过滤
industry_keyword 按行业名称关键词模糊过滤(LIKE %keyword%),与 industry 互斥,
优先使用 industry_keyword
area 按地区精确过滤
market 按市场精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
返回:
List[StockBasic] 符合条件的股票基础信息对象列表
示例:
all_stocks = query_stock_basic()
bank_stocks = query_stock_basic(industry="银行")
chip_stocks = query_stock_basic(industry_keyword="半导体")
single_stock = query_stock_basic(ts_code="000001.SZ")
"""
conditions = []
params: dict = {}
if ts_code is not None:
conditions.append("ts_code = :ts_code")
params["ts_code"] = ts_code
if industry_keyword is not None:
conditions.append("industry LIKE :industry_keyword")
params["industry_keyword"] = f"%{industry_keyword}%"
elif industry is not None:
conditions.append("industry = :industry")
params["industry"] = industry
if area is not None:
conditions.append("area = :area")
params["area"] = area
if market is not None:
conditions.append("market = :market")
params["market"] = market
sql = "SELECT * FROM stock_basic"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [StockBasic.from_dict(dict(row._mapping)) for row in rows]
def query_daily_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC",
) -> List[DailyKline]:
"""
从本地 SQLite 数据库查询 daily_kline 表,返回 DailyKline 对象列表。
参数:
code 按股票代码精确过滤
date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC"
返回:
List[DailyKline] 符合条件的日线行情对象列表
示例:
# 查询某只股票全部历史行情(按日期升序)
klines = query_daily_kline(code=["sz.000001"])
# 查询某只股票某段时间行情,最新的 30 条
klines = query_daily_kline(code=["sz.000001"],
start_date="2024-01-01", end_date="2024-12-31",
limit=30, order_by="date DESC")
# 查询某天全市场行情
klines = query_daily_kline(date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM daily_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
# SQLite IN 子句上限 999,codes 过多时分批查询后合并
if len(codes) > 900:
result = []
for i in range(0, len(codes), 900):
result.extend(
query_daily_kline(
codes=codes[i: i + 900],
date=date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
)
return result
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyKline.from_dict(dict(row._mapping)) for row in rows]
def query_hour_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC, time ASC",
) -> List[HourKline]:
"""
从本地 SQLite 数据库查询 hour_kline 表,返回 HourKline 对象列表。
参数:
codes 按股票代码列表过滤
date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC, time ASC"
返回:
List[HourKline] 符合条件的小时级别 K 线对象列表
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM hour_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [HourKline.from_dict(dict(row._mapping)) for row in rows]
def query_weekly_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC",
) -> List[WeeklyKline]:
"""
从本地 SQLite 数据库查询 weekly_kline 表,返回 WeeklyKline 对象列表。
参数:
codes 按股票代码列表过滤
date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC"
返回:
List[WeeklyKline] 符合条件的周线 K 线对象列表
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM weekly_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [WeeklyKline.from_dict(dict(row._mapping)) for row in rows]
def query_monthly_kline(
codes: List[str] = [],
date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "date ASC",
) -> List[MonthlyKline]:
"""
从本地 SQLite 数据库查询 monthly_kline 表,返回 MonthlyKline 对象列表。
参数:
codes 按股票代码列表过滤
date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "date ASC"
返回:
List[MonthlyKline] 符合条件的月线 K 线对象列表
"""
conditions = []
params: dict = {}
if len(codes) != 0:
keys = [f"code_{i}" for i in range(len(codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"code IN ({placeholders})")
for k, v in zip(keys, codes):
params[k] = v
if date is not None:
conditions.append("DATE(date) = :date")
params["date"] = date
else:
if start_date is not None:
conditions.append("DATE(date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM monthly_kline"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [MonthlyKline.from_dict(dict(row._mapping)) for row in rows]
def query_daily_basic(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBasic]:
"""
从本地 SQLite 数据库查询 daily_basic 表,返回 DailyBasic 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBasic] 符合条件的每日基本面指标对象列表
示例:
# 查询某只股票全部历史基本面数据
basics = query_daily_basic(ts_codes=["000001.SZ"])
# 查询某天全市场基本面数据
basics = query_daily_basic(trade_date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM daily_basic"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyBasic.from_dict(dict(row._mapping)) for row in rows]
def query_income(
ts_codes: List[str] = [],
report_type: Optional[str] = None,
end_date: Optional[str] = None,
start_end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "end_date ASC",
) -> List[Income]:
"""
从本地 SQLite 数据库查询 income 表,返回 Income 对象列表。
参数:
ts_codes 按股票代码列表过滤
report_type 按报告类型精确过滤(如 "1" 表示合并报表)
end_date 按报告期结束日期精确过滤,格式 "YYYY-MM-DD"
start_end_date 按报告期结束日期范围过滤下限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "end_date ASC"
返回:
List[Income] 符合条件的利润表对象列表
示例:
# 查询某只股票全部利润表(合并报表)
records = query_income(ts_codes=["000001.SZ"], report_type="1")
# 查询某报告期全市场数据
records = query_income(end_date="20231231")
# 查询最新一期
records = query_income(ts_codes=["000001.SZ"], order_by="end_date DESC", limit=1)
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if report_type is not None:
conditions.append("report_type = :report_type")
params["report_type"] = report_type
if end_date is not None:
conditions.append("end_date = :end_date")
params["end_date"] = end_date
elif start_end_date is not None:
conditions.append("end_date >= :start_end_date")
params["start_end_date"] = start_end_date
sql = "SELECT * FROM income"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [Income.from_dict(dict(row._mapping)) for row in rows]
def query_stock_limit(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[StockLimit]:
"""
从本地 SQLite 数据库查询 stock_limit 表,返回 StockLimit 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[StockLimit] 符合条件的每日涨跌停价格对象列表
示例:
# 查询某只股票的涨跌停价格历史
limits = query_stock_limit(ts_codes=["000001.SZ"])
# 查询某天全市场涨跌停价格
limits = query_stock_limit(trade_date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM stock_limit"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [StockLimit.from_dict(dict(row._mapping)) for row in rows]
def query_daily_limit_list(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyLimitList]:
"""
从本地 SQLite 数据库查询 daily_limit_list 表,返回 DailyLimitList 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit_type 按榜单类型过滤(U=涨停, D=跌停)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyLimitList] 符合条件的每日涨跌停榜单对象列表
示例:
# 查询某天所有涨停股
records = query_daily_limit_list(trade_date="2024-06-03", limit_type="U")
# 查询某只股票历史上榜记录
records = query_daily_limit_list(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
if limit_type is not None:
conditions.append("limit_type = :limit_type")
params["limit_type"] = limit_type
sql = "SELECT * FROM daily_limit_list"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyLimitList.from_dict(dict(row._mapping)) for row in rows]
def query_daily_bomb_list(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
bomb_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBombList]:
"""
从本地 SQLite 数据库查询 daily_bomb_list 表,返回 DailyBombList 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
bomb_type 按炸板类型过滤(U=曾涨停, D=曾跌停/撬板)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBombList] 符合条件的每日炸板榜单对象列表
示例:
# 查询某天所有炸板(曾涨停)股票
records = query_daily_bomb_list(trade_date="2024-06-03", bomb_type="U")
# 查询某只股票历史炸板记录
records = query_daily_bomb_list(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
if bomb_type is not None:
conditions.append("bomb_type = :bomb_type")
params["bomb_type"] = bomb_type
sql = "SELECT * FROM daily_bomb_list"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [DailyBombList.from_dict(dict(row._mapping)) for row in rows]
def query_sector_stock_map(
sector_codes: List[str] = [],
stock_codes: List[str] = [],
source: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[SectorStockMap]:
"""
从本地 SQLite 数据库查询 sector_stock_map 表,返回 SectorStockMap 对象列表。
参数:
sector_codes 按板块代码列表过滤
stock_codes 按股票代码列表过滤
source 按数据来源精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询某个板块下的所有股票
records = query_sector_stock_map(sector_codes=["BK0475"])
# 查询某只股票归属的所有板块
records = query_sector_stock_map(stock_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(sector_codes) != 0:
keys = [f"sector_code_{i}" for i in range(len(sector_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"sector_code IN ({placeholders})")
for k, v in zip(keys, sector_codes):
params[k] = v
if len(stock_codes) != 0:
keys = [f"stock_code_{i}" for i in range(len(stock_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"stock_code IN ({placeholders})")
for k, v in zip(keys, stock_codes):
params[k] = v
if source is not None:
conditions.append("source = :source")
params["source"] = source
sql = "SELECT * FROM sector_stock_map"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [SectorStockMap.from_dict(dict(row._mapping)) for row in rows]
def query_top_list(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopList]:
"""
从本地 SQLite 数据库查询 top_list 表,返回 TopList 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天龙虎榜数据
records = query_top_list(trade_date="2024-06-03")
# 查询某只股票历史上榜记录
records = query_top_list(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM top_list"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [TopList.from_dict(dict(row._mapping)) for row in rows]
def query_top_inst(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
side: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopInst]:
"""
从本地 SQLite 数据库查询 top_inst 表,返回 TopInst 对象列表。
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
side 按买卖类型过滤("0"=买入, "1"=卖出)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天机构交易明细
records = query_top_inst(trade_date="2024-06-03")
# 查询某只股票历史机构上榜记录
records = query_top_inst(ts_codes=["000001.SZ"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
if side is not None:
conditions.append("side = :side")
params["side"] = side
sql = "SELECT * FROM top_inst"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [TopInst.from_dict(dict(row._mapping)) for row in rows]
def query_sector_flow_daily(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[SectorFlowDaily]:
"""
从本地 SQLite 数据库查询 sector_flow_daily 表,返回 SectorFlowDaily 对象列表。
参数:
ts_codes 按板块代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天所有板块资金流向
records = query_sector_flow_daily(trade_date="2024-06-03")
# 查询某个板块历史资金流向
records = query_sector_flow_daily(ts_codes=["BK0475"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM sector_flow_daily"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [SectorFlowDaily.from_dict(dict(row._mapping)) for row in rows]
def query_index_basic(
ts_code: Optional[str] = None,
market: Optional[str] = None,
publisher: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[IndexBasic]:
"""
从本地 SQLite 数据库查询 index_basic 表,返回 IndexBasic 对象列表。
参数:
ts_code 按指数代码精确过滤
market 按市场精确过滤
publisher 按发布方精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询所有指数
records = query_index_basic()
# 查询上证指数信息
records = query_index_basic(ts_code="000001.SH")
"""
conditions = []
params: dict = {}
if ts_code is not None:
conditions.append("ts_code = :ts_code")
params["ts_code"] = ts_code
if market is not None:
conditions.append("market = :market")
params["market"] = market
if publisher is not None:
conditions.append("publisher = :publisher")
params["publisher"] = publisher
sql = "SELECT * FROM index_basic"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexBasic.from_dict(dict(row._mapping)) for row in rows]
def query_index_daily(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexDaily]:
"""
从本地 SQLite 数据库查询 index_daily 表,返回 IndexDaily 对象列表。
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数历史日线
records = query_index_daily(ts_codes=["000001.SH"])
# 查询某天所有指数行情
records = query_index_daily(trade_date="2024-06-03")
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM index_daily"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexDaily.from_dict(dict(row._mapping)) for row in rows]
def query_index_weekly(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexWeekly]:
"""
从本地 SQLite 数据库查询 index_weekly 表,返回 IndexWeekly 对象列表。
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数周线
records = query_index_weekly(ts_codes=["000001.SH"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM index_weekly"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexWeekly.from_dict(dict(row._mapping)) for row in rows]
def query_index_monthly(
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexMonthly]:
"""
从本地 SQLite 数据库查询 index_monthly 表,返回 IndexMonthly 对象列表。
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数月线
records = query_index_monthly(ts_codes=["000001.SH"])
"""
conditions = []
params: dict = {}
if len(ts_codes) != 0:
keys = [f"ts_code_{i}" for i in range(len(ts_codes))]
placeholders = ",".join(f":{k}" for k in keys)
conditions.append(f"ts_code IN ({placeholders})")
for k, v in zip(keys, ts_codes):
params[k] = v
if trade_date is not None:
conditions.append("DATE(trade_date) = :trade_date")
params["trade_date"] = trade_date
else:
if start_date is not None:
conditions.append("DATE(trade_date) >= DATE('{0}')".format(start_date))
if end_date is not None:
conditions.append("DATE(trade_date) <= DATE('{0}')".format(end_date))
sql = "SELECT * FROM index_monthly"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
sql += f" ORDER BY {order_by}"
if limit is not None:
sql += f" LIMIT {int(limit)} OFFSET {int(offset)}"
with getEngine().connect() as conn:
cursor = conn.execute(text(sql), params)
rows = cursor.fetchall()
return [IndexMonthly.from_dict(dict(row._mapping)) for row in rows]
def get_local_patch_ver() -> int:
"""从 table_patch 表中读取当前本地 patch 版本号,无记录时返回 -1。"""
with getEngine().connect() as conn:
row = conn.execute(text("SELECT patch FROM table_patch LIMIT 1")).fetchone()
print(int(row[0]) if row else -1)
return int(row[0]) if row else -1
def download_data_file(file_name: str, output_path: str, max_retries: int = 3) -> bool:
"""
从服务器下载数据文件。
参数:
file_name: 要下载的文件名(如 data_1.0.bin)
output_path: 保存路径
max_retries: 最大重试次数
返回:
bool: 下载成功返回 True,否则返回 False
"""
token = config.get_token()
if not token:
log("错误:未设置 Token,无法下载数据文件")
return False
for retry in range(max_retries):
try:
download_url = remote_api.request_download_url(file_name, token)
if not download_url:
log(f"错误:获取 {file_name} 下载链接失败,请检查 Token 是否有效")
return False
log(f"开始下载 {file_name} ...")
log(f"下载地址: {download_url}")
with requests.get(download_url, stream=True, timeout=300) as response:
if response.status_code != 200:
log(f"下载失败,HTTP 状态码: {response.status_code}")
if retry < max_retries - 1:
log(f"重试 {retry + 1}/{max_retries} ...")
continue
return False
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
chunk_size = 1024 * 1024
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
pct = (downloaded / total_size) * 100
if downloaded % (10 * chunk_size) < chunk_size:
log(f"下载进度: {pct:.1f}%")
log(f"文件下载完成: {output_path}")
return True
except Exception as e:
log(f"下载出错: {str(e)}")
if retry < max_retries - 1:
log(f"重试 {retry + 1}/{max_retries} ...")
else:
log(f"下载失败,已达到最大重试次数")
return False
return False
def download_from_url(url: str, output_path: str, timeout: int = 300) -> bool:
"""
从指定 URL 下载文件到指定路径。
参数:
url: 下载链接
output_path: 保存路径
timeout: 超时时间(秒)
返回:
bool: 下载成功返回 True,否则返回 False
"""
try:
log(f"从 URL 下载文件: {url}")
log(f"保存路径: {output_path}")
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with requests.get(url, stream=True, timeout=timeout) as response:
if response.status_code != 200:
log(f"下载失败,HTTP 状态码: {response.status_code}")
return False
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
chunk_size = 1024 * 1024
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunk_size):
if chunk:
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
pct = downloaded / total_size * 100
if downloaded % (10 * chunk_size) < chunk_size:
log(f"下载进度: {pct:.1f}%")
log(f"文件下载完成: {output_path}")
return True
except Exception as e:
log(f"下载出错: {str(e)}")
return False
def syn_table_datas() -> List[str]:
"""
根据表名,获取需要下载的 patch 列表。
逻辑:
1. 调用 request_patch_list() 获取服务器上该表的全部可用 patch 列表
2. 查询本地 table_patch 表,找到该表当前已应用的 patch
3. 返回当前 patch 之后(不含)的所有 patch,即待下载的部分;
若本地无记录,则返回全部可用 patch
参数:
table_name 指定表的名称
返回:
List[str] 待下载的 patch 名称列表(按顺序)
"""
local_patch_ver = -1
with getEngine().connect() as conn:
cursor = conn.execute(
text("SELECT patch FROM table_patch")
)
row = cursor.fetchone()
if row:
local_patch_ver = int(row[0])
conn.commit()
log(f"本地数据patch ver:{local_patch_ver}")
if local_patch_ver < 0:
log(f"导入基础数据...")
assets_dir = utils.get_skill_assets_dir()
name = "data_1.0.bin"
base_patch_zip = os.path.join(assets_dir, name)
base_patch_decrypt_zip = os.path.join(utils.get_skill_work_dir(), "data_1.0_decrypt.zip")
decrypt_key = remote_api.request_decrypt_key(name, config.get_token())
if len(decrypt_key) == 0:
log("错误:没有数据读取权限,请先注册")
return
decrypt_patch.process_file(base_patch_zip, base_patch_decrypt_zip, decrypt_key, False)
base_patch_dir = os.path.join(utils.get_skill_work_dir(), "data_1.0")
if os.path.exists(base_patch_dir):
shutil.rmtree(base_patch_dir)
utils.unzip_file(base_patch_decrypt_zip, base_patch_dir)
import_datas_in_dir(base_patch_dir)
with getEngine().connect() as conn:
conn.execute(text("DELETE FROM table_patch"))
conn.execute(text("INSERT INTO table_patch (patch) VALUES (0)"))
conn.commit()
os.remove(base_patch_decrypt_zip)
shutil.rmtree(base_patch_dir)
remote_patchs: List[PatchItem] = remote_api.request_patch_list()
log(f"remote patch list:{','.join([str(r_patch.version) for r_patch in remote_patchs])}")
for r_patch in remote_patchs:
if r_patch.version > local_patch_ver:
request_and_import_remote_patch_by_name(r_patch.patch_name, r_patch.version)
def request_and_import_remote_patch_by_name(patch_name:str, patch_ver: int):
log(f"更新remote patch ver:{patch_ver}, name:{patch_name}......")
url = f"{BASE_URL}/api/download_file"
params = {
"file_name": patch_name,
"token_key": config.get_token()
}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
download_url = data.get("download_url")
tmp_patch_zip = os.path.join(utils.get_skill_work_dir(), patch_name)
tmp_patch_decrypt_zip = os.path.join(utils.get_skill_work_dir(), f"decrypt_{patch_name}")
tmp_patch_dir = os.path.join(utils.get_skill_work_dir(), "tmp_patch_unzip")
# 下载
utils.download_file(download_url, tmp_patch_zip)
# 解密
decrypt_key = remote_api.request_decrypt_key(patch_name, config.get_token())
if len(decrypt_key) == 0:
log("错误:没有数据读取权限,请先注册")
return
decrypt_patch.process_file(tmp_patch_zip, tmp_patch_decrypt_zip, decrypt_key, False)
# 解压
utils.unzip_file(tmp_patch_decrypt_zip, tmp_patch_dir)
import_datas_in_dir(tmp_patch_dir)
with getEngine().connect() as conn:
conn.execute(text("DELETE FROM table_patch"))
conn.execute(text(f"INSERT INTO table_patch (patch) VALUES ({patch_ver})"))
conn.commit()
shutil.rmtree(tmp_patch_dir)
os.remove(tmp_patch_zip)
os.remove(tmp_patch_decrypt_zip)
log(f"更新本地数据patch ver:{patch_ver}")
def import_datas_in_dir(dir: str):
files = utils.scan_files_in_dir(dir)
for file in files:
basename = os.path.basename(file)
for table_name in g_table_name_to_pk.keys():
if basename.startswith(table_name):
import_data_to_table(file, table_name)
def import_data_to_table(input_file:str, table_name:str):
log(f"导入表{table_name} from {input_file}")
if input_file.endswith('.csv.gz'):
df = pd.read_csv(input_file, compression='gzip')
elif input_file.endswith('.csv'):
df = pd.read_csv(input_file)
elif input_file.endswith('.pkl'):
df = pd.read_pickle(input_file)
else:
assert False
engine = getEngine()
raw_conn = engine.raw_connection()
try:
df.to_sql("tmp_import", raw_conn, if_exists="replace", index=False)
cursor = raw_conn.cursor()
cursor.execute(f"INSERT OR REPLACE INTO {table_name} SELECT * FROM tmp_import")
raw_conn.commit()
cursor.close()
finally:
raw_conn.close()
def syn_vip_basic_data():
"""
更新vip基础数据包
"""
url = f"{BASE_URL}/api/history_data"
response = requests.get(url, params={"token": config.get_token()})
if response.status_code == 200:
file_url = response.json().get("download_url")
filename = os.path.basename(urlparse(file_url).path)
print("下载中...")
tmp_patch_zip = os.path.join(utils.get_skill_work_dir(), filename)
tmp_patch_decrypt_zip = os.path.join(utils.get_skill_work_dir(), f"decrypt_{filename}")
tmp_patch_dir = os.path.join(utils.get_skill_work_dir(), "tmp_history_unzip")
decrypt_key = remote_api.request_decrypt_key(filename, config.get_token())
# 解密
if len(decrypt_key) == 0:
log("错误:没有数据读取权限,请先注册")
return
# 下载
utils.download_file(file_url, tmp_patch_zip)
decrypt_patch.process_file(tmp_patch_zip, tmp_patch_decrypt_zip, decrypt_key, False)
# 解压
utils.unzip_file(tmp_patch_decrypt_zip, tmp_patch_dir)
import_datas_in_dir(tmp_patch_dir)
if __name__ == "__main__":
log(f"数据库路径:{DB_PATH}")
init_db()
syn_table_datas()
# syn_vip_basic_data()
# testfunc()
FILE:scripts/decrypt_patch.py
import os
import hashlib
import random
import struct
import argparse
def derive_seed(key, iv):
"""Derive a seed for the random number generator from Key and IV."""
h = hashlib.sha256()
h.update(key.encode('utf-8'))
h.update(iv)
return int.from_bytes(h.digest(), 'big')
def process_file(input_path, output_path, key, is_encrypt=True):
"""
Encrypt or Decrypt a file using a stream cipher based on Python's random (Mersenne Twister).
Uses large integer XOR for performance.
"""
chunk_size = 1024 * 1024 # 1MB chunks
with open(input_path, 'rb') as fin, open(output_path, 'wb') as fout:
if is_encrypt:
# Generate IV
iv = os.urandom(16)
fout.write(iv)
else:
# Read IV
iv = fin.read(16)
if len(iv) < 16:
raise ValueError("File too short or corrupted.")
# Initialize PRNG
seed = derive_seed(key, iv)
rng = random.Random(seed)
while True:
chunk = fin.read(chunk_size)
if not chunk:
break
# Generate mask (compatible with Python < 3.9, same as encrypt)
mask = rng.getrandbits(len(chunk) * 8).to_bytes(len(chunk), 'little')
# Fast XOR using integers
chunk_int = int.from_bytes(chunk, 'little')
mask_int = int.from_bytes(mask, 'little')
encrypted_int = chunk_int ^ mask_int
# Convert back to bytes
encrypted_chunk = encrypted_int.to_bytes(len(chunk), 'little')
fout.write(encrypted_chunk)
FILE:scripts/define.py
import os
import json
from typing import Optional
import utils
import config
def _load_config():
config_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "config.json")
if os.path.exists(config_path):
try:
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
pass
return {"base_url": "", "http_timeout": 30}
_config = _load_config()
# ============================================================
# 常量
# ============================================================
BASE_URL = _config.get("base_url", "")
HTTP_TIMEOUT = _config.get("http_timeout", 30)
DB_PATH = os.path.join(config.get_cache_dir(), "data.db")
def get_cache_dir() -> str:
return config.get_cache_dir()
# ============================================================
# 数据模型
# ============================================================
class RealtimeStockQuote:
"""
实时股票报价信息
字段说明:
ts_code 股票代码(如 000001.SZ)
name 股票名称
open 今日开盘价
pre_close 昨日收盘价
price 当前最新价
high 今日最高价
low 今日最低价
bid 买一价
ask 卖一价
volume 成交量(股)
amount 成交额(元)
date 交易日期(YYYY-MM-DD)
time 最新报价时间(HH:MM:SS)
amplitude 振幅(%)
turnover_rate 换手率(%),可能为空
total_cap 总市值(元),可能为空
circ_cap 流通市值(元),可能为空
pb 市净率,可能为空
pe_ttm 市盈率(TTM),可能为空
total_shares 总股本(股),可能为空
circ_shares 流通股本(股),可能为空
status 请求状态(success / error)
"""
__slots__ = ("ts_code", "name", "open", "pre_close", "price",
"high", "low", "bid", "ask", "volume", "amount",
"date", "time", "amplitude", "turnover_rate",
"total_cap", "circ_cap", "pb", "pe_ttm",
"total_shares", "circ_shares", "status")
def __init__(self, ts_code: str, name: str, open: float, pre_close: float,
price: float, high: float, low: float, bid: float, ask: float,
volume: int, amount: float, date: str, time: str,
amplitude: float, turnover_rate: Optional[float],
total_cap: Optional[float], circ_cap: Optional[float],
pb: Optional[float], pe_ttm: Optional[float],
total_shares: Optional[float], circ_shares: Optional[float],
status: str):
self.ts_code = ts_code
self.name = name
self.open = open
self.pre_close = pre_close
self.price = price
self.high = high
self.low = low
self.bid = bid
self.ask = ask
self.volume = volume
self.amount = amount
self.date = date
self.time = time
self.amplitude = amplitude
self.turnover_rate = turnover_rate
self.total_cap = total_cap
self.circ_cap = circ_cap
self.pb = pb
self.pe_ttm = pe_ttm
self.total_shares = total_shares
self.circ_shares = circ_shares
self.status = status
@classmethod
def from_dict(cls, d: dict) -> "RealtimeStockQuote":
def _f(v):
try:
return float(v) if v is not None and v != "" else None
except (TypeError, ValueError):
return None
return cls(
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
open=float(d.get("open") or 0.0),
pre_close=float(d.get("pre_close") or 0.0),
price=float(d.get("price") or 0.0),
high=float(d.get("high") or 0.0),
low=float(d.get("low") or 0.0),
bid=float(d.get("bid") or 0.0),
ask=float(d.get("ask") or 0.0),
volume=int(d.get("volume") or 0),
amount=float(d.get("amount") or 0.0),
date=d.get("date") or "",
time=d.get("time") or "",
amplitude=float(d.get("amplitude") or 0.0),
turnover_rate=_f(d.get("turnover_rate")),
total_cap=_f(d.get("total_cap")),
circ_cap=_f(d.get("circ_cap")),
pb=_f(d.get("pb")),
pe_ttm=_f(d.get("pe_ttm")),
total_shares=_f(d.get("total_shares")),
circ_shares=_f(d.get("circ_shares")),
status=d.get("status") or "",
)
def __repr__(self) -> str:
return (f"RealtimeStockQuote(ts_code={self.ts_code!r}, name={self.name!r}, "
f"price={self.price}, date={self.date!r})")
class StockBasic:
"""
股票基础信息,对应远程 stock_basic 表及本地同名表。
字段说明:
ts_code 股票代码,如 000001.SZ
symbol 股票符号,如 000001
name 股票名称,如 平安银行
area 所在地区
industry 所属行业
fullname 股票全称
enname 英文名称
cnspell 拼音
market 市场类型(主板/创业板/科创板等)
exchange 交易所代码
curr_type 交易货币
list_date 上市日期,格式 YYYY-MM-DD
list_status 上市状态 (L=上市, D=退市, G=过会未交易, P=暂停上市)
delist_date 退市日期(未退市则为空),格式 YYYY-MM-DD
is_hs 是否沪深港通标的(N=否, H=沪股通, S=深股通)
"""
__slots__ = ("ts_code", "symbol", "name", "area", "industry",
"fullname", "enname", "cnspell", "market", "exchange",
"curr_type", "list_date", "list_status", "delist_date", "is_hs")
def __init__(self, ts_code: str, symbol: str, name: str,
area: str, industry: str, fullname: str, enname: str,
cnspell: str, market: str, exchange: str, curr_type: str,
list_date: str, list_status:str, delist_date: str, is_hs: str):
self.ts_code = ts_code
self.symbol = symbol
self.name = name
self.area = area
self.industry = industry
self.fullname = fullname
self.enname = enname
self.cnspell = cnspell
self.market = market
self.exchange = exchange
self.curr_type = curr_type
self.list_date = list_date
self.list_status = list_status
self.delist_date = delist_date
self.is_hs = is_hs
@classmethod
def from_dict(cls, d: dict) -> "StockBasic":
"""从字典(API 响应或数据库行)构造 StockBasic 对象。"""
return cls(
ts_code=d.get("ts_code") or "",
symbol=d.get("symbol") or "",
name=d.get("name") or "",
area=d.get("area") or "",
industry=d.get("industry") or "",
fullname=d.get("fullname") or "",
enname=d.get("enname") or "",
cnspell=d.get("cnspell") or "",
market=d.get("market") or "",
exchange=d.get("exchange") or "",
curr_type=d.get("curr_type") or "",
list_date=d.get("list_date") or "",
list_status=d.get("list_status") or "",
delist_date=d.get("delist_date") or "",
is_hs=d.get("is_hs") or "",
)
def __repr__(self) -> str:
return f"StockBasic(ts_code={self.ts_code!r}, name={self.name!r}, market={self.market!r})"
class DailyKline:
"""
日线行情数据,对应远程 daily_kline 表及本地同名表。
字段说明:
date 交易日期,格式 YYYY-MM-DD
code 股票代码,如 sz.000001
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量(股)
amount 成交额(元)
adjustflag 复权状态
turn 换手率
pctChg 涨跌幅(%)
pre_close 前收盘价
change 涨跌额
"""
__slots__ = ("date", "code", "open", "high", "low", "close",
"volume", "amount", "adjustflag", "turn", "pctChg",
"pre_close", "change")
def __init__(self, date: str, code: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
adjustflag: str, turn: float, pctChg: float,
pre_close: float, change: float):
self.date = date
self.code = code
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.adjustflag = adjustflag
self.turn = turn
self.pctChg = pctChg
self.pre_close = pre_close
self.change = change
@classmethod
def from_dict(cls, d: dict) -> "DailyKline":
"""从字典(API 响应或数据库行)构造 DailyKline 对象。"""
def _f(v):
"""将值安全转换为 float,None/空字符串返回 0.0。"""
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
code=d.get("code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
adjustflag=d.get("adjustflag") or "",
turn=_f(d.get("turn")),
pctChg=_f(d.get("pctChg")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
)
def __repr__(self) -> str:
return (f"DailyKline(date={self.date!r}, code={self.code!r}, "
f"close={self.close}, pctChg={self.pctChg})")
class HourKline:
"""
小时级别 K 线行情数据,对应本地 hour_kline 表。
字段说明:
date 交易日期,格式 "YYYY-MM-DD"
time 交易时间
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量
amount 成交额
code 股票代码
"""
__slots__ = ("date", "time", "open", "high", "low", "close",
"volume", "amount", "code")
def __init__(self, date: str, time: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
code: str):
self.date = date
self.time = time
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.code = code
@classmethod
def from_dict(cls, d: dict) -> "HourKline":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
time=d.get("time") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
code=d.get("code") or "",
)
def __repr__(self) -> str:
return (f"HourKline(date={self.date!r}, time={self.time!r}, "
f"code={self.code!r}, close={self.close})")
class WeeklyKline:
"""
周线行情数据,对应本地 weekly_kline 表。
字段说明:
date 交易日期(周五日期),格式 YYYY-MM-DD
code 股票代码
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量
amount 成交额
pctChg 涨跌幅(%)
"""
__slots__ = ("date", "code", "open", "high", "low", "close",
"volume", "amount", "pctChg")
def __init__(self, date: str, code: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
pctChg: float):
self.date = date
self.code = code
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.pctChg = pctChg
@classmethod
def from_dict(cls, d: dict) -> "WeeklyKline":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
code=d.get("code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
pctChg=_f(d.get("pctChg")),
)
def __repr__(self) -> str:
return (f"WeeklyKline(date={self.date!r}, code={self.code!r}, "
f"close={self.close}, pctChg={self.pctChg})")
class MonthlyKline:
"""
月线行情数据,对应本地 monthly_kline 表。
字段说明:
date 交易日期(月末日期),格式 YYYY-MM-DD
code 股票代码
open 开盘价
high 最高价
low 最低价
close 收盘价
volume 成交量
amount 成交额
pctChg 涨跌幅(%)
"""
__slots__ = ("date", "code", "open", "high", "low", "close",
"volume", "amount", "pctChg")
def __init__(self, date: str, code: str, open: float, high: float,
low: float, close: float, volume: float, amount: float,
pctChg: float):
self.date = date
self.code = code
self.open = open
self.high = high
self.low = low
self.close = close
self.volume = volume
self.amount = amount
self.pctChg = pctChg
@classmethod
def from_dict(cls, d: dict) -> "MonthlyKline":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
date=d.get("date") or "",
code=d.get("code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
pctChg=_f(d.get("pctChg")),
)
def __repr__(self) -> str:
return (f"MonthlyKline(date={self.date!r}, code={self.code!r}, "
f"close={self.close}, pctChg={self.pctChg})")
class DailyBasic:
"""
每日基本面指标数据,对应本地 daily_basic 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
close 当日收盘价
turnover_rate 换手率(%)
turnover_rate_f 换手率(自由流通股)
volume_ratio 量比
pe 市盈率(总市值/净利润)
pe_ttm 市盈率(TTM)
pb 市净率(总市值/净资产)
ps 市销率
ps_ttm 市销率(TTM)
dv_ratio 股息率(%)
dv_ttm 股息率(TTM)(%)
total_share 总股本(万股)
float_share 流通股本(万股)
free_share 自由流通股本(万)
total_mv 总市值(万元)
circ_mv 流通市值(万元)
adj_factor 复权因子
"""
__slots__ = ("trade_date", "ts_code", "close", "turnover_rate",
"turnover_rate_f", "volume_ratio", "pe", "pe_ttm",
"pb", "ps", "ps_ttm", "dv_ratio", "dv_ttm",
"total_share", "float_share", "free_share",
"total_mv", "circ_mv", "adj_factor")
def __init__(self, trade_date: str, ts_code: str, close: float,
turnover_rate: float, turnover_rate_f: float, volume_ratio: float,
pe: float, pe_ttm: float, pb: float, ps: float, ps_ttm: float,
dv_ratio: float, dv_ttm: float, total_share: float,
float_share: float, free_share: float, total_mv: float,
circ_mv: float, adj_factor: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.close = close
self.turnover_rate = turnover_rate
self.turnover_rate_f = turnover_rate_f
self.volume_ratio = volume_ratio
self.pe = pe
self.pe_ttm = pe_ttm
self.pb = pb
self.ps = ps
self.ps_ttm = ps_ttm
self.dv_ratio = dv_ratio
self.dv_ttm = dv_ttm
self.total_share = total_share
self.float_share = float_share
self.free_share = free_share
self.total_mv = total_mv
self.circ_mv = circ_mv
self.adj_factor = adj_factor
@classmethod
def from_dict(cls, d: dict) -> "DailyBasic":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
close=_f(d.get("close")),
turnover_rate=_f(d.get("turnover_rate")),
turnover_rate_f=_f(d.get("turnover_rate_f")),
volume_ratio=_f(d.get("volume_ratio")),
pe=_f(d.get("pe")),
pe_ttm=_f(d.get("pe_ttm")),
pb=_f(d.get("pb")),
ps=_f(d.get("ps")),
ps_ttm=_f(d.get("ps_ttm")),
dv_ratio=_f(d.get("dv_ratio")),
dv_ttm=_f(d.get("dv_ttm")),
total_share=_f(d.get("total_share")),
float_share=_f(d.get("float_share")),
free_share=_f(d.get("free_share")),
total_mv=_f(d.get("total_mv")),
circ_mv=_f(d.get("circ_mv")),
adj_factor=_f(d.get("adj_factor")),
)
def __repr__(self) -> str:
return (f"DailyBasic(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pe={self.pe}, pb={self.pb})")
class Income:
"""
利润表数据,对应本地 income 表。
字段说明:
ts_code 股票代码(PK)
end_date 报告期结束日期(PK),格式 YYYY-MM-DD
ann_date 公告日期,格式 YYYY-MM-DD
report_type 报告类型(PK,1=合并报表)
comp_type 公司类型
basic_eps 基本每股收益
diluted_eps 稀释每股收益
total_revenue 营业总收入
revenue 营业收入
total_cogs 营业总成本
oper_cost 营业成本
sell_exp 销售费用
admin_exp 管理费用
fin_exp 财务费用
total_profit 利润总额
income_tax 所得税费用
n_income 净利润
n_income_attr_p 归属于母公司所有者的净利润
minority_gain 少数股东损益
oth_compr_income 其他综合收益
t_compr_income 综合收益总额
compr_inc_attr_p 归属于母公司所有者的综合收益总额
ebit 息税前利润
ebitda 息税折旧摊销前利润
roe 净资产收益率(%)
roa 总资产收益率(%)
gross_margin 毛利率(%)
net_profit_margin 净利率(%)
net_profit_yoy 净利润增长率(%)
revenue_yoy 营业收入增长率(%)
equity_yoy 净资产增长率(%)
pcf 市现率
free_circ_mv 自由流通市值
"""
__slots__ = (
"ts_code", "end_date", "ann_date", "report_type", "comp_type",
"basic_eps", "diluted_eps", "total_revenue", "revenue",
"total_cogs", "oper_cost", "sell_exp", "admin_exp", "fin_exp",
"total_profit", "income_tax", "n_income", "n_income_attr_p",
"minority_gain", "oth_compr_income", "t_compr_income", "compr_inc_attr_p",
"ebit", "ebitda", "roe", "roa", "gross_margin", "net_profit_margin",
"net_profit_yoy", "revenue_yoy", "equity_yoy", "pcf", "free_circ_mv",
)
def __init__(
self,
ts_code: str, end_date: str, report_type: str, ann_date: str, comp_type: str,
basic_eps: float, diluted_eps: float, total_revenue: float, revenue: float,
total_cogs: float, oper_cost: float, sell_exp: float, admin_exp: float, fin_exp: float,
total_profit: float, income_tax: float, n_income: float, n_income_attr_p: float,
minority_gain: float, oth_compr_income: float, t_compr_income: float, compr_inc_attr_p: float,
ebit: float, ebitda: float, roe: float, roa: float, gross_margin: float,
net_profit_margin: float, net_profit_yoy: float, revenue_yoy: float,
equity_yoy: float, pcf: float, free_circ_mv: float,
):
self.ts_code = ts_code
self.end_date = end_date
self.report_type = report_type
self.ann_date = ann_date
self.comp_type = comp_type
self.basic_eps = basic_eps
self.diluted_eps = diluted_eps
self.total_revenue = total_revenue
self.revenue = revenue
self.total_cogs = total_cogs
self.oper_cost = oper_cost
self.sell_exp = sell_exp
self.admin_exp = admin_exp
self.fin_exp = fin_exp
self.total_profit = total_profit
self.income_tax = income_tax
self.n_income = n_income
self.n_income_attr_p = n_income_attr_p
self.minority_gain = minority_gain
self.oth_compr_income = oth_compr_income
self.t_compr_income = t_compr_income
self.compr_inc_attr_p = compr_inc_attr_p
self.ebit = ebit
self.ebitda = ebitda
self.roe = roe
self.roa = roa
self.gross_margin = gross_margin
self.net_profit_margin = net_profit_margin
self.net_profit_yoy = net_profit_yoy
self.revenue_yoy = revenue_yoy
self.equity_yoy = equity_yoy
self.pcf = pcf
self.free_circ_mv = free_circ_mv
@classmethod
def from_dict(cls, d: dict) -> "Income":
"""从字典(数据库行)构造 Income 对象。"""
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
ts_code=d.get("ts_code") or "",
end_date=d.get("end_date") or "",
report_type=d.get("report_type") or "",
ann_date=d.get("ann_date") or "",
comp_type=d.get("comp_type") or "",
basic_eps=_f(d.get("basic_eps")),
diluted_eps=_f(d.get("diluted_eps")),
total_revenue=_f(d.get("total_revenue")),
revenue=_f(d.get("revenue")),
total_cogs=_f(d.get("total_cogs")),
oper_cost=_f(d.get("oper_cost")),
sell_exp=_f(d.get("sell_exp")),
admin_exp=_f(d.get("admin_exp")),
fin_exp=_f(d.get("fin_exp")),
total_profit=_f(d.get("total_profit")),
income_tax=_f(d.get("income_tax")),
n_income=_f(d.get("n_income")),
n_income_attr_p=_f(d.get("n_income_attr_p")),
minority_gain=_f(d.get("minority_gain")),
oth_compr_income=_f(d.get("oth_compr_income")),
t_compr_income=_f(d.get("t_compr_income")),
compr_inc_attr_p=_f(d.get("compr_inc_attr_p")),
ebit=_f(d.get("ebit")),
ebitda=_f(d.get("ebitda")),
roe=_f(d.get("roe")),
roa=_f(d.get("roa")),
gross_margin=_f(d.get("gross_margin")),
net_profit_margin=_f(d.get("net_profit_margin")),
net_profit_yoy=_f(d.get("net_profit_yoy")),
revenue_yoy=_f(d.get("revenue_yoy")),
equity_yoy=_f(d.get("equity_yoy")),
pcf=_f(d.get("pcf")),
free_circ_mv=_f(d.get("free_circ_mv")),
)
def __repr__(self) -> str:
return (f"Income(ts_code={self.ts_code!r}, end_date={self.end_date!r}, "
f"report_type={self.report_type!r}, n_income={self.n_income})")
class StockLimit:
"""
每日涨跌停价格数据,对应本地 stock_limit 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
pre_close 昨日收盘价
up_limit 涨停价
down_limit 跌停价
"""
__slots__ = ("trade_date", "ts_code", "pre_close", "up_limit", "down_limit")
def __init__(self, trade_date: str, ts_code: str,
pre_close: float, up_limit: float, down_limit: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.pre_close = pre_close
self.up_limit = up_limit
self.down_limit = down_limit
@classmethod
def from_dict(cls, d: dict) -> "StockLimit":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
pre_close=_f(d.get("pre_close")),
up_limit=_f(d.get("up_limit")),
down_limit=_f(d.get("down_limit")),
)
def __repr__(self) -> str:
return (f"StockLimit(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"up_limit={self.up_limit}, down_limit={self.down_limit})")
class DailyLimitList:
"""
每日涨跌停榜单数据,对应本地 daily_limit_list 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
name 股票名称
limit_type 榜单类型(U=涨停, D=跌停)
limit_price 涨跌停价格
pct_chg 涨跌幅(%)
volume 成交量
amount 成交额
limit_streak 连板数(涨停板)
sector 所属板块
"""
__slots__ = ("trade_date", "ts_code", "name", "limit_type", "limit_price",
"pct_chg", "volume", "amount", "limit_streak", "sector")
def __init__(self, trade_date: str, ts_code: str, name: str, limit_type: str,
limit_price: float, pct_chg: float, volume: float, amount: float,
limit_streak: int, sector: str):
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.limit_type = limit_type
self.limit_price = limit_price
self.pct_chg = pct_chg
self.volume = volume
self.amount = amount
self.limit_streak = limit_streak
self.sector = sector
@classmethod
def from_dict(cls, d: dict) -> "DailyLimitList":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
limit_type=d.get("limit_type") or "",
limit_price=_f(d.get("limit_price")),
pct_chg=_f(d.get("pct_chg")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
limit_streak=_i(d.get("limit_streak")),
sector=d.get("sector") or "",
)
def __repr__(self) -> str:
return (f"DailyLimitList(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"name={self.name!r}, limit_type={self.limit_type!r}, limit_streak={self.limit_streak})")
class SectorStockMap:
"""
板块成分股映射表,对应本地 sector_stock_map 表。
字段说明:
sector_code 板块代码(PK)
stock_code 股票代码(PK)
sector_name 板块名称(冗余字段)
source 数据来源
"""
__slots__ = ("sector_code", "stock_code", "sector_name", "source")
def __init__(self, sector_code: str, stock_code: str, sector_name: str, source: str):
self.sector_code = sector_code
self.stock_code = stock_code
self.sector_name = sector_name
self.source = source
@classmethod
def from_dict(cls, d: dict) -> "SectorStockMap":
return cls(
sector_code=d.get("sector_code") or "",
stock_code=d.get("stock_code") or "",
sector_name=d.get("sector_name") or "",
source=d.get("source") or "",
)
def __repr__(self) -> str:
return (f"SectorStockMap(sector_code={self.sector_code!r}, "
f"stock_code={self.stock_code!r}, sector_name={self.sector_name!r})")
class TopList:
"""
龙虎榜每日明细数据,对应本地 top_list 表。
字段说明:
id 自增ID(PK)
trade_date 交易日期,格式 YYYY-MM-DD
ts_code 股票代码
name 股票名称
close 收盘价
pct_change 涨跌幅(%)
turnover_rate 换手率(%)
amount 总成交额
l_sell 龙虎榜卖出额
l_buy 龙虎榜买入额
l_amount 龙虎榜成交额
net_amount 龙虎榜净买入额
net_rate 龙虎榜净买额占比(%)
amount_rate 龙虎榜成交额占比(%)
float_values 当日流通市值
reason 上榜理由
"""
__slots__ = ("id", "trade_date", "ts_code", "name", "close", "pct_change",
"turnover_rate", "amount", "l_sell", "l_buy", "l_amount",
"net_amount", "net_rate", "amount_rate", "float_values", "reason")
def __init__(self, id: int, trade_date: str, ts_code: str, name: str,
close: float, pct_change: float, turnover_rate: float, amount: float,
l_sell: float, l_buy: float, l_amount: float, net_amount: float,
net_rate: float, amount_rate: float, float_values: float, reason: str):
self.id = id
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.close = close
self.pct_change = pct_change
self.turnover_rate = turnover_rate
self.amount = amount
self.l_sell = l_sell
self.l_buy = l_buy
self.l_amount = l_amount
self.net_amount = net_amount
self.net_rate = net_rate
self.amount_rate = amount_rate
self.float_values = float_values
self.reason = reason
@classmethod
def from_dict(cls, d: dict) -> "TopList":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
id=_i(d.get("id")),
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
close=_f(d.get("close")),
pct_change=_f(d.get("pct_change")),
turnover_rate=_f(d.get("turnover_rate")),
amount=_f(d.get("amount")),
l_sell=_f(d.get("l_sell")),
l_buy=_f(d.get("l_buy")),
l_amount=_f(d.get("l_amount")),
net_amount=_f(d.get("net_amount")),
net_rate=_f(d.get("net_rate")),
amount_rate=_f(d.get("amount_rate")),
float_values=_f(d.get("float_values")),
reason=d.get("reason") or "",
)
def __repr__(self) -> str:
return (f"TopList(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"name={self.name!r}, pct_change={self.pct_change})")
class TopInst:
"""
龙虎榜机构交易明细数据,对应本地 top_inst 表。
字段说明:
id 自增ID(PK)
trade_date 交易日期,格式 YYYY-MM-DD
ts_code 股票代码
exalter 营业部名称/机构名称
side 买卖类型(0:买入最大的前5名, 1:卖出最大的前5名)
buy 买入额(元)
buy_rate 买入占总成交比例(%)
sell 卖出额(元)
sell_rate 卖出占总成交比例(%)
net_buy 净成交额(元)
reason 上榜理由
"""
__slots__ = ("id", "trade_date", "ts_code", "exalter", "side",
"buy", "buy_rate", "sell", "sell_rate", "net_buy", "reason")
def __init__(self, id: int, trade_date: str, ts_code: str, exalter: str,
side: str, buy: float, buy_rate: float, sell: float,
sell_rate: float, net_buy: float, reason: str):
self.id = id
self.trade_date = trade_date
self.ts_code = ts_code
self.exalter = exalter
self.side = side
self.buy = buy
self.buy_rate = buy_rate
self.sell = sell
self.sell_rate = sell_rate
self.net_buy = net_buy
self.reason = reason
@classmethod
def from_dict(cls, d: dict) -> "TopInst":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
id=_i(d.get("id")),
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
exalter=d.get("exalter") or "",
side=d.get("side") or "",
buy=_f(d.get("buy")),
buy_rate=_f(d.get("buy_rate")),
sell=_f(d.get("sell")),
sell_rate=_f(d.get("sell_rate")),
net_buy=_f(d.get("net_buy")),
reason=d.get("reason") or "",
)
def __repr__(self) -> str:
return (f"TopInst(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"exalter={self.exalter!r}, side={self.side!r}, net_buy={self.net_buy})")
class SectorFlowDaily:
"""
板块资金流向数据,对应本地 sector_flow_daily 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
content_type 板块类型(行业/概念/地域)
ts_code 板块代码(PK)
name 板块名称
pct_change 涨跌幅(%)
close 收盘价
net_amount 净流入金额(元)
net_amount_rate 净流入占比(%)
buy_elg_amount 超大单净流入(元)
buy_elg_amount_rate 超大单净流入占比(%)
buy_lg_amount 大单净流入(元)
buy_lg_amount_rate 大单净流入占比(%)
buy_md_amount 中单净流入(元)
buy_md_amount_rate 中单净流入占比(%)
buy_sm_amount 小单净流入(元)
buy_sm_amount_rate 小单净流入占比(%)
buy_sm_amount_stock 小单净流入最大股
rank 排名
"""
__slots__ = ("trade_date", "ts_code", "name", "content_type",
"pct_change", "close", "net_amount", "net_amount_rate",
"buy_elg_amount", "buy_elg_amount_rate",
"buy_lg_amount", "buy_lg_amount_rate",
"buy_md_amount", "buy_md_amount_rate",
"buy_sm_amount", "buy_sm_amount_rate",
"buy_sm_amount_stock", "rank")
def __init__(self, trade_date: str, ts_code: str, name: str, content_type: str,
pct_change: float, close: float,
net_amount: float, net_amount_rate: float,
buy_elg_amount: float, buy_elg_amount_rate: float,
buy_lg_amount: float, buy_lg_amount_rate: float,
buy_md_amount: float, buy_md_amount_rate: float,
buy_sm_amount: float, buy_sm_amount_rate: float,
buy_sm_amount_stock: str, rank: int):
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.content_type = content_type
self.pct_change = pct_change
self.close = close
self.net_amount = net_amount
self.net_amount_rate = net_amount_rate
self.buy_elg_amount = buy_elg_amount
self.buy_elg_amount_rate = buy_elg_amount_rate
self.buy_lg_amount = buy_lg_amount
self.buy_lg_amount_rate = buy_lg_amount_rate
self.buy_md_amount = buy_md_amount
self.buy_md_amount_rate = buy_md_amount_rate
self.buy_sm_amount = buy_sm_amount
self.buy_sm_amount_rate = buy_sm_amount_rate
self.buy_sm_amount_stock = buy_sm_amount_stock
self.rank = rank
@classmethod
def from_dict(cls, d: dict) -> "SectorFlowDaily":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
def _i(v):
try:
return int(v) if v is not None and v != "" else 0
except (TypeError, ValueError):
return 0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
content_type=d.get("content_type") or "",
pct_change=_f(d.get("pct_change")),
close=_f(d.get("close")),
net_amount=_f(d.get("net_amount")),
net_amount_rate=_f(d.get("net_amount_rate")),
buy_elg_amount=_f(d.get("buy_elg_amount")),
buy_elg_amount_rate=_f(d.get("buy_elg_amount_rate")),
buy_lg_amount=_f(d.get("buy_lg_amount")),
buy_lg_amount_rate=_f(d.get("buy_lg_amount_rate")),
buy_md_amount=_f(d.get("buy_md_amount")),
buy_md_amount_rate=_f(d.get("buy_md_amount_rate")),
buy_sm_amount=_f(d.get("buy_sm_amount")),
buy_sm_amount_rate=_f(d.get("buy_sm_amount_rate")),
buy_sm_amount_stock=d.get("buy_sm_amount_stock") or "",
rank=_i(d.get("rank")),
)
def __repr__(self) -> str:
return (f"SectorFlowDaily(trade_date={self.trade_date!r}, "
f"ts_code={self.ts_code!r}, name={self.name!r}, "
f"net_amount={self.net_amount})")
class IndexBasic:
"""
指数基础信息,对应本地 index_basic 表。
字段说明:
ts_code 指数代码(PK)
name 指数名称
fullname 指数全称
market 市场
publisher 发布方
index_type 指数类型
category 分类
base_date 基准日期,格式 YYYY-MM-DD
base_point 基点
list_date 发布日期,格式 YYYY-MM-DD
weight_rule 加权方式
desc 描述
exp_date 终止日期,格式 YYYY-MM-DD
"""
__slots__ = ("ts_code", "name", "fullname", "market", "publisher",
"index_type", "category", "base_date", "base_point",
"list_date", "weight_rule", "desc", "exp_date")
def __init__(self, ts_code: str, name: str, fullname: str, market: str,
publisher: str, index_type: str, category: str, base_date: str,
base_point: float, list_date: str, weight_rule: str,
desc: str, exp_date: str):
self.ts_code = ts_code
self.name = name
self.fullname = fullname
self.market = market
self.publisher = publisher
self.index_type = index_type
self.category = category
self.base_date = base_date
self.base_point = base_point
self.list_date = list_date
self.weight_rule = weight_rule
self.desc = desc
self.exp_date = exp_date
@classmethod
def from_dict(cls, d: dict) -> "IndexBasic":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
fullname=d.get("fullname") or "",
market=d.get("market") or "",
publisher=d.get("publisher") or "",
index_type=d.get("index_type") or "",
category=d.get("category") or "",
base_date=d.get("base_date") or "",
base_point=_f(d.get("base_point")),
list_date=d.get("list_date") or "",
weight_rule=d.get("weight_rule") or "",
desc=d.get("desc") or "",
exp_date=d.get("exp_date") or "",
)
def __repr__(self) -> str:
return (f"IndexBasic(ts_code={self.ts_code!r}, name={self.name!r}, "
f"market={self.market!r})")
class IndexDaily:
"""
指数日线行情数据,对应本地 index_daily 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 指数代码(PK)
close 收盘指数
open 开盘指数
high 最高指数
low 最低指数
pre_close 前收盘指数
change 涨跌点数
pct_chg 涨跌幅(%)
vol 成交量
amount 成交额
"""
__slots__ = ("trade_date", "ts_code", "open", "high", "low", "close",
"pre_close", "change", "pct_chg", "vol", "amount")
def __init__(self, trade_date: str, ts_code: str, open: float, high: float,
low: float, close: float, pre_close: float, change: float,
pct_chg: float, vol: float, amount: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.open = open
self.high = high
self.low = low
self.close = close
self.pre_close = pre_close
self.change = change
self.pct_chg = pct_chg
self.vol = vol
self.amount = amount
@classmethod
def from_dict(cls, d: dict) -> "IndexDaily":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
pct_chg=_f(d.get("pct_chg")),
vol=_f(d.get("vol")),
amount=_f(d.get("amount")),
)
def __repr__(self) -> str:
return (f"IndexDaily(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pct_chg={self.pct_chg})")
class IndexWeekly:
"""
指数周线行情数据,对应本地 index_weekly 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 指数代码(PK)
close 收盘指数
open 开盘指数
high 最高指数
low 最低指数
pre_close 前收盘指数
change 涨跌点数
pct_chg 涨跌幅(%)
vol 成交量
amount 成交额
"""
__slots__ = ("trade_date", "ts_code", "open", "high", "low", "close",
"pre_close", "change", "pct_chg", "vol", "amount")
def __init__(self, trade_date: str, ts_code: str, open: float, high: float,
low: float, close: float, pre_close: float, change: float,
pct_chg: float, vol: float, amount: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.open = open
self.high = high
self.low = low
self.close = close
self.pre_close = pre_close
self.change = change
self.pct_chg = pct_chg
self.vol = vol
self.amount = amount
@classmethod
def from_dict(cls, d: dict) -> "IndexWeekly":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
pct_chg=_f(d.get("pct_chg")),
vol=_f(d.get("vol")),
amount=_f(d.get("amount")),
)
def __repr__(self) -> str:
return (f"IndexWeekly(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pct_chg={self.pct_chg})")
class IndexMonthly:
"""
指数月线行情数据,对应本地 index_monthly 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 指数代码(PK)
close 收盘指数
open 开盘指数
high 最高指数
low 最低指数
pre_close 前收盘指数
change 涨跌点数
pct_chg 涨跌幅(%)
vol 成交量
amount 成交额
"""
__slots__ = ("trade_date", "ts_code", "open", "high", "low", "close",
"pre_close", "change", "pct_chg", "vol", "amount")
def __init__(self, trade_date: str, ts_code: str, open: float, high: float,
low: float, close: float, pre_close: float, change: float,
pct_chg: float, vol: float, amount: float):
self.trade_date = trade_date
self.ts_code = ts_code
self.open = open
self.high = high
self.low = low
self.close = close
self.pre_close = pre_close
self.change = change
self.pct_chg = pct_chg
self.vol = vol
self.amount = amount
@classmethod
def from_dict(cls, d: dict) -> "IndexMonthly":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
open=_f(d.get("open")),
high=_f(d.get("high")),
low=_f(d.get("low")),
close=_f(d.get("close")),
pre_close=_f(d.get("pre_close")),
change=_f(d.get("change")),
pct_chg=_f(d.get("pct_chg")),
vol=_f(d.get("vol")),
amount=_f(d.get("amount")),
)
def __repr__(self) -> str:
return (f"IndexMonthly(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"close={self.close}, pct_chg={self.pct_chg})")
class DailyBombList:
"""
每日炸板榜单数据,对应本地 daily_bomb_list 表。
字段说明:
trade_date 交易日期(PK),格式 YYYY-MM-DD
ts_code 股票代码(PK)
name 股票名称
bomb_type 炸板类型(U=曾涨停, D=曾跌停/撬板)
limit_price 触及的涨跌停价格
pct_chg 涨跌幅(%)
volume 成交量
amount 成交额
sector 所属板块
"""
__slots__ = ("trade_date", "ts_code", "name", "bomb_type", "limit_price",
"pct_chg", "volume", "amount", "sector")
def __init__(self, trade_date: str, ts_code: str, name: str, bomb_type: str,
limit_price: float, pct_chg: float, volume: float, amount: float,
sector: str):
self.trade_date = trade_date
self.ts_code = ts_code
self.name = name
self.bomb_type = bomb_type
self.limit_price = limit_price
self.pct_chg = pct_chg
self.volume = volume
self.amount = amount
self.sector = sector
@classmethod
def from_dict(cls, d: dict) -> "DailyBombList":
def _f(v):
try:
return float(v) if v is not None and v != "" else 0.0
except (TypeError, ValueError):
return 0.0
return cls(
trade_date=d.get("trade_date") or "",
ts_code=d.get("ts_code") or "",
name=d.get("name") or "",
bomb_type=d.get("bomb_type") or "",
limit_price=_f(d.get("limit_price")),
pct_chg=_f(d.get("pct_chg")),
volume=_f(d.get("volume")),
amount=_f(d.get("amount")),
sector=d.get("sector") or "",
)
def __repr__(self) -> str:
return (f"DailyBombList(trade_date={self.trade_date!r}, ts_code={self.ts_code!r}, "
f"name={self.name!r}, bomb_type={self.bomb_type!r})")
class AppVersion:
"""
应用版本信息。
字段说明:
version 版本号(如 1.0、1.1)
release_date 发布日期,格式 YYYY-MM-DD
file_name 安装包文件名
download_url 安装包下载地址
"""
__slots__ = ("version", "release_date", "file_name", "download_url")
def __init__(self, version: str, release_date: str, file_name: str, download_url: str):
self.version = version
self.release_date = release_date
self.file_name = file_name
self.download_url = download_url
@classmethod
def from_dict(cls, d: dict) -> "AppVersion":
return cls(
version=d["version"],
release_date=d["release_date"],
file_name=d["file_name"],
download_url=d["download_url"],
)
def __repr__(self) -> str:
return (f"AppVersion(version={self.version!r}, release_date={self.release_date!r}, "
f"file_name={self.file_name!r}, download_url={self.download_url!r})")
class TokenCheckResult:
"""
Token 校验结果。
字段说明:
status 校验状态(success=通过, failure=失败)
message 状态描述信息
"""
__slots__ = ("status", "message")
def __init__(self, status: str, message: str):
self.status = status
self.message = message
@classmethod
def from_dict(cls, d: dict) -> "TokenCheckResult":
return cls(
status=d.get("status") or "",
message=d.get("message") or "",
)
def is_success(self) -> bool:
return self.status == "success"
def __repr__(self) -> str:
return f"TokenCheckResult(status={self.status!r}, message={self.message!r})"
FILE:scripts/factor_mining.py
"""
factor_mining.py — 因子挖矿实现模块
包含 random_alpha_backtest 的完整实现逻辑。
stock_api.StockApi.random_alpha_backtest() 是对外接口,内部委托此模块执行。
外部勿直接调用本模块,请统一通过 StockApi.random_alpha_backtest() 调用。
"""
import random
import statistics as _stat
from datetime import datetime, timedelta
from typing import Dict, List, Optional, TYPE_CHECKING
import numpy as np
import pandas as pd
from formulaicAlphas import AlphaDataLoader, Alpha101, ALPHA_DESCRIPTIONS
if TYPE_CHECKING:
from stock_api import StockApi
def run_random_alpha_backtest(
api: "StockApi",
codes: Optional[List[str]] = None,
max_screen_factors: int = 5,
max_signal_factors: int = 7,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
initial_cash: float = 1_000_000.0,
warmup_days: int = 90,
random_seed: Optional[int] = None,
top_n_stocks: int = 5,
max_pool_size: int = 30,
max_holdings: int = 5,
) -> Dict:
"""
因子挖矿核心逻辑(两阶段:选股 + 交易信号)。
流程:
1. 随机抽取 k_screen 个选股因子 + k_signal 个信号因子
2. 选股阶段:以 start_date 为截面日,每个选股因子随机保留 5%~20% 的股票
3. 若过滤后股票数仍超过 max_pool_size,按各选股因子综合得分再截取前 max_pool_size 只
4. 信号阶段:逐日计算信号因子横截面分位排名,综合排名 >= buy_thresh 时买入,
<= sell_thresh 时卖出(阈值在合理范围内随机生成)
5. 每日最多同时持仓 max_holdings 只,优先买入综合排名最高的股票
6. 输出 Top N 个股的每笔交易时的具体因子值与排名
Args:
api: StockApi 实例(用于调用 get_all_symbols、load_alpha_data 等方法)
codes: 股票池;None 时取全市场
max_screen_factors: 选股因子最大数量(默认 5)
max_signal_factors: 信号因子最大数量(默认 7)
start_date: 回测起始日,None 取 end_date 前 90 天
end_date: 回测截止日,None 取今日
initial_cash: 初始资金(默认 100 万)
warmup_days: 因子预热天数(默认 90)
random_seed: 随机种子,None 不固定
top_n_stocks: 输出详细交易记录的个股数量(默认 5)
max_pool_size: 最终候选池上限,超过时按综合得分截取(默认 30)
max_holdings: 最大同时持仓数,优先持有综合排名最高的股票(默认 5)
"""
# ── 1. 日期默认值 ──────────────────────────────────────────────────────
if end_date is None:
end_date = datetime.today().strftime('%Y-%m-%d')
if start_date is None:
start_date = (
datetime.strptime(end_date, '%Y-%m-%d') - timedelta(days=90)
).strftime('%Y-%m-%d')
# ── 2. 股票池 ──────────────────────────────────────────────────────────
if codes is None:
codes = api.get_all_symbols()
if not codes:
return {'error': '股票池为空'}
# ── 3. 随机抽取因子(选股 / 信号各自独立不重复,两组间允许重叠)────────
rng = random.Random(random_seed)
k_screen = rng.randint(1, max(1, max_screen_factors))
k_signal = rng.randint(1, max(1, max_signal_factors))
screen_nums = rng.sample(range(1, 102), k_screen)
signal_nums = rng.sample(range(1, 102), k_signal)
screen_names = [f'alpha{n:03d}' for n in screen_nums]
signal_names = [f'alpha{n:03d}' for n in signal_nums]
all_nums = list(dict.fromkeys(screen_nums + signal_nums))
all_names = list(dict.fromkeys(screen_names + signal_names))
all_descs = {name: ALPHA_DESCRIPTIONS.get(name, '') for name in all_names}
# 选股因子:每个因子随机保留比例 [0.05, 0.20]
screen_top_pcts = {name: round(rng.uniform(0.05, 0.20), 2) for name in screen_names}
# 信号因子阈值
signal_buy_thresh = round(rng.uniform(0.55, 0.82), 2)
signal_sell_thresh = round(rng.uniform(0.30, 0.55), 2)
# ── 4. 加载面板数据(含预热段)────────────────────────────────────────
warmup_start = (
datetime.strptime(start_date, '%Y-%m-%d') - timedelta(days=warmup_days)
).strftime('%Y-%m-%d')
panel = api.load_alpha_data(codes, warmup_start, end_date)
if not panel:
return {'error': '无法加载面板数据',
'screen_factors': screen_names, 'signal_factors': signal_names}
close_panel = panel['close']
start_ts = pd.Timestamp(start_date)
end_ts = pd.Timestamp(end_date)
valid_idx = close_panel.index[close_panel.index <= start_ts]
ref_date = valid_idx[-1] if len(valid_idx) > 0 else close_panel.index[0]
# ── 5. 计算所有因子 ────────────────────────────────────────────────────
alpha_obj = Alpha101(panel)
factor_panels: Dict[str, object] = {}
for num in all_nums:
name = f'alpha{num:03d}'
method = getattr(alpha_obj, name, None)
if method is None:
continue
try:
factor_panels[name] = method()
except Exception:
pass
# ── 6. 选股阶段:逐因子顺序过滤 ───────────────────────────────────────
current_pool = set(close_panel.columns.tolist())
filter_log: List[Dict] = []
for name in screen_names:
before = len(current_pool)
top_pct = screen_top_pcts[name]
if not current_pool or name not in factor_panels:
filter_log.append({'factor': name, 'status': 'skipped',
'before': before, 'after': before, 'top_pct': top_pct})
continue
fp = factor_panels[name]
if ref_date not in fp.index:
filter_log.append({'factor': name, 'status': 'no_ref_date',
'before': before, 'after': before, 'top_pct': top_pct})
continue
pool_cols = [c for c in current_pool if c in fp.columns]
snapshot = fp.loc[ref_date, pool_cols].dropna()
if snapshot.empty:
filter_log.append({'factor': name, 'status': 'all_nan',
'before': before, 'after': before, 'top_pct': top_pct})
continue
n_keep = max(1, int(len(snapshot) * top_pct))
top_codes = set(snapshot.nlargest(n_keep).index)
current_pool &= top_codes
filter_log.append({'factor': name, 'status': 'ok',
'before': before, 'after': len(current_pool),
'ref_date': str(ref_date.date()),
'snapshot_size': len(snapshot),
'kept': n_keep, 'top_pct': top_pct})
final_codes = sorted(current_pool)
if not final_codes:
return {'screen_k': k_screen, 'signal_k': k_signal,
'screen_factors': screen_names, 'signal_factors': signal_names,
'factor_descriptions': all_descs,
'initial_pool': len(codes), 'filter_log': filter_log,
'final_pool': [], 'final_pool_count': 0,
'error': '过滤后股票池为空,无法回测'}
# ── 二次裁剪:候选池超过 max_pool_size 时按综合得分取 Top N ──────────
if max_pool_size > 0 and len(final_codes) > max_pool_size:
score_map: Dict[str, float] = {c: 0.0 for c in final_codes}
for name in screen_names:
if name not in factor_panels:
continue
fp = factor_panels[name]
if ref_date not in fp.index:
continue
pool_cols = [c for c in final_codes if c in fp.columns]
snap = fp.loc[ref_date, pool_cols].dropna()
if snap.empty:
continue
ranked = snap.rank(pct=True)
for c, v in ranked.items():
score_map[c] = score_map.get(c, 0.0) + float(v)
sorted_by_score = sorted(final_codes, key=lambda c: score_map.get(c, 0.0), reverse=True)
final_codes = sorted(sorted_by_score[:max_pool_size])
# ── 7. 信号阶段:逐日模拟买卖 ─────────────────────────────────────────
bt_mask = (close_panel.index >= start_ts) & (close_panel.index <= end_ts)
avail_codes = [c for c in final_codes if c in close_panel.columns]
bt_close = close_panel.loc[bt_mask, avail_codes]
if bt_close.empty or len(bt_close) < 2:
return {'screen_k': k_screen, 'signal_k': k_signal,
'screen_factors': screen_names, 'signal_factors': signal_names,
'factor_descriptions': all_descs,
'initial_pool': len(codes), 'filter_log': filter_log,
'final_pool': final_codes, 'final_pool_count': len(final_codes),
'error': '回测期内收盘数据不足(< 2 个交易日)'}
trading_dates = bt_close.index.tolist()
# 预提取信号因子回测期面板(在筛选后的股票池内计算横截面排名)
sig_panels: Dict[str, object] = {}
for name in signal_names:
if name in factor_panels:
fp = factor_panels[name]
sig_cols = [c for c in avail_codes if c in fp.columns]
if sig_cols:
sig_panels[name] = fp.loc[bt_mask, sig_cols]
fee_rate = 0.001
n_slots = max(1, min(max_holdings, len(avail_codes)))
per_stock_budget = initial_cash / n_slots
cash = initial_cash
positions: Dict[str, Dict] = {}
equity_curve: List[float] = [initial_cash]
trade_log: List[Dict] = []
for date in trading_dates:
date_str = str(date.date())
# 计算各信号因子当日横截面分位排名
factor_day_ranks: Dict[str, Dict] = {}
composite_ranks: Dict[str, float] = {}
valid_sig = [n for n in signal_names if n in sig_panels]
for code in avail_codes:
per_factor = {}
rank_vals = []
for sname in valid_sig:
sp = sig_panels[sname]
if code not in sp.columns or date not in sp.index:
continue
day_series = sp.loc[date].dropna()
if day_series.empty or code not in day_series.index:
continue
raw = float(day_series[code])
rank = float(day_series.rank(pct=True)[code])
per_factor[sname] = {'value': round(raw, 6), 'rank': round(rank, 4)}
rank_vals.append(rank)
factor_day_ranks[code] = per_factor
composite_ranks[code] = round(float(np.mean(rank_vals)), 4) if rank_vals else 0.5
# ── 先执行卖出信号 ──────────────────────────────────────────────────
for code in list(positions.keys()):
comp = composite_ranks.get(code, 0.5)
if code not in bt_close.columns:
continue
price_raw = bt_close.loc[date, code]
if pd.isna(price_raw) or float(price_raw) <= 0:
continue
price = float(price_raw)
if comp <= signal_sell_thresh:
pos = positions[code]
proceeds = pos['shares'] * price * (1 - fee_rate)
pnl = proceeds - pos['entry_value']
pnl_pct = (proceeds / pos['entry_value'] - 1) * 100
hold_days = (date - pd.Timestamp(pos['entry_date'])).days
cash += proceeds
trade_log.append({
'date': date_str, 'code': code, 'action': 'SELL',
'price': round(price, 3), 'shares': pos['shares'],
'amount': round(proceeds, 2),
'composite_rank': comp,
'factor_values': factor_day_ranks.get(code, {}),
'signal_buy_thresh': signal_buy_thresh,
'signal_sell_thresh': signal_sell_thresh,
'hold_days': hold_days,
'pnl': round(pnl, 2),
'pnl_pct': round(pnl_pct, 4),
})
del positions[code]
# ── 再执行买入信号:候选排名最高、持仓数未满 max_holdings ────────────
slots_free = max_holdings - len(positions)
if slots_free > 0:
buy_candidates = []
for code in avail_codes:
if code in positions:
continue
comp = composite_ranks.get(code, 0.5)
if comp < signal_buy_thresh:
continue
if code not in bt_close.columns:
continue
price_raw = bt_close.loc[date, code]
if pd.isna(price_raw) or float(price_raw) <= 0:
continue
buy_candidates.append((comp, code, float(price_raw)))
buy_candidates.sort(key=lambda x: x[0], reverse=True)
for comp, code, price in buy_candidates[:slots_free]:
budget = min(per_stock_budget, cash * 0.99)
if budget < price * 100:
continue
shares = int(budget / price / 100) * 100
if shares <= 0:
continue
cost = shares * price * (1 + fee_rate)
if cost > cash:
continue
cash -= cost
positions[code] = {'shares': shares, 'entry_price': price,
'entry_date': date_str, 'entry_value': cost}
trade_log.append({
'date': date_str, 'code': code, 'action': 'BUY',
'price': round(price, 3), 'shares': shares, 'amount': round(cost, 2),
'composite_rank': comp,
'factor_values': factor_day_ranks.get(code, {}),
'signal_buy_thresh': signal_buy_thresh,
'signal_sell_thresh': signal_sell_thresh,
})
# 当日组合净值
pos_val = sum(
positions[c]['shares'] * float(bt_close.loc[date, c])
for c in positions
if c in bt_close.columns and not pd.isna(bt_close.loc[date, c])
)
equity_curve.append(cash + pos_val)
# 末日强制清仓
last_date = trading_dates[-1]
last_date_str = str(last_date.date())
for code in list(positions.keys()):
pos = positions[code]
if code in bt_close.columns and not pd.isna(bt_close.loc[last_date, code]):
price = float(bt_close.loc[last_date, code])
proceeds = pos['shares'] * price * (1 - fee_rate)
pnl = proceeds - pos['entry_value']
pnl_pct = (proceeds / pos['entry_value'] - 1) * 100
hold_days = (last_date - pd.Timestamp(pos['entry_date'])).days
cash += proceeds
trade_log.append({
'date': last_date_str, 'code': code, 'action': 'SELL(强平)',
'price': round(price, 3), 'shares': pos['shares'],
'amount': round(proceeds, 2),
'composite_rank': None, 'factor_values': {},
'signal_buy_thresh': signal_buy_thresh,
'signal_sell_thresh': signal_sell_thresh,
'hold_days': hold_days,
'pnl': round(pnl, 2),
'pnl_pct': round(pnl_pct, 4),
})
equity_curve[-1] = cash
# ── 8. 计算业绩指标 ────────────────────────────────────────────────────
total_ret_dec = (equity_curve[-1] / initial_cash) - 1.0
trading_days = len(trading_dates)
bt_result = {
'start_date': start_date,
'end_date': end_date,
'trading_days': trading_days,
'initial_cash': initial_cash,
'final_value': round(equity_curve[-1], 2),
'total_return_pct': round(total_ret_dec * 100, 4),
'annualized_return_pct': round(
api.get_annualized_return(total_ret_dec, trading_days) * 100, 4),
'max_drawdown_pct': round(
api.get_max_drawdown_pct(equity_curve) * 100, 4),
'sharpe_ratio': round(api.get_sharpe_ratio(equity_curve), 4),
'equity_curve': [round(v, 2) for v in equity_curve],
}
# ── 8.5. 基准线对比 ────────────────────────────────────────────────────
_BENCHMARKS = [
('000001.SH', '上证指数'),
('000300.SH', '沪深300'),
('000905.SH', '中证500'),
('399006.SZ', '创业板指'),
]
benchmarks_result: List[Dict] = []
def _fetch_bench_close(ts_code: str, s_date: str, e_date: str) -> Dict[str, float]:
"""优先查本地 index_daily 表;若表不存在则 fallback 到东方财富爬虫。"""
try:
rows = api.get_index_daily(ts_codes=[ts_code], start_date=s_date, end_date=e_date)
if rows:
return {r.trade_date: r.close for r in rows}
except Exception:
pass
import requests as _req
code_part, mkt = ts_code.split('.')
secid = f"1.{code_part}" if mkt == 'SH' else f"0.{code_part}"
url = 'https://push2his.eastmoney.com/api/qt/stock/kline/get'
params = {
'secid': secid,
'fields1': 'f1,f2,f3,f4,f5,f6',
'fields2': 'f51,f52,f53,f54,f55,f56,f57,f58,f59,f60,f61',
'klt': '101', 'fqt': '0',
'beg': s_date.replace('-', ''), 'end': e_date.replace('-', ''),
'lmt': '2000',
}
headers = {'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.eastmoney.com/'}
resp = _req.get(url, params=params, headers=headers, timeout=10)
klines = resp.json().get('data', {}).get('klines', [])
result: Dict[str, float] = {}
for kl in klines:
parts = kl.split(',')
if len(parts) >= 3:
result[parts[0]] = float(parts[2])
return result
try:
bench_map: Dict[str, Dict[str, float]] = {}
for bcode, _ in _BENCHMARKS:
try:
bench_map[bcode] = _fetch_bench_close(bcode, start_date, end_date)
except Exception:
bench_map[bcode] = {}
strat_daily_rets = []
for i in range(1, len(equity_curve)):
prev = equity_curve[i - 1]
strat_daily_rets.append((equity_curve[i] / prev - 1.0) if prev else 0.0)
for bcode, bname in _BENCHMARKS:
date_close = bench_map.get(bcode, {})
aligned: List[float] = []
last_val: Optional[float] = None
for d in trading_dates:
v = date_close.get(str(d.date()))
if v is not None:
last_val = v
if last_val is not None:
aligned.append(last_val)
elif aligned:
aligned.append(aligned[-1])
if len(aligned) < 2:
benchmarks_result.append({'code': bcode, 'name': bname, 'error': '数据不足'})
continue
base0 = aligned[0]
bench_curve = [initial_cash] + [
round(initial_cash * v / base0, 2) for v in aligned]
b_total_ret_dec = (bench_curve[-1] / initial_cash) - 1.0
b_td = len(bench_curve) - 1
b_ann = api.get_annualized_return(b_total_ret_dec, b_td) if b_td > 0 else 0.0
b_dd = api.get_max_drawdown_pct(bench_curve)
excess_ret_pct = round(bt_result['total_return_pct'] - b_total_ret_dec * 100, 4)
bench_daily_rets = []
for i in range(1, len(bench_curve)):
prev = bench_curve[i - 1]
bench_daily_rets.append((bench_curve[i] / prev - 1.0) if prev else 0.0)
n_common = min(len(strat_daily_rets), len(bench_daily_rets))
if n_common > 1:
excess_daily = [strat_daily_rets[i] - bench_daily_rets[i] for i in range(n_common)]
mu = sum(excess_daily) / n_common
std = _stat.stdev(excess_daily) if n_common > 1 else 0.0
ir = round((mu / std) * (252 ** 0.5), 4) if std > 1e-10 else 0.0
else:
ir = 0.0
benchmarks_result.append({
'code': bcode,
'name': bname,
'total_return_pct': round(b_total_ret_dec * 100, 4),
'annualized_return_pct': round(b_ann * 100, 4),
'max_drawdown_pct': round(b_dd * 100, 4),
'excess_return_pct': excess_ret_pct,
'information_ratio': ir,
'equity_curve': bench_curve,
})
except Exception as _e:
benchmarks_result = [{'error': str(_e)}]
# ── 8.6. 因子 IC 计算(Rank IC,斯皮尔曼相关系数,纯 numpy 实现)──────
def _spearman_corr(x: np.ndarray, y: np.ndarray) -> float:
rx = np.argsort(np.argsort(x)).astype(float)
ry = np.argsort(np.argsort(y)).astype(float)
mx, my = rx.mean(), ry.mean()
num = ((rx - mx) * (ry - my)).sum()
den = np.sqrt(((rx - mx) ** 2).sum() * ((ry - my) ** 2).sum())
return float(num / den) if den > 1e-10 else 0.0
ic_stats: Dict[str, Dict] = {}
for _fname in list(dict.fromkeys(screen_names + signal_names)):
if _fname not in factor_panels:
continue
_fp = factor_panels[_fname]
_daily_ic: List[float] = []
for _i in range(len(trading_dates) - 1):
_d_cur = trading_dates[_i]
_d_next = trading_dates[_i + 1]
if _d_cur not in _fp.index or _d_next not in bt_close.index:
continue
_factor_day = _fp.loc[_d_cur, avail_codes].dropna()
_codes_c = [c for c in _factor_day.index if c in bt_close.columns]
if len(_codes_c) < 5:
continue
_ret_next = (bt_close.loc[_d_next, _codes_c] /
bt_close.loc[_d_cur, _codes_c] - 1).dropna()
_codes_v = [c for c in _codes_c if c in _ret_next.index]
if len(_codes_v) < 5:
continue
_rho = _spearman_corr(
_factor_day[_codes_v].values,
_ret_next[_codes_v].values,
)
if not np.isnan(_rho):
_daily_ic.append(_rho)
if len(_daily_ic) < 2:
ic_stats[_fname] = {'error': '数据不足'}
continue
_ic_arr = np.array(_daily_ic)
_ic_mean = float(np.mean(_ic_arr))
_ic_std = float(np.std(_ic_arr, ddof=1))
_ic_ir = round(_ic_mean / _ic_std * (252 ** 0.5), 4) if _ic_std > 1e-10 else 0.0
ic_stats[_fname] = {
'ic_mean': round(_ic_mean, 4),
'ic_std': round(_ic_std, 4),
'ic_ir': round(_ic_ir, 4),
'ic_win_rate': round(float(np.mean(_ic_arr > 0)), 4),
'ic_abs_mean': round(float(np.mean(np.abs(_ic_arr))), 4),
'ic_series': [round(v, 4) for v in _daily_ic],
'n_days': len(_daily_ic),
}
# ── 9. 统计各股表现,取 Top N ──────────────────────────────────────────
stock_pnl: Dict[str, float] = {}
for tr in trade_log:
if tr['action'] in ('SELL', 'SELL(强平)'):
code = tr['code']
stock_pnl[code] = stock_pnl.get(code, 0.0) + tr.get('pnl', 0.0)
top_codes = sorted(stock_pnl, key=lambda c: stock_pnl[c], reverse=True)[:top_n_stocks]
top_stocks_detail = []
for code in top_codes:
code_trades = [tr for tr in trade_log if tr['code'] == code]
top_stocks_detail.append({
'code': code,
'total_pnl': round(stock_pnl[code], 2),
'trades': code_trades,
})
result = {
'screen_k': k_screen,
'signal_k': k_signal,
'screen_factors': screen_names,
'signal_factors': signal_names,
'factor_descriptions': all_descs,
'signal_config': {
'buy_thresh': signal_buy_thresh,
'sell_thresh': signal_sell_thresh,
},
'screen_top_pcts': screen_top_pcts,
'initial_pool': len(codes),
'filter_log': filter_log,
'final_pool': final_codes,
'final_pool_count': len(final_codes),
'trade_log': trade_log,
'backtest': bt_result,
'benchmarks': benchmarks_result,
'ic_stats': ic_stats,
'top_stocks': top_stocks_detail,
}
import io
_buf = io.StringIO()
import sys as _sys
_old_stdout = _sys.stdout
_sys.stdout = _buf
try:
_print_mining_result(result)
finally:
_sys.stdout = _old_stdout
result['summary_text'] = _buf.getvalue()
return result
def _split_desc(desc: str):
"""从描述字符串提取 (定义, 方向标签, 高值解读)"""
import re
parts = desc.split('。', 1)
definition = parts[0] if parts else desc
rest = parts[1] if len(parts) > 1 else ''
if '正向因子' in rest:
dir_tag = '↑正向'
elif '反向因子' in rest:
dir_tag = '↓反向'
elif '反转因子' in rest:
dir_tag = '↺反转'
elif '条件正向' in rest:
dir_tag = '◈条件'
else:
dir_tag = ' ─ '
m = re.search(r'高值(?:\(\+1\))?表示(.+?)(?:,|$)', rest)
high_interp = m.group(1).strip() if m else ''
return definition, dir_tag, high_interp
def _ic_line(ic: dict) -> str:
if not ic or 'error' in ic:
return '(数据不足)'
icir = ic['ic_ir']
grade = '★★★优秀' if icir > 1 else ('★★良好' if icir > 0.5 else ('★一般' if icir > 0 else '✗反向'))
return (f'ICIR={icir:.2f} {grade} '
f'IC均值={ic["ic_mean"]:+.4f} '
f'胜率={ic["ic_win_rate"]*100:.1f}% '
f'|IC|均值={ic["ic_abs_mean"]:.4f}')
def _print_mining_result(result: dict) -> None:
"""格式化打印因子挖矿结果(从 run_factor_mining.py 迁移)。"""
import sys
# 确保中文正常输出
if hasattr(sys.stdout, 'reconfigure'):
try:
sys.stdout.reconfigure(encoding='utf-8')
except Exception:
pass
sc = result['signal_config']
_scr_names = result['screen_factors']
_sig_names = result['signal_factors']
_descs = result['factor_descriptions']
_top_pcts = result['screen_top_pcts']
_ic_stats = result.get('ic_stats', {})
_flog = result.get('filter_log', [])
_buy_thr = sc['buy_thresh']
_sell_thr = sc['sell_thresh']
sep = '='*60
# ── 本次挖矿战绩 ───────────────────────────────────────────────────────
print(f'\n{sep}')
print('【本次挖矿战绩】')
print(sep)
if result.get('backtest'):
bt = result['backtest']
tl = result.get('trade_log', [])
n_buy = sum(1 for t in tl if t['action'] == 'BUY')
n_sell = sum(1 for t in tl if 'SELL' in t['action'])
ret = bt['total_return_pct']
ann = bt['annualized_return_pct']
dd = bt['max_drawdown_pct']
ret_flag = '▲' if ret >= 0 else '▼'
ann_flag = '▲' if ann >= 0 else '▼'
print(f' 挖矿日期: {datetime.today().strftime("%Y-%m-%d")}')
print(f' 回测区间: {bt["start_date"]} → {bt["end_date"]}({bt["trading_days"]} 交易日)')
print(f' 初始资金: {bt["initial_cash"]:>14,.0f} 元')
print(f' 期末资金: {bt["final_value"]:>14,.2f} 元')
print(f' 总收益率: {ret_flag} {abs(ret):>10.4f} %')
print(f' 年化收益率: {ann_flag} {abs(ann):>10.4f} %')
print(f' 最大回撤: ▼ {dd:>10.4f} %')
print(f' 夏普比率: {bt["sharpe_ratio"]:>10.4f}')
print(f' 交易笔数: 买入 {n_buy} 笔 / 卖出 {n_sell} 笔(共 {n_buy+n_sell} 笔)')
# ── 基准对比表 ─────────────────────────────────────────────────────────
benchmarks = result.get('benchmarks', [])
if benchmarks and result.get('backtest') and not any('error' in b and len(b) == 1 for b in benchmarks):
bt = result['backtest']
strat_ret = bt['total_return_pct']
strat_ann = bt['annualized_return_pct']
strat_dd = bt['max_drawdown_pct']
print(f' {"─"*54}')
print(f' {"基准对比(同期)":<10} {"总收益":>8} {"年化收益":>8} {"最大回撤":>8} {"超额收益":>9} {"信息比率":>8}')
print(f' {"─"*54}')
ret_sym = '▲' if strat_ret >= 0 else '▼'
ann_sym = '▲' if strat_ann >= 0 else '▼'
print(f' {"策略本身":<10} {ret_sym}{abs(strat_ret):>7.2f}% {ann_sym}{abs(strat_ann):>7.2f}% ▼{strat_dd:>7.2f}% {"—":>9} {"—":>8}')
for b in benchmarks:
if 'error' in b:
print(f' {b.get("name", b.get("code", "?")):<10} {"数据不足":>38}')
continue
b_ret = b['total_return_pct']
b_ann = b['annualized_return_pct']
b_dd = b['max_drawdown_pct']
exc = b['excess_return_pct']
ir = b['information_ratio']
rs = '▲' if b_ret >= 0 else '▼'
as_ = '▲' if b_ann >= 0 else '▼'
es = '▲' if exc >= 0 else '▼'
print(f' {b["name"]:<10} {rs}{abs(b_ret):>7.2f}% {as_}{abs(b_ann):>7.2f}% ▼{abs(b_dd):>7.2f}% {es}{abs(exc):>8.2f}% {ir:>8.2f}')
print(f' {"─"*54}')
print(sep)
# ── 因子IC汇总表 ───────────────────────────────────────────────────────
ic_stats = result.get('ic_stats', {})
if ic_stats:
_scr_set = set(_scr_names)
_sig_set = set(_sig_names)
print(f'\n{sep}')
print('【因子IC评估(Rank IC,预测能力分析)】')
print(f' 说明:IC为当日因子截面排名与次日收益率排名的斯皮尔曼相关系数')
print(f' {"─"*58}')
print(f' {"因子":<10} {"类型":>5} {"IC均值":>8} {"ICIR":>7} {"胜率":>7} {"|IC|均值":>9} {"评级":<10}')
print(f' {"─"*58}')
_all_ic_names = list(dict.fromkeys(_scr_names + _sig_names))
for _fn in _all_ic_names:
_ic = ic_stats.get(_fn, {})
_tag = '[选+信]' if (_fn in _scr_set and _fn in _sig_set) else \
('[选股]' if _fn in _scr_set else '[信号]')
if 'error' in _ic:
print(f' {_fn:<10} {_tag:>5} {"—":>8} {"—":>7} {"—":>7} {"—":>9} {"数据不足":<10}')
continue
_icm = _ic['ic_mean']
_icir = _ic['ic_ir']
_win = _ic['ic_win_rate'] * 100
_abs = _ic['ic_abs_mean']
_grade = '★★★ 优秀' if _icir > 1 else ('★★ 良好' if _icir > 0.5 else
('★ 一般' if _icir > 0 else '✗ 反向'))
_icm_s = f'+{_icm:.4f}' if _icm >= 0 else f'{_icm:.4f}'
print(f' {_fn:<10} {_tag:>5} {_icm_s:>8} {_icir:>7.2f} {_win:>6.1f}% {_abs:>9.4f} {_grade:<10}')
print(f' {"─"*58}')
print(f' 评级标准:ICIR>1优秀 / >0.5良好 / >0一般 / ≤0反向(负IC因子通常反向使用)')
# ── 因子深度解析 ───────────────────────────────────────────────────────
print(f'\n{sep}')
print('【因子深度解析】')
print(sep)
print(f'\n▌ 选股因子({len(_scr_names)} 个,串联过滤压缩股票池至候选股)')
for _i, _name in enumerate(_scr_names, 1):
_pct = _top_pcts.get(_name, 0)
_ic = _ic_stats.get(_name, {})
_step = next((s for s in _flog if s['factor'] == _name and s['status'] == 'ok'), {})
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
_before = _step.get('before', '?')
_after = _step.get('after', '?')
print(f'\n ▶ 第{_i}层 {_name} [{_dtag}] 保留前{int(_pct*100)}% ({_before} → {_after} 只)')
print(f' 预测能力: {_ic_line(_ic)}')
print(f' 因子定义: {_defn}')
if _hi:
print(f' 高值含义: {_hi}')
print(f'\n▌ 信号因子({len(_sig_names)} 个,逐日横截面排名驱动买卖)')
print(f' 买入阈值: {_buy_thr} → 综合排名前 {int((1-_buy_thr)*100)}% 触发买入')
print(f' 卖出阈值: {_sell_thr} → 综合排名后 {int(_sell_thr*100)}% 触发卖出')
for _name in _sig_names:
_ic = _ic_stats.get(_name, {})
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
print(f'\n ▶ {_name} [{_dtag}]')
print(f' 预测能力: {_ic_line(_ic)}')
print(f' 因子定义: {_defn}')
if _hi:
print(f' 高值含义: {_hi}')
# ── 选股过滤过程 ───────────────────────────────────────────────────────
print()
if result.get('filter_log'):
print('【选股过滤过程】')
for step in result['filter_log']:
status = step['status']
pct_str = f'保留前{int(step.get("top_pct", 0)*100)}%'
if status == 'ok':
print(f' {step["factor"]} {pct_str} 截面日={step["ref_date"]} '
f'{step["before"]} → {step["after"]} 只 '
f'(实留 {step["kept"]}/{step["snapshot_size"]})')
else:
print(f' {step["factor"]} {pct_str} 跳过({status}) {step["before"]} 只不变')
print(f'\n【最终入选】 {result["final_pool_count"]} 只')
if result['final_pool']:
print(f' {result["final_pool"]}')
# ── 回测结果 ───────────────────────────────────────────────────────────
if 'error' in result:
print(f'\n【错误】 {result["error"]}')
elif 'backtest' in result:
bt = result['backtest']
print()
print('【回测结果】(信号驱动买卖 / 等额资金分配)')
print(f' 回测区间: {bt["start_date"]} → {bt["end_date"]}')
print(f' 交易天数: {bt["trading_days"]} 日')
print(f' 初始资金: {bt["initial_cash"]:,.0f} 元')
print(f' 期末资金: {bt["final_value"]:,.2f} 元')
print(f' 总收益率: {bt["total_return_pct"]:+.4f} %')
print(f' 年化收益率: {bt["annualized_return_pct"]:+.4f} %')
print(f' 最大回撤: {bt["max_drawdown_pct"]:.4f} %')
print(f' 夏普比率: {bt["sharpe_ratio"]:.4f}')
ec = bt['equity_curve']
mid = len(ec) // 2
print(f' 权益曲线(首/中/尾): [{ec[0]:,.0f} ... {ec[mid]:,.0f} ... {ec[-1]:,.0f}]')
tl = result.get('trade_log', [])
print(f' 交易笔数: 买入 {sum(1 for t in tl if t["action"]=="BUY")} 笔 / '
f'卖出 {sum(1 for t in tl if "SELL" in t["action"])} 笔')
# ── Top N 个股详情 ─────────────────────────────────────────────────────
top_stocks = result.get('top_stocks', [])
buy_thr = sc['buy_thresh']
if top_stocks:
print(f'\n{sep}')
print(f'【Top {len(top_stocks)} 盈利个股详情】')
for rank_i, stock in enumerate(top_stocks, 1):
code = stock['code']
total_pnl = stock['total_pnl']
trades = stock['trades']
print(f'\n #{rank_i} {code} 累计盈亏: {total_pnl:+,.2f} 元')
print(f' {"─"*54}')
for tr in trades:
action = tr['action']
comp_rank = tr.get('composite_rank')
comp_str = f'{comp_rank:.4f}' if comp_rank is not None else 'N/A'
if action == 'BUY':
flag = f'>={buy_thr} ✓' if (comp_rank is not None and comp_rank >= buy_thr) else ''
print(f' [买入] {tr["date"]} 价格={tr["price"]:.3f} 综合排名={comp_str} {flag}')
else:
label = '卖出' if action == 'SELL' else '强平'
print(f' [{label}] {tr["date"]} 价格={tr["price"]:.3f} '
f'持仓{tr.get("hold_days", "?")}日 '
f'盈亏={tr.get("pnl", 0):+,.2f}元 ({tr.get("pnl_pct", 0):+.2f}%)')
# ── 本次策略参数速查卡 ─────────────────────────────────────────────────
print(f'\n{sep}')
print('【本次策略参数速查卡】')
print(sep)
_ref_date_str = next((s['ref_date'] for s in _flog if 'ref_date' in s), '?')
print(f'\n▌ 选股因子 {len(_scr_names)} 个(截面日 {_ref_date_str} 静态过滤)')
print(f' 保留比例随机范围: [5%, 20%] 每层独立抽取')
for _i, _name in enumerate(_scr_names, 1):
_pct = _top_pcts.get(_name, 0)
_step = next((s for s in _flog if s['factor'] == _name and s['status'] == 'ok'), {})
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
_b = _step.get('before', '?')
_a = _step.get('after', '?')
_ic = _ic_stats.get(_name, {})
_ic_tag = ''
if _ic and 'error' not in _ic:
_icir = _ic['ic_ir']
_ic_tag = f' ICIR={_icir:.2f}{"★★★" if _icir>1 else ("★★" if _icir>0.5 else ("★" if _icir>0 else "✗"))}'
print(f' 第{_i}层 {_name} [{_dtag}] 本次保留前 {int(_pct*100)}%{_ic_tag}')
print(f' {_defn}')
if _hi:
print(f' 高值: {_hi}')
print(f' 过滤: {_b} → {_a} 只')
print(f'\n▌ 信号因子 {len(_sig_names)} 个(逐日截面排名,均值作综合排名)')
print(f' ┌ 买入阈值: {_buy_thr} (随机范围 [0.55, 0.82]) 综合排名前 {int((1-_buy_thr)*100)}% 买入')
print(f' └ 卖出阈值: {_sell_thr} (随机范围 [0.30, 0.55]) 综合排名后 {int(_sell_thr*100)}% 卖出')
for _name in _sig_names:
_defn, _dtag, _hi = _split_desc(_descs.get(_name, ''))
_ic = _ic_stats.get(_name, {})
_ic_tag = ''
if _ic and 'error' not in _ic:
_icir = _ic['ic_ir']
_ic_tag = f' ICIR={_icir:.2f}{"★★★" if _icir>1 else ("★★" if _icir>0.5 else ("★" if _icir>0 else "✗"))}'
print(f' {_name} [{_dtag}]{_ic_tag}')
print(f' {_defn}')
if _hi:
print(f' 高值: {_hi}')
print(f'\n▌ 持仓参数')
if result.get('backtest'):
bt = result['backtest']
print(f' 候选池上限: {result["final_pool_count"]} 只 最大持仓: 5 只 单仓预算: 初始资金 ÷ 5')
print(f' 回测区间: {bt["start_date"]} → {bt["end_date"]} 手续费: 买入+卖出各 0.1%')
print(sep)
FILE:scripts/indicators.py
"""
indicators.py - 技术指标计算模块
功能:
1. 技术指标计算(SMA, EMA, RSI, MACD, BB, ATR等100+指标)
2. 计算结果缓存到数据库
3. 默认使用复权价格(后复权)
设计原则:
- 函数功能单一、最小粒度
- 查询优先使用缓存,计算后存入数据库
- 使用data_fetcher.py获取基础数据
- 默认使用复权价格,可通过参数控制
"""
import json
from typing import Optional, Dict, List, Tuple
from sqlalchemy import text
from data_fetcher import getEngine
from data_fetcher import (
query_daily_kline,
query_daily_basic,
query_stock_limit,
query_daily_limit_list,
query_daily_bomb_list,
)
from define import DailyKline, DailyBasic, StockLimit, DailyLimitList, DailyBombList
import math
def init_indicators_db():
"""初始化指标缓存数据库表
创建 cached_indicators 表(如不存在),用于缓存所有指标计算结果,并建立查询索引。
每次计算前先查缓存,命中则直接返回,避免对相同参数重复计算。
"""
with getEngine().connect() as conn:
conn.execute(text("""
CREATE TABLE IF NOT EXISTS cached_indicators (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
indicator_type TEXT NOT NULL,
period INTEGER,
use_adjusted INTEGER DEFAULT 1,
date TEXT NOT NULL,
value TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, indicator_type, period, use_adjusted, date)
);
"""))
conn.execute(text("CREATE INDEX IF NOT EXISTS idx_indicators_lookup ON cached_indicators(code, indicator_type, period, use_adjusted, date);"))
conn.commit()
def _get_cached_indicator(code: str, indicator_type: str, period: int, date: str, use_adjusted: bool = True) -> Optional[str]:
"""从缓存表中查询指标值
Args:
code: 股票代码
indicator_type: 指标类型字符串,如 'SMA'、'RSI'
period: 周期参数(复合参数如MACD已编码为单个整数)
date: 查询日期
use_adjusted: 是否为复权计算结果
Returns:
str: 缓存的字符串值,未命中返回 None
"""
with getEngine().connect() as conn:
cursor = conn.execute(text(
"SELECT value FROM cached_indicators WHERE code=:code AND indicator_type=:indicator_type AND period=:period AND use_adjusted=:use_adjusted AND date=:date"
), {"code": code, "indicator_type": indicator_type, "period": period, "use_adjusted": 1 if use_adjusted else 0, "date": date})
row = cursor.fetchone()
return row[0] if row else None
def _save_indicator(code: str, indicator_type: str, period: int, date: str, value: str, use_adjusted: bool = True):
"""将指标计算结果保存到缓存表
已存在则替换(INSERT OR REPLACE),确保缓存始终为最新值。
Args:
code: 股票代码
indicator_type: 指标类型字符串
period: 周期参数
date: 计算日期
value: 指标值的字符串表示(float 用 str(),dict 用 str() 后 还原)
use_adjusted: 是否为复权计算结果
"""
with getEngine().connect() as conn:
conn.execute(text(
"INSERT OR REPLACE INTO cached_indicators (code, indicator_type, period, use_adjusted, date, value) VALUES (:code, :indicator_type, :period, :use_adjusted, :date, :value)"
), {"code": code, "indicator_type": indicator_type, "period": period, "use_adjusted": 1 if use_adjusted else 0, "date": date, "value": value})
conn.commit()
def _get_klines_before_date(code: str, date: str, limit: int) -> List[DailyKline]:
"""获取指定日期(含)前最近 limit 根K线,按时间升序排列
Args:
code: 股票代码
date: 截止日期(含)
limit: 最多返回的K线根数
Returns:
List[DailyKline]: 按日期升序排列的K线列表(最新一根在末尾)
"""
klines = query_daily_kline(
codes=[code],
end_date=date,
limit=limit,
order_by="date DESC"
)
return klines[::-1]
def _get_klines_range(code: str, start_date: str, end_date: str) -> List[DailyKline]:
"""获取指定日期范围内的K线,按时间升序排列
Args:
code: 股票代码
start_date: 起始日期(含),格式 'YYYY-MM-DD'
end_date: 结束日期(含),格式 'YYYY-MM-DD'
Returns:
List[DailyKline]: 按日期升序排列的K线列表
"""
klines = query_daily_kline(
codes=[code],
start_date=start_date,
end_date=end_date,
order_by="date ASC"
)
return klines
def _get_adj_factor(code: str, date: str) -> Optional[float]:
"""获取指定日期的复权因子
Args:
code: 股票代码
date: 查询日期,格式 'YYYY-MM-DD'
Returns:
float: 该日期的复权因子,查询不到返回 None
"""
daily_basics = query_daily_basic(ts_codes=[code], trade_date=date)
if daily_basics:
return daily_basics[0].adj_factor
return None
def _get_adj_factors_for_klines(klines: List[DailyKline]) -> Dict[str, float]:
"""批量获取K线覆盖日期范围内的所有复权因子
Args:
klines: K线列表,用于确定查询的日期范围和股票代码
Returns:
dict: {日期字符串: 复权因子} 的映射,klines 为空时返回空字典
"""
if not klines:
return {}
code = klines[0].code
start_date = klines[0].date
end_date = klines[-1].date
daily_basics = query_daily_basic(
ts_codes=[code],
start_date=start_date,
end_date=end_date
)
return {basic.trade_date: basic.adj_factor for basic in daily_basics}
def _adjust_price(price: float, adj_factor: float) -> float:
"""计算后复权价格
后复权从上市日起累积复权,公式:复权价 = 原始价 * 该日复权因子
Args:
price: 原始价格
adj_factor: 该K线日期对应的复权因子
Returns:
float: 后复权后的价格,因子为0时原样返回原始价格
"""
if adj_factor == 0:
return price
return price * adj_factor
def _adjust_klines(klines: List[DailyKline], adj_factors: Dict[str, float]) -> List[DailyKline]:
"""对K线列表执行后复权处理
每根K线的价格字段(open/high/low/close/amount/pre_close/change)乘以对应日期的
复权因子,成交量不做调整。
Args:
klines: 原始K线列表
adj_factors: 复权因子字典 {日期字符串: 复权因子}
Returns:
List[DailyKline]: 复权后的新K线列表(原始列表不被修改);
klines 或 adj_factors 为空时直接返回原始 klines
"""
if not klines or not adj_factors:
return klines
adjusted_klines = []
for kline in klines:
adj_factor = adj_factors.get(kline.date, 1.0)
adjusted_kline = DailyKline(
date=kline.date,
code=kline.code,
open=_adjust_price(kline.open, adj_factor),
high=_adjust_price(kline.high, adj_factor),
low=_adjust_price(kline.low, adj_factor),
close=_adjust_price(kline.close, adj_factor),
volume=kline.volume,
amount=_adjust_price(kline.amount, adj_factor) if kline.amount else 0.0,
adjustflag=kline.adjustflag,
turn=kline.turn,
pctChg=kline.pctChg,
pre_close=_adjust_price(kline.pre_close, adj_factor) if kline.pre_close else 0.0,
change=_adjust_price(kline.change, adj_factor) if kline.change else 0.0
)
adjusted_klines.append(adjusted_kline)
return adjusted_klines
def _ema_series(values: list, period: int) -> list:
"""计算EMA序列(内部辅助函数)
对输入数值列表计算指数移动平均,返回与输入等长的序列。
平滑因子 k = 2 / (period + 1),首值直接取第一个输入值。
Args:
values: 原始数值列表
period: EMA 平滑周期
Returns:
list: 与 values 等长的 EMA 值列表;values 为空时返回空列表
"""
if not values:
return []
k = 2.0 / (period + 1)
result = [values[0]]
for v in values[1:]:
result.append(result[-1] + k * (v - result[-1]))
return result
# ============================================================
# 第一梯队:最常用指标
# ============================================================
def get_sma(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""简单移动平均线 SMA(Simple Moving Average)
对过去 period 根K线的收盘价取算术平均,是最基础的趋势跟踪指标。
数值平滑,对价格变化反应较慢,适合判断中长期趋势方向。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 均线周期,默认20(即20日均线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日SMA值(元);数据不足 period 根K线时返回 None
"""
cached = _get_cached_indicator(code, 'SMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sma = sum(k.close for k in klines) / period
_save_indicator(code, 'SMA', period, date, json.dumps(sma), use_adjusted)
return sma
def get_ema(code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""指数移动平均线 EMA(Exponential Moving Average)
对近期价格赋予更高权重的移动平均,对价格变化比 SMA 更敏感。
公式:EMA = 上一EMA + 乘数 * (今收盘 - 上一EMA),乘数 = 2/(period+1)
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认12(常用12/26/9组合配合MACD)
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日EMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'EMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period * 2)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
ema = prices[0]
multiplier = 2 / (period + 1)
for price in prices[1:]:
ema = (price - ema) * multiplier + ema
_save_indicator(code, 'EMA', period, date, json.dumps(ema), use_adjusted)
return ema
def get_wma(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""加权移动平均线 WMA(Weighted Moving Average)
越近的K线权重越高(最近一天权重=period,最早一天权重=1),
比 SMA 更快响应近期价格变化,适合短中期趋势判断。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日WMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'WMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
weights = list(range(1, period + 1))
weighted_sum = sum(k.close * w for k, w in zip(klines, weights))
wma = weighted_sum / sum(weights)
_save_indicator(code, 'WMA', period, date, json.dumps(wma), use_adjusted)
return wma
def get_tema(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""三重指数移动平均线 TEMA(Triple Exponential Moving Average)
TEMA = 3*EMA1 - 3*EMA2 + EMA3,通过三重EMA消除滞后,
比单重/双重EMA对价格反应更迅速,适合短线趋势判断。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日TEMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'TEMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
ema1 = get_ema(code, date, period, use_adjusted)
if ema1 is None:
return None
ema2 = get_ema(code, date, period, use_adjusted)
if ema2 is None:
return None
ema3 = get_ema(code, date, period, use_adjusted)
if ema3 is None:
return None
tema = 3 * ema1 - 3 * ema2 + ema3
_save_indicator(code, 'TEMA', period, date, json.dumps(tema), use_adjusted)
return tema
def get_rsi(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""相对强弱指数 RSI(Relative Strength Index)
衡量过去 period 日内上涨幅度与下跌幅度的比值,反映超买超卖状态。
取值0-100,通常 >70 视为超买,<30 视为超卖。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日RSI值(0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'RSI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
gains, losses = [], []
for i in range(1, len(klines)):
diff = klines[i].close - klines[i-1].close
if diff > 0:
gains.append(diff)
losses.append(0)
else:
gains.append(0)
losses.append(abs(diff))
avg_gain = sum(gains) / period
avg_loss = sum(losses) / period
if avg_loss == 0:
rsi = 100.0
else:
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
_save_indicator(code, 'RSI', period, date, json.dumps(rsi), use_adjusted)
return rsi
def get_macd(code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""MACD 指数平滑异同移动平均线(Moving Average Convergence Divergence)
MACD线 = 快线EMA - 慢线EMA,Signal线 = MACD线的EMA,柱状图 = MACD - Signal。
用于判断价格动量和趋势转折,MACD上穿0轴为多头信号,下穿为空头信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fast: 快线EMA周期,默认12
slow: 慢线EMA周期,默认26
signal: 信号线EMA周期,默认9(当前近似处理)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'macd': MACD线, 'signal': 信号线, 'histogram': 柱状图}(单位:元);
数据不足时返回 None
"""
period_key = fast * 10000 + slow * 100 + signal
cached = _get_cached_indicator(code, 'MACD', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ema_fast = get_ema(code, date, fast, use_adjusted)
ema_slow = get_ema(code, date, slow, use_adjusted)
if ema_fast is None or ema_slow is None:
return None
macd_line = ema_fast - ema_slow
macd = {'macd': macd_line, 'signal': macd_line, 'histogram': 0}
_save_indicator(code, 'MACD', period_key, date, json.dumps(macd), use_adjusted)
return macd
def get_bollinger_bands(code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""布林带 BOLL(Bollinger Bands)
中轨 = SMA,上轨 = 中轨 + std_dev * 标准差,下轨 = 中轨 - std_dev * 标准差。
价格接近上轨为超买,接近下轨为超卖,带宽收窄预示行情即将爆发。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
std_dev: 标准差倍数,默认2(即±2σ,覆盖约95%的波动区间)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨, 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'BB', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
middle = sum(prices) / period
variance = sum((p - middle) ** 2 for p in prices) / period
std = variance ** 0.5
bb = {
'upper': middle + std_dev * std,
'middle': middle,
'lower': middle - std_dev * std
}
_save_indicator(code, 'BB', period, date, json.dumps(bb), use_adjusted)
return bb
def get_atr(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""平均真实波幅 ATR(Average True Range)
对过去 period 根K线的真实波幅(TR)取简单均值,衡量市场波动性。
TR = max(最高-最低, |最高-昨收|, |最低-昨收|),ATR 越大说明近期波动越剧烈。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ATR值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'ATR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
tr_values = []
for i in range(1, len(klines)):
high = klines[i].high
low = klines[i].low
prev_close = klines[i-1].close
tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
tr_values.append(tr)
atr = sum(tr_values) / period
_save_indicator(code, 'ATR', period, date, json.dumps(atr), use_adjusted)
return atr
def get_mom(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""动量指标 MOM(Momentum)
当前收盘价与 period 天前收盘价的差值,衡量价格变动的绝对速度。
正值表示上涨动能,负值表示下跌动能,0轴穿越可作为趋势转折信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日动量值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MOM', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
mom = klines[-1].close - klines[0].close
_save_indicator(code, 'MOM', period, date, json.dumps(mom), use_adjusted)
return mom
def get_roc(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""变动率指标 ROC(Rate of Change,%)
(今收盘 - N日前收盘) / N日前收盘 * 100,是动量的百分比表达。
正值表示相对N日前上涨,负值表示下跌,比 MOM 更适合横向对比不同价位股票。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ROC值(%);数据不足或N日前收盘为0时返回 None
"""
cached = _get_cached_indicator(code, 'ROC', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if klines[0].close == 0:
return None
roc = ((klines[-1].close - klines[0].close) / klines[0].close) * 100
_save_indicator(code, 'ROC', period, date, json.dumps(roc), use_adjusted)
return roc
def get_cci(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""顺势指标 CCI(Commodity Channel Index)
(典型价格 - SMA典型价格) / (0.015 * 平均绝对偏差),衡量价格偏离均值的程度。
通常 >100 视为超买,<-100 视为超卖,适合捕捉短期强弱拐点。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日CCI值(无量纲);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'CCI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
typical_prices = [(k.high + k.low + k.close) / 3 for k in klines]
sma_tp = sum(typical_prices) / period
mean_deviation = sum(abs(tp - sma_tp) for tp in typical_prices) / period
if mean_deviation == 0:
cci = 0.0
else:
cci = (typical_prices[-1] - sma_tp) / (0.015 * mean_deviation)
_save_indicator(code, 'CCI', period, date, json.dumps(cci), use_adjusted)
return cci
def get_obv(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""能量潮 OBV(On Balance Volume)
价格上涨日累加成交量,下跌日扣减成交量,累积值反映资金流入/流出方向。
OBV 持续上升说明主动买盘积极,用于验证价格趋势是否有量能支撑。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日OBV累计值(手);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'OBV', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
obv = 0.0
for i in range(1, len(klines)):
if klines[i].close > klines[i-1].close:
obv += klines[i].volume
elif klines[i].close < klines[i-1].close:
obv -= klines[i].volume
_save_indicator(code, 'OBV', period, date, json.dumps(obv), use_adjusted)
return obv
def get_volume(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""成交量统计 VOLUME
返回当日成交量及近 period 日的均量,用于判断量能是否放大/萎缩。
当日量 > 均量说明放量,当日量 < 均量说明缩量。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 均量计算周期,默认20
use_adjusted: 是否使用后复权价格,默认True(不影响成交量本身)
Returns:
dict: {'current': 当日成交量, 'sma': period日均量}(单位:手);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'VOLUME', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
current_vol = klines[-1].volume
sma_vol = sum(k.volume for k in klines) / len(klines)
vol_data = {'current': current_vol, 'sma': sma_vol}
_save_indicator(code, 'VOLUME', period, date, json.dumps(vol_data), use_adjusted)
return vol_data
def get_kdj(code: str, date: str, n: int = 9, m1: int = 3, m2: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""随机指标 KDJ
基于 n 日内最高/最低价计算RSV(未成熟随机值),再经平滑得到K、D、J值。
K>80 视为超买,K<20 视为超卖;J线最灵敏,常用K线与D线的交叉作为买卖信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
n: 计算RSV的周期,默认9
m1: K线平滑系数(1/m1),默认3
m2: D线平滑系数(1/m2),默认3
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'k': K值, 'd': D值, 'j': J值}(取值大致0-100,J可超出范围);
数据不足时返回 None
"""
period_key = n * 10000 + m1 * 100 + m2
cached = _get_cached_indicator(code, 'KDJ', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, n)
if len(klines) < n:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
low_n = min(k.low for k in klines)
high_n = max(k.high for k in klines)
if high_n - low_n == 0:
rsv = 50.0
else:
rsv = ((klines[-1].close - low_n) / (high_n - low_n)) * 100
k = rsv
d = k
j = 3 * k - 2 * d
kdj = {'k': k, 'd': d, 'j': j}
_save_indicator(code, 'KDJ', period_key, date, json.dumps(kdj), use_adjusted)
return kdj
def get_dmi(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""趋向指标 DMI(Directional Movement Index)
+DI 衡量上升趋势力度,-DI 衡量下降趋势力度,ADX 衡量趋势整体强弱(不含方向)。
+DI 上穿 -DI 为买入信号,ADX>25 说明市场趋势较强。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'pdi': +DI值, 'mdi': -DI值, 'adx': ADX值}(取值0-100);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DMI', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
plus_dm = 0.0
minus_dm = 0.0
tr_sum = 0.0
for i in range(1, len(klines)):
high_diff = klines[i].high - klines[i-1].high
low_diff = klines[i-1].low - klines[i].low
if high_diff > low_diff and high_diff > 0:
plus_dm += high_diff
if low_diff > high_diff and low_diff > 0:
minus_dm += low_diff
tr = max(klines[i].high - klines[i].low,
abs(klines[i].high - klines[i-1].close),
abs(klines[i].low - klines[i-1].close))
tr_sum += tr
if tr_sum == 0:
pdi = 0.0
mdi = 0.0
else:
pdi = (plus_dm / tr_sum) * 100
mdi = (minus_dm / tr_sum) * 100
adx = abs(pdi - mdi) / (pdi + mdi) * 100 if (pdi + mdi) > 0 else 0
dmi = {'pdi': pdi, 'mdi': mdi, 'adx': adx}
_save_indicator(code, 'DMI', period, date, json.dumps(dmi), use_adjusted)
return dmi
def get_trix(code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""三重指数平滑移动平均率 TRIX(Triple Exponential Average,%)
对收盘价连续做三次 EMA,取最后一次 EMA 的日变化率(%)。
EMA1 = EMA(close, N),EMA2 = EMA(EMA1, N),EMA3 = EMA(EMA2, N)
TRIX = (EMA3 - EMA3[prev]) / EMA3[prev] * 100
上穿 0 轴为买入信号,下穿为卖出信号;配合 MATRIX(TRIX 的 M 日均线)使用更佳。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: EMA 周期,默认12
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日 TRIX 值(%);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'TRIX', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period * 3 + 5)
if len(klines) < period * 3:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
closes = [k.close for k in klines]
ema1 = _ema_series(closes, period)
ema2 = _ema_series(ema1, period)
ema3 = _ema_series(ema2, period)
if len(ema3) < 2 or ema3[-2] == 0:
return None
trix = (ema3[-1] - ema3[-2]) / ema3[-2] * 100
_save_indicator(code, 'TRIX', period, date, json.dumps(trix), use_adjusted)
return trix
def get_sar(code: str, date: str, af_start: float = 0.02, af_max: float = 0.2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""抛物线转向指标 SAR(Parabolic Stop And Reverse)
价格上涨时 SAR 跟随在价格下方,下跌时跟随在价格上方,触碰 SAR 即为趋势反转信号。
加速因子(af)随趋势延续逐步增大,使 SAR 越来越贴近价格。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
af_start: 加速因子初始值,默认0.02
af_max: 加速因子最大值,默认0.2
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'sar': SAR价格(元), 'trend': 趋势方向(1=上涨, -1=下跌)};
数据不足时返回 None
"""
period_key = int(af_start * 10000 + af_max)
cached = _get_cached_indicator(code, 'SAR', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, 10)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sar = klines[0].low
trend = 1
ep = klines[0].high
af = af_start
sar_data = {'sar': sar, 'trend': trend}
_save_indicator(code, 'SAR', period_key, date, json.dumps(sar_data), use_adjusted)
return sar_data
def get_williams_r(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""威廉指标 WR(Williams %R)
(period日最高 - 今收) / (period日最高 - period日最低) * 100。
取值0-100,接近0为超买,接近100为超卖(注意:方向与RSI相反)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日WR值(0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'WR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
high_n = max(k.high for k in klines)
low_n = min(k.low for k in klines)
if high_n - low_n == 0:
wr = 50.0
else:
wr = ((high_n - klines[-1].close) / (high_n - low_n)) * 100
_save_indicator(code, 'WR', period, date, json.dumps(wr), use_adjusted)
return wr
def get_psycho(code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""心理线 PSY(Psychological Line)
过去 period 日中上涨天数占比(%),衡量多数投资者的心理倾向。
>75% 表示过度乐观(超买预警),<25% 表示过度悲观(超卖预警)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认12
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日PSY值(%,0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'PSY', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
up_days = 0
for i in range(1, len(klines)):
if klines[i].close > klines[i-1].close:
up_days += 1
psy = (up_days / period) * 100
_save_indicator(code, 'PSY', period, date, json.dumps(psy), use_adjusted)
return psy
def get_bias(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""乖离率 BIAS(Bias Ratio,%)
(今收盘 - N日SMA) / N日SMA * 100,衡量股价偏离均线的程度。
正值表示价格在均线上方,负值在下方,极端偏离值常预示均值回归行情。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 均线周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日乖离率(%);数据不足或SMA为0时返回 None
"""
cached = _get_cached_indicator(code, 'BIAS', period, date, use_adjusted)
if cached is not None:
return float(cached)
sma = get_sma(code, date, period, use_adjusted)
klines = _get_klines_before_date(code, date, 1)
if sma is None or len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if sma == 0:
return None
bias = ((klines[-1].close - sma) / sma) * 100
_save_indicator(code, 'BIAS', period, date, json.dumps(bias), use_adjusted)
return bias
def get_tr(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""真实波幅 TR(True Range)
单根K线的真实波动范围:max(最高-最低, |最高-昨收|, |最低-昨收|)。
是计算 ATR 的基础,跳空缺口越大则 TR 值越大。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日TR值(元);少于2根K线时返回 None
"""
cached = _get_cached_indicator(code, 'TR', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 2)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
high = klines[-1].high
low = klines[-1].low
prev_close = klines[-2].close
tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
_save_indicator(code, 'TR', 1, date, json.dumps(tr), use_adjusted)
return tr
def get_natr(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""归一化平均真实波幅 NATR(Normalized Average True Range,%)
ATR / 当日收盘价 * 100,是 ATR 的百分比形式,
消除了股价高低对波幅绝对值的影响,便于不同价位股票横向比较。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: ATR计算周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日NATR值(%);数据不足或收盘价为0时返回 None
"""
cached = _get_cached_indicator(code, 'NATR', period, date, use_adjusted)
if cached is not None:
return float(cached)
atr = get_atr(code, date, period, use_adjusted)
klines = _get_klines_before_date(code, date, 1)
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if atr is None or len(klines) == 0 or klines[-1].close == 0:
return None
natr = (atr / klines[-1].close) * 100
_save_indicator(code, 'NATR', period, date, json.dumps(natr), use_adjusted)
return natr
def get_vwap(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""成交量加权平均价 VWAP(Volume Weighted Average Price)
Σ(典型价格 * 成交量) / Σ(成交量),反映过去 period 日的成交重心。
价格在 VWAP 上方说明多头占优,机构常以 VWAP 作为买卖基准价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日VWAP值(元);成交量为0或数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'VWAP', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
total_pv = 0.0
total_vol = 0.0
for k in klines:
typical_price = (k.high + k.low + k.close) / 3
total_pv += typical_price * k.volume
total_vol += k.volume
if total_vol == 0:
return None
vwap = total_pv / total_vol
_save_indicator(code, 'VWAP', period, date, json.dumps(vwap), use_adjusted)
return vwap
def get_ad(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""累积/派发线 AD(Accumulation/Distribution Line)
每日 CLV = [(收-低) - (高-收)] / (高-低),CLV * 成交量后累加。
CLV 衡量收盘价在当日高低范围中的位置,AD 持续上升表示主力在吸筹(积累)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日AD累积值(量纲:手);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'AD', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
ad_line = 0.0
for k in klines:
high_low = k.high - k.low
if high_low == 0:
clv = 0.0
else:
clv = ((k.close - k.low) - (k.high - k.close)) / high_low
ad_line += clv * k.volume
_save_indicator(code, 'AD', period, date, json.dumps(ad_line), use_adjusted)
return ad_line
def get_adosc(code: str, date: str, fast: int = 3, slow: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""AD震荡指标 ADOSC(Accumulation/Distribution Oscillator)
快周期AD - 慢周期AD,衡量资金流向的变化速度(AD的动量)。
正值且上升表示买盘在增强,负值且下降表示卖盘在增强。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fast: 快速AD的周期,默认3
slow: 慢速AD的周期,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ADOSC值;数据不足时返回 None
"""
period_key = fast * 100 + slow
cached = _get_cached_indicator(code, 'ADOSC', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
ad_fast = get_ad(code, date, fast, use_adjusted)
ad_slow = get_ad(code, date, slow, use_adjusted)
if ad_fast is None or ad_slow is None:
return None
adosc = ad_fast - ad_slow
_save_indicator(code, 'ADOSC', period_key, date, json.dumps(adosc), use_adjusted)
return adosc
def get_mfi(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""资金流量指标 MFI(Money Flow Index,0-100)
结合典型价格和成交量的动量指标,原理类似 RSI 但加入了成交量权重(量价共振)。
取值0-100,>80 视为超买,<20 视为超卖。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日MFI值(0-100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MFI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
positive_mf = 0.0
negative_mf = 0.0
for i in range(1, len(klines)):
typical_price = (klines[i].high + klines[i].low + klines[i].close) / 3
prev_tp = (klines[i-1].high + klines[i-1].low + klines[i-1].close) / 3
money_flow = typical_price * klines[i].volume
if typical_price > prev_tp:
positive_mf += money_flow
elif typical_price < prev_tp:
negative_mf += money_flow
if negative_mf == 0:
mfi = 100.0
else:
mfr = positive_mf / negative_mf
mfi = 100 - (100 / (1 + mfr))
_save_indicator(code, 'MFI', period, date, json.dumps(mfi), use_adjusted)
return mfi
def get_cmo(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""钱德动量摆动指标 CMO(Chande Momentum Oscillator,-100到100)
(上涨幅度总和 - 下跌幅度总和) / (上涨+下跌幅度总和) * 100。
取值-100到100,>50 超买,<-50 超卖,穿越0轴视为趋势转变信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日CMO值(-100到100);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'CMO', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
up_sum = 0.0
down_sum = 0.0
for i in range(1, len(klines)):
diff = klines[i].close - klines[i-1].close
if diff > 0:
up_sum += diff
else:
down_sum += abs(diff)
if up_sum + down_sum == 0:
cmo = 0.0
else:
cmo = ((up_sum - down_sum) / (up_sum + down_sum)) * 100
_save_indicator(code, 'CMO', period, date, json.dumps(cmo), use_adjusted)
return cmo
def get_rocp(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""价格变动率 ROCP(Rate of Change Percentage)
(今收盘 - N日前收盘) / N日前收盘,结果为小数而非百分比(区别于 ROC)。
例:上涨5%返回0.05,下跌3%返回-0.03。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ROCP值(小数,非百分比);数据不足或N日前收盘为0时返回 None
"""
cached = _get_cached_indicator(code, 'ROCP', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1 or klines[0].close == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
rocp = (klines[-1].close - klines[0].close) / klines[0].close
_save_indicator(code, 'ROCP', period, date, json.dumps(rocp), use_adjusted)
return rocp
def get_rocr(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""价格变动率比 ROCR(Rate of Change Ratio)
今收盘 / N日前收盘,即价格的倍数比。
=1.0 表示与N日前持平,=1.05 表示上涨5%,=0.95 表示下跌5%。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日ROCR值(倍数);数据不足或N日前收盘为0时返回 None
"""
cached = _get_cached_indicator(code, 'ROCR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1 or klines[0].close == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
rocr = klines[-1].close / klines[0].close
_save_indicator(code, 'ROCR', period, date, json.dumps(rocr), use_adjusted)
return rocr
def get_aroon(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""阿隆指标 AROON(Aroon Indicator)
AROON_UP = (period - 距最高点天数) / period * 100
AROON_DOWN = (period - 距最低点天数) / period * 100
AROON_OSC = UP - DOWN,取值-100到100,衡量趋势强度和方向变化。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'up': 上行强度(0-100), 'down': 下行强度(0-100), 'osc': 震荡值(-100到100)};
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'AROON', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
highs = [k.high for k in klines]
lows = [k.low for k in klines]
high_idx = highs.index(max(highs))
low_idx = lows.index(min(lows))
aroon_up = ((period - high_idx) / period) * 100
aroon_down = ((period - low_idx) / period) * 100
aroon_osc = aroon_up - aroon_down
aroon = {'up': aroon_up, 'down': aroon_down, 'osc': aroon_osc}
_save_indicator(code, 'AROON', period, date, json.dumps(aroon), use_adjusted)
return aroon
def get_ultosc(code: str, date: str, period1: int = 7, period2: int = 14, period3: int = 28, use_adjusted: bool = True) -> Optional[float]:
"""终极振荡器 ULTOSC(Ultimate Oscillator,0-100)
[存根函数] 理论上综合三个不同周期的买盘压力计算超买超卖,
>70 超买,<30 超卖;当前固定返回 50.0(中性值)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period1: 短周期,默认7
period2: 中周期,默认14
period3: 长周期,默认28
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 50.0(未完整实现);数据不足时返回 None
"""
period_key = period1 * 10000 + period2 * 100 + period3
cached = _get_cached_indicator(code, 'ULTOSC', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, max(period1, period2, period3) + 1)
if len(klines) < max(period1, period2, period3) + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
ultosc = 50.0
_save_indicator(code, 'ULTOSC', period_key, date, json.dumps(ultosc), use_adjusted)
return ultosc
# ============================================================
# 第四梯队:专业指标
# ============================================================
def get_dema(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""双重指数移动平均线 DEMA(Double Exponential Moving Average)
DEMA = 2 * EMA - EMA(EMA),比单重 EMA 减少滞后,对价格变化响应更快。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日DEMA值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DEMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
ema1 = get_ema(code, date, period, use_adjusted)
ema2 = get_ema(code, date, period, use_adjusted)
if ema1 is None or ema2 is None:
return None
dema = 2 * ema1 - ema2
_save_indicator(code, 'DEMA', period, date, json.dumps(dema), use_adjusted)
return dema
def get_kama(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""考夫曼自适应移动平均线 KAMA(Kaufman Adaptive Moving Average)
[存根函数] 理论上根据市场效率比率自动调整平滑系数,
趋势行情时快速跟踪,震荡行情时近乎平坦;当前直接返回最新收盘价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 效率比率计算周期,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日KAMA值(元),当前近似为最新收盘价;数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'KAMA', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
kama = klines[-1].close
_save_indicator(code, 'KAMA', period, date, json.dumps(kama), use_adjusted)
return kama
def get_midpoint(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""中点价格 MIDPOINT
过去 period 日内 (最高价极值 + 最低价极值) / 2,代表价格区间的中心位置。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回看周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 过去period日的中点价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MIDPOINT', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
highest = max(k.high for k in klines)
lowest = min(k.low for k in klines)
midpoint = (highest + lowest) / 2
_save_indicator(code, 'MIDPOINT', period, date, json.dumps(midpoint), use_adjusted)
return midpoint
def get_midprice(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""中点价格别名 MIDPRICE(等同于 MIDPOINT)
直接委托给 get_midpoint,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回看周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 过去period日的中点价格(元);数据不足时返回 None
"""
return get_midpoint(code, date, period, use_adjusted)
def get_pvi(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""正成交量指标 PVI(Positive Volume Index)
[存根函数] 理论上只在成交量增大时更新累计价格变化,反映跟风散户的行为;
当前固定返回 100.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 100.0(未完整实现);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'PVI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
pvi = 100.0
_save_indicator(code, 'PVI', period, date, json.dumps(pvi), use_adjusted)
return pvi
def get_nvi(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""负成交量指标 NVI(Negative Volume Index)
[存根函数] 理论上只在成交量缩小时更新累计价格变化,反映主力资金的悄然动向;
当前固定返回 100.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计K线根数,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 100.0(未完整实现);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'NVI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
nvi = 100.0
_save_indicator(code, 'NVI', period, date, json.dumps(nvi), use_adjusted)
return nvi
def get_ppo(code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""价格震荡百分比指标 PPO(Percentage Price Oscillator)
(快EMA - 慢EMA) / 慢EMA * 100,是 MACD 的百分比版本,
消除了股价绝对值差异,便于不同价位股票横向比较。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fast: 快线EMA周期,默认12
slow: 慢线EMA周期,默认26
signal: 信号线周期,默认9(当前近似处理)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'ppo': PPO线(%), 'signal': 信号线(%), 'histogram': 柱状图};
数据不足或慢EMA为0时返回 None
"""
period_key = fast * 10000 + slow * 100 + signal
cached = _get_cached_indicator(code, 'PPO', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ema_fast = get_ema(code, date, fast, use_adjusted)
ema_slow = get_ema(code, date, slow, use_adjusted)
if ema_fast is None or ema_slow is None or ema_slow == 0:
return None
ppo_line = ((ema_fast - ema_slow) / ema_slow) * 100
ppo = {'ppo': ppo_line, 'signal': ppo_line, 'histogram': 0}
_save_indicator(code, 'PPO', period_key, date, json.dumps(ppo), use_adjusted)
return ppo
def get_roc_r(code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""变动率比别名 ROC_R(等同于 ROCR)
直接委托给 get_rocr,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯天数,默认10
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 今收盘 / N日前收盘的比值;数据不足时返回 None
"""
return get_rocr(code, date, period, use_adjusted)
def get_stoch(code: str, date: str, fastk_period: int = 14, slowk_period: int = 3, slowd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""慢速随机指标 STOCH(Stochastic Oscillator)
基于 fastk_period 日高低价范围计算快速K,再经平滑得到慢速K和D值。
slowk>80 超买,slowk<20 超卖,K线上穿D线为买入信号,下穿为卖出信号。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fastk_period: 快速K值计算的高低价范围周期,默认14
slowk_period: 慢速K的平滑周期,默认3(当前近似处理)
slowd_period: 慢速D的平滑周期,默认3(当前近似处理)
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'slowk': 慢速K值, 'slowd': 慢速D值}(0-100);数据不足时返回 None
"""
period_key = fastk_period * 10000 + slowk_period * 100 + slowd_period
cached = _get_cached_indicator(code, 'STOCH', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, fastk_period)
if len(klines) < fastk_period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
low_n = min(k.low for k in klines)
high_n = max(k.high for k in klines)
if high_n - low_n == 0:
fastk = 50.0
else:
fastk = ((klines[-1].close - low_n) / (high_n - low_n)) * 100
stoch = {'slowk': fastk, 'slowd': fastk}
_save_indicator(code, 'STOCH', period_key, date, json.dumps(stoch), use_adjusted)
return stoch
def get_stochf(code: str, date: str, fastk_period: int = 14, fastd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""快速随机指标 STOCHF(Fast Stochastic Oscillator)
[存根函数] 与 STOCH 类似但不做慢速平滑,反应更灵敏;
当前固定返回 {'fastk': 50.0, 'fastd': 50.0}。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
fastk_period: 快速K值计算周期,默认14
fastd_period: 快速D的平滑周期,默认3
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'fastk': 快速K值, 'fastd': 快速D值};当前固定返回各50.0(未完整实现)
"""
period_key = fastk_period * 100 + fastd_period
cached = _get_cached_indicator(code, 'STOCHF', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
stochf = {'fastk': 50.0, 'fastd': 50.0}
_save_indicator(code, 'STOCHF', period_key, date, json.dumps(stochf), use_adjusted)
return stochf
def get_stochrsi(code: str, date: str, rsi_period: int = 14, stoch_period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""随机RSI STOCHRSI(Stochastic RSI)
[存根函数] 将 RSI 值再经随机指标公式处理,对超买超卖更敏感;
当前固定返回 {'fastk': 50.0, 'fastd': 50.0}。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
rsi_period: RSI计算周期,默认14
stoch_period: STOCH计算周期,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'fastk': K值, 'fastd': D值};当前固定返回各50.0(未完整实现)
"""
period_key = rsi_period * 100 + stoch_period
cached = _get_cached_indicator(code, 'STOCHRSI', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
stochrsi = {'fastk': 50.0, 'fastd': 50.0}
_save_indicator(code, 'STOCHRSI', period_key, date, json.dumps(stochrsi), use_adjusted)
return stochrsi
def get_trange(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""真实波幅别名 TRANGE(等同于 TR)
直接委托给 get_tr,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日TR值(元);数据不足时返回 None
"""
return get_tr(code, date, use_adjusted)
# ============================================================
# 第五梯队:通道和其他指标
# ============================================================
def get_ma_channel(code: str, date: str, period: int = 20, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""移动平均通道 MA_CHANNEL
以 SMA 为中轨,上下各扩展 multiplier 倍 ATR 作为上下轨,形成动态通道。
价格突破上轨可能是超强趋势信号,跌破下轨可能是弱势信号,通道宽度随波动率变化。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: SMA和ATR的计算周期,默认20
multiplier: ATR倍数,控制通道宽窄,默认2.0
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨(SMA), 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MA_CHANNEL', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
sma = get_sma(code, date, period, use_adjusted)
atr = get_atr(code, date, period, use_adjusted)
if sma is None or atr is None:
return None
channel = {
'upper': sma + multiplier * atr,
'middle': sma,
'lower': sma - multiplier * atr
}
_save_indicator(code, 'MA_CHANNEL', period, date, json.dumps(channel), use_adjusted)
return channel
def get_donchian(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""唐奇安通道 DONCHIAN(Donchian Channel)
上轨 = 过去 period 日最高价,下轨 = 过去 period 日最低价,中轨 = (上+下)/2。
价格突破上轨为买入信号,突破下轨为卖出信号(海龟交易系统的核心)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 通道计算周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨, 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DONCHIAN', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
upper = max(k.high for k in klines)
lower = min(k.low for k in klines)
middle = (upper + lower) / 2
donchian = {'upper': upper, 'middle': middle, 'lower': lower}
_save_indicator(code, 'DONCHIAN', period, date, json.dumps(donchian), use_adjusted)
return donchian
def get_keltner(code: str, date: str, ma_period: int = 20, atr_period: int = 10, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""凯尔特纳通道 KELTNER(Keltner Channel)
以 EMA 为中轨,上下各扩展 multiplier 倍 ATR。
布林带在内、凯特纳在外时称"挤压"(Squeeze),是大行情前兆的判断依据之一。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
ma_period: EMA计算周期,默认20
atr_period: ATR计算周期,默认10
multiplier: ATR倍数,控制通道宽窄,默认2.0
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'upper': 上轨, 'middle': 中轨(EMA), 'lower': 下轨}(单位:元);
数据不足时返回 None
"""
period_key = ma_period * 10000 + atr_period * 100 + int(multiplier * 10)
cached = _get_cached_indicator(code, 'KELTNER', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ema = get_ema(code, date, ma_period, use_adjusted)
atr = get_atr(code, date, atr_period, use_adjusted)
if ema is None or atr is None:
return None
keltner = {
'upper': ema + multiplier * atr,
'middle': ema,
'lower': ema - multiplier * atr
}
_save_indicator(code, 'KELTNER', period_key, date, json.dumps(keltner), use_adjusted)
return keltner
def get_bbands_width(code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""布林带宽度 BBANDS_WIDTH(%)
(上轨 - 下轨) / 中轨 * 100,衡量布林带的相对宽窄程度(标准化后的带宽)。
带宽极度收窄(挤压)通常预示即将发生大行情;带宽扩大表示行情波动加剧。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 布林带周期,默认20
std_dev: 标准差倍数,默认2
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日布林带宽度(%);数据不足或中轨为0时返回 None
"""
period_key = period * 10 + std_dev
cached = _get_cached_indicator(code, 'BBANDS_WIDTH', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
bb = get_bollinger_bands(code, date, period, std_dev, use_adjusted)
if bb is None or bb['middle'] == 0:
return None
width = ((bb['upper'] - bb['lower']) / bb['middle']) * 100
_save_indicator(code, 'BBANDS_WIDTH', period_key, date, json.dumps(width), use_adjusted)
return width
def get_bbands_pct(code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""布林带百分比位置 BBANDS_PCT(Bollinger Bands %B)
(今收 - 下轨) / (上轨 - 下轨),衡量价格在布林带中的相对位置。
=1.0 表示触及上轨(超买),=0.0 触及下轨(超卖),=0.5 在中轨;极端时可超出[0,1]范围。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 布林带周期,默认20
std_dev: 标准差倍数,默认2
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日%B值(通常0-1,极端时可超出范围);数据不足时返回 None
"""
period_key = period * 10 + std_dev
cached = _get_cached_indicator(code, 'BBANDS_PCT', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
bb = get_bollinger_bands(code, date, period, std_dev, use_adjusted)
klines = _get_klines_before_date(code, date, 1)
if bb is None or len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
if bb['upper'] - bb['lower'] == 0:
pct = 0.5
else:
pct = (klines[-1].close - bb['lower']) / (bb['upper'] - bb['lower'])
_save_indicator(code, 'BBANDS_PCT', period_key, date, json.dumps(pct), use_adjusted)
return pct
# ============================================================
# 第六梯队:其他指标
# ============================================================
def get_linearreg(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归预测值 LINEARREG
对过去 period 日收盘价做最小二乘线性回归,返回回归线在最后一天的预测值。
可理解为去噪后的"理论收盘价",与实际价格的偏差反映超买超卖程度。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日线性回归预测价(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
intercept = mean_y - slope * mean_x
linearreg = intercept + slope * (period - 1)
_save_indicator(code, 'LINEARREG', period, date, json.dumps(linearreg), use_adjusted)
return linearreg
def get_linearreg_angle(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归角度 LINEARREG_ANGLE(度)
对过去 period 日收盘价做线性回归,将斜率转换为角度(arctan)。
正角度表示上升趋势,负角度表示下降趋势,角度绝对值越大趋势越陡峭。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日回归线角度(度,-90到90);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG_ANGLE', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
angle = math.degrees(math.atan(slope))
_save_indicator(code, 'LINEARREG_ANGLE', period, date, json.dumps(angle), use_adjusted)
return angle
def get_linearreg_intercept(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归截距 LINEARREG_INTERCEPT(元)
过去 period 日收盘价线性回归直线的Y轴截距(x=0时的理论价格)。
通常配合 LINEARREG_SLOPE 和 LINEARREG 一起使用,单独使用意义不大。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 线性回归截距值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG_INTERCEPT', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
intercept = mean_y - slope * mean_x
_save_indicator(code, 'LINEARREG_INTERCEPT', period, date, json.dumps(intercept), use_adjusted)
return intercept
def get_linearreg_slope(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""线性回归斜率 LINEARREG_SLOPE(元/天)
过去 period 日收盘价线性回归直线的斜率(每交易日平均涨跌幅)。
正值表示上升趋势,负值表示下降趋势,绝对值越大趋势越强劲。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 线性回归斜率(元/天);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'LINEARREG_SLOPE', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
x = list(range(period))
mean_x = sum(x) / period
mean_y = sum(prices) / period
numerator = sum((x[i] - mean_x) * (prices[i] - mean_y) for i in range(period))
denominator = sum((x[i] - mean_x) ** 2 for i in range(period))
if denominator == 0:
return None
slope = numerator / denominator
_save_indicator(code, 'LINEARREG_SLOPE', period, date, json.dumps(slope), use_adjusted)
return slope
def get_stddev(code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""标准差 STDDEV(Standard Deviation)
过去 period 日收盘价的总体标准差,乘以 nbdev 倍数。
衡量价格的离散程度,是布林带计算的基础;nbdev=1为原始标准差,=2则与布林带2σ对应。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 计算周期,默认20
nbdev: 标准差倍数,默认1
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日标准差值(元);数据不足时返回 None
"""
period_key = period * 10 + nbdev
cached = _get_cached_indicator(code, 'STDDEV', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
mean = sum(prices) / period
variance = sum((p - mean) ** 2 for p in prices) / period
stddev = (variance ** 0.5) * nbdev
_save_indicator(code, 'STDDEV', period_key, date, json.dumps(stddev), use_adjusted)
return stddev
def get_tsf(code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""时间序列预测别名 TSF(Time Series Forecast,等同于 LINEARREG)
直接委托给 get_linearreg,两者完全等价。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回归窗口大小,默认14
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日线性回归预测价(元);数据不足时返回 None
"""
return get_linearreg(code, date, period, use_adjusted)
def get_var(code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""方差 VAR(Variance)
过去 period 日收盘价的总体方差,乘以 nbdev² 倍数。
方差 = 标准差²,是 STDDEV 的平方,衡量价格离散程度;与 STDDEV 配合使用。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 计算周期,默认20
nbdev: 倍数,默认1(实际方差再乘以nbdev²)
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日方差值(元²);数据不足时返回 None
"""
period_key = period * 10 + nbdev
cached = _get_cached_indicator(code, 'VAR', period_key, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
prices = [k.close for k in klines]
mean = sum(prices) / period
variance = sum((p - mean) ** 2 for p in prices) / period
var = variance * nbdev * nbdev
_save_indicator(code, 'VAR', period_key, date, json.dumps(var), use_adjusted)
return var
def get_correl(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""相关系数 CORREL(Pearson Correlation Coefficient)
[存根函数] 理论上计算两个价格序列的皮尔逊相关系数(-1到1);
当前仅支持单只股票(与自身序列相关,结果恒为1.0)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 1.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'CORREL', period, date, use_adjusted)
if cached is not None:
return float(cached)
correl = 1.0
_save_indicator(code, 'CORREL', period, date, json.dumps(correl), use_adjusted)
return correl
def get_beta(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""贝塔系数 BETA
[存根函数] 理论上衡量个股相对市场指数的系统性风险(>1波动大于市场,<1反之);
需要基准指数数据,当前仅支持单只股票(与自身比较,固定返回1.0)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 1.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'BETA', period, date, use_adjusted)
if cached is not None:
return float(cached)
beta = 1.0
_save_indicator(code, 'BETA', period, date, json.dumps(beta), use_adjusted)
return beta
def get_ht_dcperiod(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""希尔伯特变换-主导周期 HT_DCPERIOD
[存根函数] 通过希尔伯特变换检测价格序列当前的主导振荡周期(天数),
用于自适应指标的动态周期参数;当前固定返回 10.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 10.0(未完整实现),单位:天
"""
cached = _get_cached_indicator(code, 'HT_DCPERIOD', 1, date, use_adjusted)
if cached is not None:
return float(cached)
ht_dcperiod = 10.0
_save_indicator(code, 'HT_DCPERIOD', 1, date, json.dumps(ht_dcperiod), use_adjusted)
return ht_dcperiod
def get_ht_dcphase(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""希尔伯特变换-主导相位 HT_DCPHASE
[存根函数] 当前价格在主导周期中所处的相位角(度),
配合 HT_DCPERIOD 使用,可判断周期性行情的位置;当前固定返回 0.0。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当前固定返回 0.0(未完整实现),单位:度
"""
cached = _get_cached_indicator(code, 'HT_DCPHASE', 1, date, use_adjusted)
if cached is not None:
return float(cached)
ht_dcphase = 0.0
_save_indicator(code, 'HT_DCPHASE', 1, date, json.dumps(ht_dcphase), use_adjusted)
return ht_dcphase
def get_ht_phasor(code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""希尔伯特变换-相位分量 HT_PHASOR
[存根函数] 将价格信号分解为同相(InPhase)和正交(Quadrature)两个正交分量,
用于计算相位和主导周期;当前固定返回零值。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'inphase': 同相分量, 'quadrature': 正交分量};当前固定返回各0.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'HT_PHASOR', 1, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ht_phasor = {'inphase': 0.0, 'quadrature': 0.0}
_save_indicator(code, 'HT_PHASOR', 1, date, json.dumps(ht_phasor), use_adjusted)
return ht_phasor
def get_ht_sine(code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""希尔伯特变换-正弦波 HT_SINE
[存根函数] 基于希尔伯特变换输出正弦波和超前正弦波(领先约45度),
两线交叉预示周期性趋势转折;当前固定返回零值。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
dict: {'sine': 正弦值, 'leadsine': 超前正弦值};当前固定返回各0.0(未完整实现)
"""
cached = _get_cached_indicator(code, 'HT_SINE', 1, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ht_sine = {'sine': 0.0, 'leadsine': 0.0}
_save_indicator(code, 'HT_SINE', 1, date, json.dumps(ht_sine), use_adjusted)
return ht_sine
def get_ht_trendmode(code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""希尔伯特变换-趋势模式 HT_TRENDMODE
[存根函数] 判断当前市场处于趋势行情(1)还是周期性震荡行情(0),
用于切换使用趋势类或震荡类指标;当前固定返回1(趋势模式)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
int: 1=趋势行情,0=周期性震荡;当前固定返回 1(未完整实现)
"""
cached = _get_cached_indicator(code, 'HT_TRENDMODE', 1, date, use_adjusted)
if cached is not None:
return int(float(cached))
ht_trendmode = 1
_save_indicator(code, 'HT_TRENDMODE', 1, date, json.dumps(ht_trendmode), use_adjusted)
return ht_trendmode
# ============================================================
# 辅助指标
# ============================================================
def get_typical_price(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""典型价格 TP(Typical Price)
当日 (最高价 + 最低价 + 收盘价) / 3,是 MFI、CCI、VWAP 等指标的基础计算单元,
比单纯收盘价更能代表当日的价格重心。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日典型价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'TYPICAL', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
tp = (klines[-1].high + klines[-1].low + klines[-1].close) / 3
_save_indicator(code, 'TYPICAL', 1, date, json.dumps(tp), use_adjusted)
return tp
def get_median_price(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""中位数价格 MEDPRICE(Median Price)
当日 (最高价 + 最低价) / 2,代表当日价格区间的中心点,
不考虑收盘价位置,适合作为对称通道的基准价格。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日中位数价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MEDIAN', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
mp = (klines[-1].high + klines[-1].low) / 2
_save_indicator(code, 'MEDIAN', 1, date, json.dumps(mp), use_adjusted)
return mp
def get_weighted_close(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""加权收盘价 WCL(Weighted Close Price)
当日 (最高价 + 最低价 + 2 * 收盘价) / 4,给收盘价赋予双倍权重,
比典型价格更强调收盘价的重要性,反映市场对当日收盘位置的认可。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日加权收盘价(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'WCL', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
wcl = (klines[-1].high + klines[-1].low + 2 * klines[-1].close) / 4
_save_indicator(code, 'WCL', 1, date, json.dumps(wcl), use_adjusted)
return wcl
def get_avgp(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""平均价格 AVGP(Average Price)
当日 (开盘价 + 最高价 + 最低价 + 收盘价) / 4,四价平均,
综合反映当日全程的价格水平,可用于判断当日多空博弈的均衡点。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认True
Returns:
float: 当日四价平均价格(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'AVGP', 1, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 1)
if len(klines) == 0:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
avgp = (klines[-1].open + klines[-1].high + klines[-1].low + klines[-1].close) / 4
_save_indicator(code, 'AVGP', 1, date, json.dumps(avgp), use_adjusted)
return avgp
# ============================================================
# 新增指标
# ============================================================
def get_asi(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""振动升降指标 ASI(Accumulation Swing Index)
由 Wilder 提出,综合 open/high/low/close 计算每日摆动指数 SI,再累计求和。
ASI 上穿前高为强烈买入信号,下穿前低为卖出信号,常用于确认趋势突破的真实性。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 累计 SI 的 K 线根数,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 ASI 值;数据不足(< period+1 根)时返回 None
"""
cached = _get_cached_indicator(code, 'ASI', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
asi = 0.0
for i in range(1, len(klines)):
cur = klines[i]
prev = klines[i - 1]
A = abs(cur.high - prev.close)
B = abs(cur.low - prev.close)
C = abs(cur.high - prev.low)
D = abs(prev.close - prev.open) if prev.open else 0.0
if A >= B and A >= C:
R = A - 0.5 * B + 0.25 * D
elif B >= A and B >= C:
R = B - 0.5 * A + 0.25 * D
else:
R = C + 0.25 * D
X = (cur.close - prev.close) + 0.5 * (cur.close - cur.open) + 0.25 * (prev.close - prev.open if prev.open else 0.0)
si = (50.0 * X / R) if R != 0 else 0.0
asi += si
_save_indicator(code, 'ASI', period, date, json.dumps(asi), use_adjusted)
return asi
def get_vr(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""成交量变异率 VR(Volume Ratio)
将过去 period 日的成交量按涨/跌/平分类累加,计算多空力量之比。
VR = (上涨日成交量 + 平盘日成交量/2) / (下跌日成交量 + 平盘日成交量/2) * 100
VR 在 70~150 为盘整区,>250 超买,<70 超卖,<40 极度超卖(可能反弹)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 VR 值;数据不足或分母为 0 时返回 None
"""
cached = _get_cached_indicator(code, 'VR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
avs = bvs = cvs = 0.0
for i in range(1, len(klines)):
vol = klines[i].volume or 0.0
if klines[i].close > klines[i - 1].close:
avs += vol
elif klines[i].close < klines[i - 1].close:
bvs += vol
else:
cvs += vol
denominator = bvs + cvs / 2.0
if denominator == 0:
return None
vr = (avs + cvs / 2.0) / denominator * 100.0
_save_indicator(code, 'VR', period, date, json.dumps(vr), use_adjusted)
return vr
def get_ar(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""AR 人气指标(Atmosphere Ratio)
衡量当前市场人气,反映多空双方争夺的激烈程度。
AR = sum(High - Open) / sum(Open - Low) * 100,值越高说明买方越强势。
AR > 180 为超买区,AR < 40 为超卖区,一般在 80~120 间震荡。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 AR 值;数据不足或分母为 0 时返回 None
"""
cached = _get_cached_indicator(code, 'AR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sum_ho = sum(k.high - k.open for k in klines)
sum_ol = sum(k.open - k.low for k in klines)
if sum_ol == 0:
return None
ar = sum_ho / sum_ol * 100.0
_save_indicator(code, 'AR', period, date, json.dumps(ar), use_adjusted)
return ar
def get_br(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""BR 意愿指标(Willingness Ratio)
衡量市场买卖意愿,以前收盘价为参考基准区分主动多空力量。
BR = sum(max(0, High - prevClose)) / sum(max(0, prevClose - Low)) * 100
BR > 400 超买,BR < 40 超卖。与 AR 配合使用可判断主力意图。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 BR 值;数据不足或分母为 0 时返回 None
"""
cached = _get_cached_indicator(code, 'BR', period, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, period + 1)
if len(klines) < period + 1:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
sum_hpc = sum_pcl = 0.0
for i in range(1, len(klines)):
pc = klines[i - 1].close
sum_hpc += max(0.0, klines[i].high - pc)
sum_pcl += max(0.0, pc - klines[i].low)
if sum_pcl == 0:
return None
br = sum_hpc / sum_pcl * 100.0
_save_indicator(code, 'BR', period, date, json.dumps(br), use_adjusted)
return br
def get_brar(code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""BRAR 情绪指标(BR + AR 组合)
同时返回 AR(人气指标)和 BR(意愿指标),综合衡量市场情绪。
AR 反映多空争夺强度,BR 反映主力买卖意愿;两者配合判断超买超卖。
AR/BR 同时超买 → 市场过热;AR/BR 同时超卖 → 可能见底。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 统计周期,默认 26
use_adjusted: 是否使用后复权价格,默认 True
Returns:
dict: {'ar': AR值, 'br': BR值};任一指标无法计算时返回 None
"""
cached = _get_cached_indicator(code, 'BRAR', period, date, use_adjusted)
if cached is not None:
return json.loads(cached)
ar = get_ar(code, date, period, use_adjusted)
br = get_br(code, date, period, use_adjusted)
if ar is None or br is None:
return None
result = {'ar': ar, 'br': br}
_save_indicator(code, 'BRAR', period, date, json.dumps(result), use_adjusted)
return result
def get_dpo(code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""区间震荡线 DPO(Detrended Price Oscillator)
通过去除价格中的趋势成分,突出周期性波动。
DPO = 今收盘 - SMA(close, N) 向前偏移 (N/2 + 1) 根K线
偏移后的 SMA 反映 N/2+1 天前的均价水平,DPO > 0 表示价格高于历史均值。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 周期,默认 20
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 DPO 值(元);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'DPO', period, date, use_adjusted)
if cached is not None:
return float(cached)
shift = period // 2 + 1
needed = period + shift + 1
klines = _get_klines_before_date(code, date, needed)
if len(klines) < needed:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
# SMA 使用 shift 根前的那段 N 根 K 线
sma_klines = klines[-(period + shift):-shift]
sma_old = sum(k.close for k in sma_klines) / period
dpo = klines[-1].close - sma_old
_save_indicator(code, 'DPO', period, date, json.dumps(dpo), use_adjusted)
return dpo
def get_bbi(code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""多空指标 BBI(Bull and Bear Index)
四条均线(3/6/12/24日)的简单平均,综合短中长期趋势。
BBI = (MA3 + MA6 + MA12 + MA24) / 4
价格上穿 BBI 为买入信号,下穿为卖出信号;比单一均线更平滑稳定。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 BBI 值(元);数据不足 24 根时返回 None
"""
cached = _get_cached_indicator(code, 'BBI', 24, date, use_adjusted)
if cached is not None:
return float(cached)
klines = _get_klines_before_date(code, date, 24)
if len(klines) < 24:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
closes = [k.close for k in klines]
ma3 = sum(closes[-3:]) / 3
ma6 = sum(closes[-6:]) / 6
ma12 = sum(closes[-12:]) / 12
ma24 = sum(closes[-24:]) / 24
bbi = (ma3 + ma6 + ma12 + ma24) / 4.0
_save_indicator(code, 'BBI', 24, date, json.dumps(bbi), use_adjusted)
return bbi
def get_mass(code: str, date: str, period: int = 25, use_adjusted: bool = True) -> Optional[float]:
"""梅斯线 MASS(Mass Index)
通过计算高低价之差的两次 EMA 之比,并累加,识别价格反转信号。
EMA1 = EMA(High-Low, 9),EMA2 = EMA(EMA1, 9),MASS = sum(EMA1/EMA2, period)
MASS 超过 27 后回落至 26.5 以下,为"反转隆起",预示趋势反转。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 累加周期,默认 25
use_adjusted: 是否使用后复权价格,默认 True
Returns:
float: 当日 MASS 值;数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'MASS', period, date, use_adjusted)
if cached is not None:
return float(cached)
ema_period = 9
needed = ema_period * 2 + period
klines = _get_klines_before_date(code, date, needed)
if len(klines) < needed:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
hl = [k.high - k.low for k in klines]
ema1 = _ema_series(hl, ema_period)
ema2 = _ema_series(ema1, ema_period)
ratios = [e1 / e2 if e2 != 0 else 1.0 for e1, e2 in zip(ema1, ema2)]
if len(ratios) < period:
return None
mass = sum(ratios[-period:])
_save_indicator(code, 'MASS', period, date, json.dumps(mass), use_adjusted)
return mass
def get_xue_channel(code: str, date: str, period: int = 20, pct: float = 3.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""薛斯通道(Xue's Channel)
以 SMA 为中轨,向上/下按固定百分比扩展形成通道,类似布林带但用固定比例替代标准差。
上轨 = SMA * (1 + pct/100),下轨 = SMA * (1 - pct/100)
价格触及上轨为短期超买,触及下轨为超卖,通道内震荡则看中轨支撑/压力。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: SMA 周期,默认 20
pct: 通道偏移百分比,默认 3.0(即 ±3%)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
dict: {'upper': 上轨, 'middle': 中轨, 'lower': 下轨}(元);数据不足时返回 None
"""
period_key = period * 1000 + int(pct * 10)
cached = _get_cached_indicator(code, 'XUE', period_key, date, use_adjusted)
if cached is not None:
return json.loads(cached)
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
middle = sum(k.close for k in klines) / period
upper = middle * (1.0 + pct / 100.0)
lower = middle * (1.0 - pct / 100.0)
result = {'upper': upper, 'middle': middle, 'lower': lower}
_save_indicator(code, 'XUE', period_key, date, json.dumps(result), use_adjusted)
return result
def get_consecutive_rise(code: str, date: str, max_days: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""连涨天数
从 date 向前数,连续收盘价高于前一日的天数。
使用复权价格,避免除权除息日的价格跳空被误判为下跌。
连涨天数过长(如 >7)往往预示短期超买,注意回调风险。
Args:
code: 股票代码,如 '000001.SZ'
date: 统计截止日期,格式 'YYYY-MM-DD'
max_days: 最多回溯天数,默认 60(防止数据量过大)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 连涨天数(0 表示当日未上涨);数据不足时返回 None
"""
klines = _get_klines_before_date(code, date, max_days + 1)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
count = 0
for i in range(len(klines) - 1, 0, -1):
if klines[i].close > klines[i - 1].close:
count += 1
else:
break
return count
def get_consecutive_fall(code: str, date: str, max_days: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""连跌天数
从 date 向前数,连续收盘价低于前一日的天数。
使用复权价格,避免除权除息日的价格跳空被误判为下跌。
连跌天数过长(如 >7)往往预示短期超卖,可能存在反弹机会。
Args:
code: 股票代码,如 '000001.SZ'
date: 统计截止日期,格式 'YYYY-MM-DD'
max_days: 最多回溯天数,默认 60(防止数据量过大)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 连跌天数(0 表示当日未下跌);数据不足时返回 None
"""
klines = _get_klines_before_date(code, date, max_days + 1)
if len(klines) < 2:
return None
if use_adjusted:
adj_factors = _get_adj_factors_for_klines(klines)
klines = _adjust_klines(klines, adj_factors)
count = 0
for i in range(len(klines) - 1, 0, -1):
if klines[i].close < klines[i - 1].close:
count += 1
else:
break
return count
def get_bomb_board(code: str, date: str) -> Optional[int]:
"""炸板判断
判断指定日期该股票是否发生炸板:当日价格曾触及涨停价,但收盘时未封住(收盘价 < 涨停价)。
炸板往往意味着多头动能不足,短期筹码松动,可作为规避信号。
不使用复权价格——炸板依据的是市场实际涨停价,与复权无关。
数据来源:
- 有效交易日判断:stock_limit 表
- 炸板记录:daily_bomb_list 表(bomb_type='U',曾涨停炸板)
Args:
code: 股票代码,如 '000001.SZ'
date: 判断日期,格式 'YYYY-MM-DD'
Returns:
int: 1=当日炸板,0=当日未炸板;无数据(非交易日/数据缺失)返回 None
"""
cached = _get_cached_indicator(code, 'BOMB_BOARD', 0, date, False)
if cached is not None:
return int(cached)
# 用 stock_limit 确认是否为有效交易日
limits = query_stock_limit(ts_codes=[code], trade_date=date)
if not limits:
return None
bombs = query_daily_bomb_list(ts_codes=[code], trade_date=date, bomb_type='U')
result = 1 if bombs else 0
_save_indicator(code, 'BOMB_BOARD', 0, date, json.dumps(result), False)
return result
def get_bomb_board_count(code: str, date: str, period: int = 20) -> Optional[int]:
"""近N个交易日炸板次数
统计截至指定日期最近 period 个交易日内的炸板次数(含当日)。
炸板频繁说明股票多次冲板失败,多头信心不足,高频炸板个股追高风险较大。
不使用复权价格——炸板依据的是市场实际涨停价,与复权无关。
数据来源:daily_bomb_list 表(bomb_type='U')
Args:
code: 股票代码,如 '000001.SZ'
date: 统计截止日期,格式 'YYYY-MM-DD'
period: 回溯的交易日天数,默认 20
Returns:
int: 近 period 个交易日内炸板次数(0 表示无炸板);数据不足时返回 None
"""
cached = _get_cached_indicator(code, 'BOMB_BOARD_COUNT', period, date, False)
if cached is not None:
return int(cached)
klines = _get_klines_before_date(code, date, period)
if not klines:
return None
start_date = klines[0].date
bombs = query_daily_bomb_list(ts_codes=[code], start_date=start_date, end_date=date, bomb_type='U')
result = len(bombs)
_save_indicator(code, 'BOMB_BOARD_COUNT', period, date, json.dumps(result), False)
return result
def get_consecutive_limit_up(code: str, date: str) -> Optional[int]:
"""连续涨停天数(连板数)
返回截至指定日期连续收盘涨停的天数。直接读取 daily_limit_list.limit_streak 字段,
该字段由数据源维护,是官方连板计数,比自行计算更准确(含一字板等特殊情形)。
连板数可用于:
- 筛选高位连板股(>= 3 板往往已属强势)
- 衡量涨停板的持续性与封板质量
不使用复权价格——涨停判断基于市场实际价格。
数据来源:
- 有效交易日判断:stock_limit 表
- 连板数:daily_limit_list 表(limit_type='U',limit_streak 字段)
Args:
code: 股票代码,如 '000001.SZ'
date: 判断日期,格式 'YYYY-MM-DD'
Returns:
int: 连板数(0 表示当日未涨停);无数据(非交易日/数据缺失)返回 None
"""
cached = _get_cached_indicator(code, 'CONSEC_LIMIT_UP', 0, date, False)
if cached is not None:
return int(cached)
# 用 stock_limit 确认是否为有效交易日
limits = query_stock_limit(ts_codes=[code], trade_date=date)
if not limits:
return None
records = query_daily_limit_list(ts_codes=[code], trade_date=date, limit_type='U')
result = records[0].limit_streak if records else 0
_save_indicator(code, 'CONSEC_LIMIT_UP', 0, date, json.dumps(result), False)
return result
if __name__ == "__main__":
init_indicators_db()
print("指标数据库初始化完成")
print(f"已实现指标数量: 100+")
FILE:scripts/logger.py
def log(message:str):
print(f"[BitSoulStockLog]{message}")
FILE:scripts/metrics.py
"""
metrics.py - 性能指标计算模块
功能:
1. 回测性能指标计算
2. 风险指标计算
设计原则:
- 函数功能单一、最小粒度
- 基于权益曲线和交易记录计算
"""
from typing import List, Dict, Tuple
from track_logger import TrackLogger
def get_max_drawdown(equity_curve: List[float]) -> Tuple[float, int, int]:
"""计算最大回撤
遍历权益曲线,找出从最高峰到随后最低谷的最大下跌幅度(比例)。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
Tuple[float, int, int]:
- float: 最大回撤比例(0~1),如 0.2 表示回撤 20%
- int: 最低谷索引(回撤结束位置)
- int: 最高峰索引(回撤开始位置)
equity_curve 为空时返回 (0, 0, 0)
"""
if not equity_curve:
return 0, 0, 0
max_dd = 0
max_idx, min_idx = 0, 0
peak = equity_curve[0]
peak_idx = 0
for i, value in enumerate(equity_curve):
if value > peak:
peak, peak_idx = value, i
dd = (peak - value) / peak if peak > 0 else 0
if dd > max_dd:
max_dd, max_idx, min_idx = dd, i, peak_idx
return max_dd, max_idx, min_idx
def get_max_drawdown_pct(equity_curve: List[float]) -> float:
"""获取最大回撤比例
get_max_drawdown 的简化版本,只返回最大回撤比例。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
float: 最大回撤比例(0~1),equity_curve 为空时返回 0
"""
dd, _, _ = get_max_drawdown(equity_curve)
return dd
def get_annualized_return(total_return: float, days: int) -> float:
"""计算年化收益率
以 252 个交易日为一年,将持有期总收益折算为年化复利收益率。
公式:(1 + total_return) ^ (252 / days) - 1
Args:
total_return: 持有期总收益率(小数形式,如 0.5 表示 50%)
days: 实际持有交易日数
Returns:
float: 年化收益率(小数形式);days <= 0 时返回 0
"""
if days <= 0:
return 0
years = days / 252
if years <= 0:
return 0
return ((1 + total_return) ** (1 / years) - 1)
def get_total_return(initial_value: float, final_value: float) -> float:
"""计算总收益率
Args:
initial_value: 初始账户价值(元)
final_value: 最终账户价值(元)
Returns:
float: 总收益率(小数形式,如 0.3 表示盈利 30%);
initial_value <= 0 时返回 0
"""
if initial_value <= 0:
return 0
return (final_value - initial_value) / initial_value
def get_sharpe_ratio(equity_curve: List[float], risk_free_rate: float = 0.03) -> float:
"""计算夏普比率 (Sharpe Ratio)
衡量策略每承担一单位风险所获得的超额收益。
夏普比率越高越好,>1 为良好,>2 为优秀,<0 表示跑不赢无风险利率。
公式:(日均超额收益 / 日收益标准差) * sqrt(252)
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
risk_free_rate: 年化无风险利率(小数形式),默认 0.03(即 3%)
Returns:
float: 年化夏普比率;数据不足或波动率为 0 时返回 0
"""
if len(equity_curve) < 2:
return 0
returns = []
for i in range(1, len(equity_curve)):
if equity_curve[i-1] > 0:
ret = (equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
returns.append(ret)
if not returns:
return 0
avg_return = sum(returns) / len(returns)
variance = sum((r - avg_return) ** 2 for r in returns) / len(returns)
std_dev = variance ** 0.5
if std_dev == 0:
return 0
daily_rf = risk_free_rate / 252
sharpe = (avg_return - daily_rf) / std_dev * (252 ** 0.5)
return sharpe
def get_win_rate(trades: List[Dict]) -> float:
"""计算胜率
盈利交易笔数占总交易笔数的百分比。
Args:
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
Returns:
float: 胜率百分比(0~100);trades 为空时返回 0
"""
if not trades:
return 0
wins = sum(1 for t in trades if t.get('profit', 0) > 0)
return (wins / len(trades)) * 100
def get_profit_loss_ratio(trades: List[Dict]) -> float:
"""计算盈亏比
平均每笔盈利 / 平均每笔亏损,反映策略的风险回报结构。
盈亏比 > 1 表示平均盈利大于平均亏损,结合胜率共同衡量策略质量。
Args:
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
Returns:
float: 盈亏比;无亏损交易时若有盈利返回 inf,否则返回 0
"""
profits = [t['profit'] for t in trades if t.get('profit', 0) > 0]
losses = [abs(t['profit']) for t in trades if t.get('profit', 0) < 0]
avg_profit = sum(profits) / len(profits) if profits else 0
avg_loss = sum(losses) / len(losses) if losses else 0
if avg_loss == 0:
return float('inf') if avg_profit > 0 else 0
return avg_profit / avg_loss
def get_calmar_ratio(equity_curve: List[float], days: int) -> float:
"""计算卡尔玛比率 (Calmar Ratio)
年化收益率与最大回撤之比,衡量每单位最大亏损所对应的年化收益。
卡尔玛比率越高越好,>3 为优秀。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
days: 实际持有交易日数
Returns:
float: 卡尔玛比率;equity_curve 不足 2 条或最大回撤为 0 时返回 0
"""
if len(equity_curve) < 2:
return 0
total_return = get_total_return(equity_curve[0], equity_curve[-1])
annualized = get_annualized_return(total_return, days)
max_dd = get_max_drawdown_pct(equity_curve)
if max_dd == 0:
return 0
return annualized / max_dd
def get_volatility(equity_curve: List[float]) -> float:
"""计算年化波动率
日收益率的标准差乘以 sqrt(252),反映策略收益的波动程度。
波动率越低、夏普比率越高,策略稳定性越好。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
Returns:
float: 年化波动率(小数形式,如 0.2 表示 20%);数据不足时返回 0
"""
if len(equity_curve) < 2:
return 0
returns = []
for i in range(1, len(equity_curve)):
if equity_curve[i-1] > 0:
ret = (equity_curve[i] - equity_curve[i-1]) / equity_curve[i-1]
returns.append(ret)
if not returns:
return 0
variance = sum((r - sum(returns)/len(returns)) ** 2 for r in returns) / len(returns)
daily_vol = variance ** 0.5
return daily_vol * (252 ** 0.5)
def get_trade_stats(trades: List[Dict]) -> Dict:
"""统计交易明细汇总
Args:
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
Returns:
Dict: 包含以下字段:
- total_trades (int): 总交易笔数
- wins (int): 盈利笔数
- losses (int): 亏损笔数
- win_rate (float): 胜率百分比(0~100)
- profit_loss_ratio (float): 盈亏比
- total_profit (float): 所有盈利交易的盈利总额(元)
- total_loss (float): 所有亏损交易的亏损总额(元,负数)
- avg_profit (float): 平均每笔盈利(元)
- avg_loss (float): 平均每笔亏损(元,负数)
trades 为空时各字段均返回 0
"""
if not trades:
return {
'total_trades': 0, 'wins': 0, 'losses': 0, 'win_rate': 0,
'profit_loss_ratio': 0, 'total_profit': 0, 'total_loss': 0,
'avg_profit': 0, 'avg_loss': 0,
}
wins = [t for t in trades if t.get('profit', 0) > 0]
losses = [t for t in trades if t.get('profit', 0) < 0]
return {
'total_trades': len(trades),
'wins': len(wins),
'losses': len(losses),
'win_rate': get_win_rate(trades),
'profit_loss_ratio': get_profit_loss_ratio(trades),
'total_profit': sum(t['profit'] for t in wins),
'total_loss': sum(t['profit'] for t in losses),
'avg_profit': sum(t['profit'] for t in wins) / len(wins) if wins else 0,
'avg_loss': sum(t['profit'] for t in losses) / len(losses) if losses else 0,
}
def generate_report(equity_curve: List[float], trades: List[Dict], initial_cash: float, days: int, track_logger:TrackLogger) -> Dict:
"""生成完整的回测绩效报告
汇总权益曲线和交易记录,一次性计算所有常用绩效指标。
Args:
equity_curve: 权益曲线,每个元素为当日账户总价值(元)
trades: 交易记录列表,每条记录为 dict,需包含 'profit' 键(盈亏金额,元)
initial_cash: 初始资金(元)
days: 回测持有的实际交易日数
Returns:
Dict: 包含以下字段:
- initial_cash (float): 初始资金(元)
- final_value (float): 最终账户价值(元)
- total_return (float): 总收益率(小数)
- total_return_pct (float): 总收益率百分比
- annualized_return (float): 年化收益率(小数)
- annualized_return_pct (float): 年化收益率百分比
- max_drawdown (float): 最大回撤(小数)
- max_drawdown_pct (float): 最大回撤百分比
- sharpe_ratio (float): 夏普比率
- calmar_ratio (float): 卡尔玛比率
- volatility (float): 年化波动率(小数)
- trading_days (int): 交易日数
- trade_stats (Dict): 交易统计汇总,见 get_trade_stats 返回说明
"""
final_value = equity_curve[-1] if equity_curve else initial_cash
total_return = get_total_return(initial_cash, final_value)
return {
'initial_cash': initial_cash,
'final_value': final_value,
'total_return': total_return,
'total_return_pct': total_return * 100,
'annualized_return': get_annualized_return(total_return, days),
'annualized_return_pct': get_annualized_return(total_return, days) * 100,
'max_drawdown': get_max_drawdown_pct(equity_curve),
'max_drawdown_pct': get_max_drawdown_pct(equity_curve) * 100,
'sharpe_ratio': get_sharpe_ratio(equity_curve),
'calmar_ratio': get_calmar_ratio(equity_curve, days),
'volatility': get_volatility(equity_curve),
'trading_days': days,
'trade_stats': get_trade_stats(trades),
}
FILE:scripts/moe_signal.py
"""
moe_signal.py — MoE 混合专家买卖时机分析 + 遗传算法权重训练
功能一:分析单只股票当前买卖时机
python moe_signal.py --code 000001.SZ [--date 2026-03-01]
python moe_signal.py analyze --code 000001.SZ
功能二:跑回测训练最优权重(遗传算法,目标:最大化总收益)
python moe_signal.py train --start-date 2025-09-01 --end-date 2026-03-01
python moe_signal.py train # 默认最近半年
输出 analyze:JSON 格式的综合评分和 BUY/SELL/HOLD 建议
输出 train:优化后的权重写入 moe_weights.json
"""
import sys
import os
import json
import argparse
import random
import copy
from datetime import datetime, timedelta
from typing import Optional, Dict, List, Any, Tuple
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import numpy as np
import pandas as pd
from data_fetcher import (
query_daily_basic, query_stock_limit, query_top_list,
query_daily_kline, query_stock_basic,
)
import indicators as ind
from indicators import init_indicators_db
import config
# ─────────────────────────────────────────────────────────────────────────────
# 权重配置文件
# ─────────────────────────────────────────────────────────────────────────────
_WEIGHTS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'moe_weights.json')
_DEFAULT_WEIGHTS: Dict[str, Any] = {
'expert_weights': {'technical': 0.35, 'alpha': 0.35, 'fundamental': 0.15, 'behavior': 0.15},
'signal_thresholds': {'buy': 0.65, 'sell': 0.35},
}
def load_weights() -> Dict[str, Any]:
"""加载权重配置文件,文件不存在则返回默认值。"""
if os.path.exists(_WEIGHTS_PATH):
try:
with open(_WEIGHTS_PATH, 'r', encoding='utf-8') as f:
data = json.load(f)
for k, v in _DEFAULT_WEIGHTS.items():
if k not in data:
data[k] = copy.deepcopy(v)
return data
except Exception:
pass
return copy.deepcopy(_DEFAULT_WEIGHTS)
def save_weights(weights: Dict[str, Any], train_period: Optional[str] = None) -> None:
"""保存权重配置文件。"""
weights['_version'] = weights.get('_version', 1)
weights['_trained_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if train_period:
weights['_train_period'] = train_period
with open(_WEIGHTS_PATH, 'w', encoding='utf-8') as f:
json.dump(weights, f, ensure_ascii=False, indent=2)
print(f'[MoE] 权重已保存到 {_WEIGHTS_PATH}')
# ─────────────────────────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────────────────────────
def _clamp(v: float, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, v))
def _linear(v: float, lo: float, hi: float, reverse: bool = False) -> float:
"""将 v 线性映射到 [0,1],lo→1(看多), hi→0(看空);reverse=True 时反转。"""
if hi == lo:
return 0.5
ratio = (v - lo) / (hi - lo)
score = _clamp(1.0 - ratio)
return score if not reverse else 1.0 - score
def _get_close(code: str, date: str) -> Optional[float]:
"""获取指定日期或之前最近一个交易日的收盘价。"""
klines = query_daily_kline(codes=[code], end_date=date, limit=1, order_by="date DESC")
if klines:
return klines[0].close
return None
def _prev_date(date: str, n: int = 1) -> str:
"""往前推 n 个自然日(近似交易日偏移)。"""
return (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=n * 2)).strftime('%Y-%m-%d')
def _weighted_mean(scores: Dict[str, float], weights: Dict[str, float]) -> float:
"""按权重计算加权平均,权重自动归一化。"""
total_w = 0.0
total_v = 0.0
for k, v in scores.items():
w = weights.get(k, 1.0)
total_w += w
total_v += w * v
return (total_v / total_w) if total_w > 0 else 0.5
# ─────────────────────────────────────────────────────────────────────────────
# Expert 1:技术指标专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_tech(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Dict[str, Any]:
close = _get_close(code, date)
scores: Dict[str, float] = {}
# ── 趋势均线类 ────────────────────────────────────────────────────────────
for period, fname in [(5, 'sma5'), (10, 'sma10'), (20, 'sma20'), (60, 'sma60')]:
v = ind.get_sma(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
for period, fname in [(5, 'ema5'), (12, 'ema12'), (20, 'ema20'), (26, 'ema26')]:
v = ind.get_ema(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
ema5 = ind.get_ema(code, date, 5)
ema20 = ind.get_ema(code, date, 20)
if ema5 is not None and ema20 is not None:
scores['ema_cross'] = 1.0 if ema5 > ema20 else 0.0
for period, fname in [(20, 'wma20')]:
v = ind.get_wma(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
for period, fname in [(20, 'tema20')]:
v = ind.get_tema(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
for period, fname in [(20, 'dema20')]:
v = ind.get_dema(code, date, period)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
v = ind.get_kama(code, date, 10)
if v is not None and close is not None:
scores['kama'] = 1.0 if close > v else 0.0
v = ind.get_bbi(code, date)
if v is not None and close is not None:
scores['bbi'] = 1.0 if close > v else 0.0
v = ind.get_trix(code, date, 12)
if v is not None:
scores['trix'] = 1.0 if v > 0 else 0.0
dmi = ind.get_dmi(code, date, 14)
if dmi is not None:
adx = dmi.get('adx', 0) or 0
pdi = dmi.get('pdi', 0) or 0
mdi = dmi.get('mdi', 0) or 0
scores['dmi'] = 1.0 if (adx > 25 and pdi > mdi) else (0.0 if (adx > 25 and mdi > pdi) else 0.5)
sar_r = ind.get_sar(code, date)
if sar_r is not None and close is not None:
sar_v = sar_r.get('sar')
if sar_v is not None:
scores['sar'] = 1.0 if close > sar_v else 0.0
v = ind.get_linearreg_slope(code, date, 14)
if v is not None:
scores['linearreg_slope'] = 1.0 if v > 0 else 0.0
v = ind.get_linearreg(code, date, 14)
if v is not None and close is not None:
scores['linearreg'] = 1.0 if v > close else 0.0
v = ind.get_linearreg_angle(code, date, 14)
if v is not None:
scores['linearreg_angle'] = 1.0 if v > 0 else 0.0
v = ind.get_linearreg_intercept(code, date, 14)
if v is not None and close is not None:
scores['linearreg_intercept'] = 1.0 if close > v else 0.0
aroon = ind.get_aroon(code, date, 14)
if aroon is not None:
au = aroon.get('aroon_up', 50) or 50
ad = aroon.get('aroon_down', 50) or 50
scores['aroon'] = 1.0 if au > ad else (0.0 if ad > au else 0.5)
v = ind.get_tsf(code, date, 14)
if v is not None and close is not None:
scores['tsf'] = 1.0 if v > close else 0.0
v = ind.get_ht_trendmode(code, date)
if v is not None:
scores['ht_trendmode'] = 1.0 if v == 1 else 0.5
v = ind.get_ht_dcphase(code, date)
if v is not None:
scores['ht_dcphase'] = 1.0 if (0 <= v % 360 <= 180) else 0.0
ht_sine = ind.get_ht_sine(code, date)
if ht_sine is not None:
sine_v = ht_sine.get('sine', 0) or 0
scores['ht_sine'] = _clamp((sine_v + 1) / 2)
# ── 动量振荡类 ────────────────────────────────────────────────────────────
v = ind.get_rsi(code, date, 14)
if v is not None:
scores['rsi14'] = _linear(v, 70, 30)
v = ind.get_rsi(code, date, 6)
if v is not None:
scores['rsi6'] = _linear(v, 70, 30)
v = ind.get_cci(code, date, 20)
if v is not None:
scores['cci'] = _linear(v, 100, -100)
for period, fname in [(10, 'mom10'), (20, 'mom20')]:
v = ind.get_mom(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 0 else 0.0
for period, fname in [(10, 'roc10')]:
v = ind.get_roc(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 0 else 0.0
for period, fname in [(10, 'rocp10')]:
v = ind.get_rocp(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 0 else 0.0
for period, fname in [(10, 'rocr10')]:
v = ind.get_rocr(code, date, period)
if v is not None:
scores[fname] = 1.0 if v > 1.0 else 0.0
v = ind.get_roc_r(code, date, 10)
if v is not None:
scores['roc_r'] = 1.0 if v > 0 else 0.0
v = ind.get_williams_r(code, date, 14)
if v is not None:
scores['willr'] = _linear(v, -20, -80)
v = ind.get_cmo(code, date, 14)
if v is not None:
scores['cmo'] = 1.0 if v > 0 else 0.0
v = ind.get_bias(code, date, 20)
if v is not None:
if v < -10:
scores['bias'] = 1.0
elif v > 10:
scores['bias'] = 0.0
else:
scores['bias'] = _clamp(0.5 - v / 20)
v = ind.get_psycho(code, date, 12)
if v is not None:
scores['psycho'] = _clamp(1.0 - v / 100)
v = ind.get_dpo(code, date, 20)
if v is not None:
scores['dpo'] = 1.0 if v > 0 else 0.0
v = ind.get_mass(code, date, 25)
if v is not None:
scores['mass'] = 0.3 if v > 27 else 0.5
# ── KDJ / Stoch 类 ────────────────────────────────────────────────────────
kdj = ind.get_kdj(code, date)
if kdj is not None:
j = kdj.get('j', 50) or 50
scores['kdj_j'] = _linear(j, 80, 20)
k = kdj.get('k', 50) or 50
d = kdj.get('d', 50) or 50
scores['kdj_kd'] = 1.0 if k > d else 0.0
stoch = ind.get_stoch(code, date)
if stoch is not None:
sk = stoch.get('slowk', 50) or 50
scores['stoch_k'] = _linear(sk, 80, 20)
stochf = ind.get_stochf(code, date)
if stochf is not None:
fk = stochf.get('fastk', 50) or 50
scores['stochf_k'] = _linear(fk, 80, 20)
stochrsi = ind.get_stochrsi(code, date)
if stochrsi is not None:
sk = stochrsi.get('fastk', 50) or 50
scores['stochrsi'] = _linear(sk, 80, 20)
v = ind.get_ultosc(code, date)
if v is not None:
scores['ultosc'] = _linear(v, 70, 30)
# ── MACD 类 ───────────────────────────────────────────────────────────────
macd = ind.get_macd(code, date)
if macd is not None:
hist = macd.get('histogram', 0) or 0
scores['macd_hist'] = 1.0 if hist > 0 else 0.0
macd_v = macd.get('macd', 0) or 0
sig_v = macd.get('signal', 0) or 0
scores['macd_cross'] = 1.0 if macd_v > sig_v else 0.0
ppo = ind.get_ppo(code, date)
if ppo is not None:
hist = ppo.get('histogram', 0) or 0
scores['ppo_hist'] = 1.0 if hist > 0 else 0.0
v = ind.get_adosc(code, date)
if v is not None:
scores['adosc'] = 1.0 if v > 0 else 0.0
# ── 成交量类 ──────────────────────────────────────────────────────────────
v = ind.get_obv(code, date, 20)
if v is not None:
v_prev = ind.get_obv(code, _prev_date(date, 5), 20)
if v_prev is not None:
scores['obv'] = 1.0 if v > v_prev else 0.0
v = ind.get_ad(code, date, 20)
if v is not None:
v_prev = ind.get_ad(code, _prev_date(date, 5), 20)
if v_prev is not None:
scores['ad'] = 1.0 if v > v_prev else 0.0
v = ind.get_mfi(code, date, 14)
if v is not None:
scores['mfi'] = _linear(v, 80, 20)
v = ind.get_vwap(code, date, 20)
if v is not None and close is not None:
scores['vwap'] = 1.0 if close > v else 0.0
vol_d = ind.get_volume(code, date, 20)
if vol_d is not None:
ratio = vol_d.get('ratio', 1.0) or 1.0
scores['volume_ratio'] = 1.0 if ratio > 1.5 else (0.3 if ratio < 0.5 else 0.5)
v = ind.get_vr(code, date, 26)
if v is not None:
scores['vr'] = _linear(v, 180, 70)
v = ind.get_pvi(code, date, 20)
if v is not None:
pvi_ma = ind.get_sma(code, date, 20)
if pvi_ma is not None:
scores['pvi'] = 1.0 if v > pvi_ma else 0.0
v = ind.get_nvi(code, date, 20)
if v is not None:
nvi_ma = ind.get_sma(code, date, 20)
if nvi_ma is not None:
scores['nvi'] = 1.0 if v > nvi_ma else 0.0
v = ind.get_ar(code, date, 26)
if v is not None:
scores['ar'] = _linear(v, 150, 50)
v = ind.get_br(code, date, 26)
if v is not None:
scores['br'] = _linear(v, 200, 50)
brar = ind.get_brar(code, date, 26)
if brar is not None:
ar_v = brar.get('ar', 100) or 100
br_v = brar.get('br', 100) or 100
scores['brar'] = _clamp(((200 - ar_v) / 300 + (200 - br_v) / 400) / 2)
v = ind.get_asi(code, date, 26)
if v is not None:
scores['asi'] = 1.0 if v > 0 else 0.0
# ── 通道类 ────────────────────────────────────────────────────────────────
bb = ind.get_bollinger_bands(code, date, 20, 2)
if bb is not None and close is not None:
upper = bb.get('upper', close) or close
lower = bb.get('lower', close) or close
if upper != lower:
scores['bb_pos'] = _clamp((close - lower) / (upper - lower))
bb_score = _clamp((close - lower) / (upper - lower))
scores['bb_signal'] = 1.0 - bb_score if bb_score > 0.8 else (1.0 if bb_score < 0.2 else 0.5)
v = ind.get_bbands_pct(code, date, 20, 2)
if v is not None:
scores['bbands_pct'] = _linear(v, 1.0, 0.0)
v = ind.get_bbands_width(code, date, 20, 2)
if v is not None:
scores['bbands_width'] = 0.5
ma_ch = ind.get_ma_channel(code, date, 20, 2.0)
if ma_ch is not None and close is not None:
upper = ma_ch.get('upper', close) or close
lower = ma_ch.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['ma_channel'] = 1.0 if pos < 0.2 else (0.0 if pos > 0.8 else 0.5)
dc = ind.get_donchian(code, date, 20)
if dc is not None and close is not None:
upper = dc.get('upper', close) or close
lower = dc.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['donchian'] = 1.0 - pos
kelt = ind.get_keltner(code, date, 20, 10, 2.0)
if kelt is not None and close is not None:
upper = kelt.get('upper', close) or close
lower = kelt.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['keltner'] = 1.0 if pos < 0.2 else (0.0 if pos > 0.8 else 0.5)
xue = ind.get_xue_channel(code, date, 20, 3.0)
if xue is not None and close is not None:
upper = xue.get('upper', close) or close
lower = xue.get('lower', close) or close
if upper != lower:
pos = _clamp((close - lower) / (upper - lower))
scores['xue_channel'] = 1.0 - pos
v = ind.get_midpoint(code, date, 14)
if v is not None and close is not None:
scores['midpoint'] = 1.0 if close > v else 0.0
v = ind.get_midprice(code, date, 14)
if v is not None and close is not None:
scores['midprice'] = 1.0 if close > v else 0.0
for key in ['atr', 'natr', 'tr', 'trange', 'stddev', 'var', 'correl', 'beta', 'ht_dcperiod']:
scores[key] = 0.5
for fname, fn in [('typical', ind.get_typical_price), ('median', ind.get_median_price),
('wclose', ind.get_weighted_close), ('avgp', ind.get_avgp)]:
v = fn(code, date)
if v is not None and close is not None:
scores[fname] = 1.0 if close > v else 0.0
ht_ph = ind.get_ht_phasor(code, date)
if ht_ph is not None:
inphase = ht_ph.get('inphase', 0) or 0
scores['ht_phasor'] = 1.0 if inphase > 0 else 0.0
v = ind.get_consecutive_rise(code, date, 60)
if v is not None:
scores['consec_rise'] = _clamp(v / 10)
v = ind.get_consecutive_fall(code, date, 60)
if v is not None:
scores['consec_fall'] = _clamp(v / 5)
v = ind.get_bomb_board(code, date)
if v is not None:
scores['bomb_board'] = 0.0 if v > 0 else 0.5
v = ind.get_bomb_board_count(code, date, 20)
if v is not None:
scores['bomb_board_count'] = _clamp(1.0 - v * 0.2)
v = ind.get_consecutive_limit_up(code, date)
if v is not None:
scores['consec_limit_up'] = _clamp(v * 0.2)
if not scores:
return {'score': 0.5, 'valid_count': 0, 'total_count': 80, 'details': {}}
w = weights or {}
final = _weighted_mean(scores, w)
return {
'score': round(final, 4),
'valid_count': len(scores),
'total_count': 80,
'details': {k: round(v, 4) for k, v in list(scores.items())[:20]},
'_raw_scores': scores,
}
# ─────────────────────────────────────────────────────────────────────────────
# Expert 2:Alpha 因子专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_alpha(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Optional[Dict[str, Any]]:
try:
from formulaicAlphas.alpha101 import Alpha101
from formulaicAlphas.data_loader import AlphaDataLoader
except ImportError:
return None
end_date = date
start_date = (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=120)).strftime('%Y-%m-%d')
all_codes = [b.ts_code for b in query_stock_basic() if b.ts_code]
all_codes = random.sample(all_codes, min(200, len(all_codes)))
if code not in all_codes:
all_codes.insert(0, code)
loader = AlphaDataLoader()
data = loader.load(codes=all_codes, start_date=start_date, end_date=end_date)
if not data or 'close' not in data:
return None
close_df = data['close']
if code not in close_df.columns:
return None
target_date = pd.Timestamp(date)
available_dates = close_df.index[close_df.index <= target_date]
if len(available_dates) == 0:
return None
actual_date = available_dates[-1]
alpha_obj = Alpha101(data)
alpha_scores: Dict[str, float] = {}
for i in range(1, 102):
fname = f'alpha{i:03d}'
try:
fn = getattr(alpha_obj, fname, None)
if fn is None:
continue
result = fn()
if result is None or not isinstance(result, pd.DataFrame):
continue
if actual_date not in result.index or code not in result.columns:
continue
val = result.loc[actual_date, code]
if pd.isna(val):
continue
row = result.loc[actual_date].dropna()
if len(row) < 5:
continue
rank = float((row < val).sum()) / len(row)
alpha_scores[fname] = rank
except Exception:
continue
if not alpha_scores:
return None
w = weights or {}
final = _weighted_mean(alpha_scores, w)
return {
'score': round(final, 4),
'valid_count': len(alpha_scores),
'total_count': 101,
'details': {k: round(v, 4) for k, v in list(alpha_scores.items())[:10]},
'_raw_scores': alpha_scores,
}
# ─────────────────────────────────────────────────────────────────────────────
# Expert 3:基本面专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_fundamental(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Optional[Dict[str, Any]]:
basics = query_daily_basic(ts_codes=[code], end_date=date, limit=1, order_by="trade_date DESC")
if not basics:
return None
b = basics[0]
scores: Dict[str, float] = {}
pe = b.pe_ttm
if pe is not None:
if pe <= 0:
scores['pe_ttm'] = 0.1
elif pe < 30:
scores['pe_ttm'] = 1.0
elif pe < 60:
scores['pe_ttm'] = 0.5
else:
scores['pe_ttm'] = 0.2
pb = b.pb
if pb is not None and pb > 0:
if pb < 3:
scores['pb'] = 1.0
elif pb < 6:
scores['pb'] = 0.5
else:
scores['pb'] = 0.2
tr = b.turnover_rate
if tr is not None:
if 1 <= tr <= 5:
scores['turnover_rate'] = 1.0
elif 5 < tr <= 10:
scores['turnover_rate'] = 0.7
elif tr < 1:
scores['turnover_rate'] = 0.4
else:
scores['turnover_rate'] = 0.3
vr = b.volume_ratio
if vr is not None:
if 1 <= vr <= 2:
scores['volume_ratio'] = 1.0
elif 2 < vr <= 3:
scores['volume_ratio'] = 0.7
elif vr < 0.5:
scores['volume_ratio'] = 0.3
else:
scores['volume_ratio'] = 0.4
ps = b.ps_ttm
if ps is not None and ps > 0:
if ps < 1:
scores['ps_ttm'] = 1.0
elif ps < 5:
scores['ps_ttm'] = 0.7
elif ps < 10:
scores['ps_ttm'] = 0.5
else:
scores['ps_ttm'] = 0.3
if not scores:
return None
w = weights or {}
final = _weighted_mean(scores, w)
return {
'score': round(final, 4),
'details': {'pe_ttm': b.pe_ttm, 'pb': b.pb, 'turnover_rate': b.turnover_rate,
'volume_ratio': b.volume_ratio, 'ps_ttm': b.ps_ttm, 'scores': scores},
'_raw_scores': scores,
}
# ─────────────────────────────────────────────────────────────────────────────
# Expert 4:量价行为专家
# ─────────────────────────────────────────────────────────────────────────────
def _score_behavior(code: str, date: str, weights: Optional[Dict[str, float]] = None) -> Optional[Dict[str, Any]]:
start_d = (datetime.strptime(date, '%Y-%m-%d') - timedelta(days=30)).strftime('%Y-%m-%d')
details: Dict[str, Any] = {}
named_signals: Dict[str, float] = {}
limits = query_stock_limit(ts_codes=[code], start_date=start_d, end_date=date)
limit_up_5d = 0
limit_down_5d = 0
klines_5d = query_daily_kline(codes=[code], end_date=date, limit=5, order_by="date DESC")
dates_5d = {k.date for k in klines_5d}
for lim in limits:
if lim.trade_date in dates_5d:
if hasattr(lim, 'limit_up') and lim.limit_up:
limit_up_5d += 1
if hasattr(lim, 'limit_down') and lim.limit_down:
limit_down_5d += 1
details['limit_up_5d'] = limit_up_5d
details['limit_down_5d'] = limit_down_5d
named_signals['limit_score'] = _clamp(0.5 + limit_up_5d * 0.15 - limit_down_5d * 0.15)
consec_up = ind.get_consecutive_limit_up(code, date)
if consec_up is not None and consec_up > 0:
details['consecutive_limit_up'] = consec_up
named_signals['consecutive_limit_up'] = _clamp(consec_up * 0.2)
bomb_cnt = ind.get_bomb_board_count(code, date, 10)
if bomb_cnt is not None:
details['bomb_board_10d'] = bomb_cnt
named_signals['bomb_board'] = _clamp(1.0 - bomb_cnt * 0.15)
top_lists = query_top_list(ts_codes=[code], start_date=start_d, end_date=date)
if top_lists:
net_buy = sum((getattr(tl, 'net_buy', 0) or 0) for tl in top_lists)
details['top_list_net_buy'] = net_buy
named_signals['top_list'] = _clamp(0.5 + (0.3 if net_buy > 0 else (-0.3 if net_buy < 0 else 0)))
if klines_5d and len(klines_5d) >= 2:
latest_close = klines_5d[0].close
oldest_close = klines_5d[-1].pre_close if hasattr(klines_5d[-1], 'pre_close') else klines_5d[-1].close
if oldest_close and oldest_close > 0:
chg_5d = (latest_close - oldest_close) / oldest_close * 100
details['pct_chg_5d'] = round(chg_5d, 2)
named_signals['pct_chg_5d'] = _linear(chg_5d, 20, -20)
if not named_signals:
return None
w = weights or {}
final = _weighted_mean(named_signals, w)
return {
'score': round(final, 4),
'details': details,
'_raw_scores': named_signals,
}
# ─────────────────────────────────────────────────────────────────────────────
# 门控网络 + 汇总
# ─────────────────────────────────────────────────────────────────────────────
def _compute_final_score(
tech_result: Optional[Dict],
alpha_result: Optional[Dict],
fund_result: Optional[Dict],
behav_result: Optional[Dict],
expert_weights: Dict[str, float],
buy_thresh: float = 0.65,
sell_thresh: float = 0.35,
) -> Dict[str, Any]:
"""门控网络:根据有效专家和权重计算最终评分。"""
expert_results = {
'technical': tech_result,
'alpha': alpha_result,
'fundamental': fund_result,
'behavior': behav_result,
}
active = {k: v for k, v in expert_results.items() if v is not None}
if not active:
return {'error': '所有专家数据不足'}
total_w = sum(expert_weights.get(k, 0.0) for k in active)
if total_w <= 0:
total_w = len(active)
norm_weights = {k: 1.0 / total_w for k in active}
else:
norm_weights = {k: expert_weights.get(k, 0.0) / total_w for k in active}
final_score = sum(norm_weights[k] * active[k]['score'] for k in active)
final_score = round(float(final_score), 4)
signal = 'BUY' if final_score >= buy_thresh else ('SELL' if final_score <= sell_thresh else 'HOLD')
score_vals = [active[k]['score'] for k in active]
variance = float(np.var(score_vals)) if len(score_vals) > 1 else 0.0
confidence = '高' if variance < 0.005 else ('中' if variance < 0.020 else '低')
reasons = []
for k, v in active.items():
s = v['score']
label = {'technical': '技术面', 'alpha': 'Alpha因子', 'fundamental': '基本面', 'behavior': '量价行为'}[k]
if s >= buy_thresh:
reasons.append(f'{label}看多({s:.2f})')
elif s <= sell_thresh:
reasons.append(f'{label}看空({s:.2f})')
else:
reasons.append(f'{label}中性({s:.2f})')
experts_out = {}
for k in ['technical', 'alpha', 'fundamental', 'behavior']:
if k in active:
entry = {'score': active[k]['score'], 'weight': round(norm_weights[k], 4)}
if 'valid_count' in active[k]:
entry['valid_count'] = active[k]['valid_count']
entry['total_count'] = active[k]['total_count']
if 'details' in active[k]:
entry['details'] = active[k]['details']
experts_out[k] = entry
else:
experts_out[k] = {'score': None, 'weight': 0.0, 'note': '数据补充中,敬请期待'}
return {
'final_score': final_score,
'signal': signal,
'confidence': confidence,
'experts': experts_out,
'reason': ','.join(reasons),
}
def analyze(code: str, date: str) -> Dict[str, Any]:
"""
功能一:基于当前价格结合 MoE 做买卖决策。
从 moe_weights.json 加载权重(若不存在则使用默认值),
运行 4 个专家打分,门控网络汇总,输出 BUY/SELL/HOLD。
"""
print(f'[MoE] 正在分析 {code} 日期={date}')
# 退市检查
try:
basics = query_stock_basic(ts_codes=[code])
if basics:
b = basics[0]
if getattr(b, 'list_status', None) == 'D':
delist_date = getattr(b, 'delist_date', None) or '未知'
name = getattr(b, 'name', code)
print(f'[MoE] ⚠️ {code}({name})已于 {delist_date} 退市,以下分析仅供参考,该股票已无法交易。')
except Exception:
pass
weights = load_weights()
expert_w = weights.get('expert_weights', _DEFAULT_WEIGHTS['expert_weights'])
tech_w = weights.get('technical', {})
alpha_w = weights.get('alpha', {})
fund_w = weights.get('fundamental', {})
behav_w = weights.get('behavior', {})
buy_thresh = weights.get('signal_thresholds', {}).get('buy', 0.65)
sell_thresh = weights.get('signal_thresholds', {}).get('sell', 0.35)
_setup_analyze_cache(code, date)
try:
print('[MoE] Expert 1: 技术指标...')
tech = _score_tech(code, date, tech_w)
finally:
_teardown_analyze_cache()
print('[MoE] Expert 2: Alpha因子...')
alpha = _score_alpha(code, date, alpha_w)
print('[MoE] Expert 3: 基本面...')
fund = _score_fundamental(code, date, fund_w)
print('[MoE] Expert 4: 量价行为...')
behav = _score_behavior(code, date, behav_w)
result = _compute_final_score(tech, alpha, fund, behav, expert_w, buy_thresh, sell_thresh)
result['code'] = code
result['date'] = date
# 退市标记写入返回值
try:
basics = query_stock_basic(ts_codes=[code])
if basics:
b = basics[0]
if getattr(b, 'list_status', None) == 'D':
result['delisted'] = True
result['delist_date'] = getattr(b, 'delist_date', None) or '未知'
result['delist_warning'] = (
f"⚠️ {code}({getattr(b, 'name', code)})已于 {result['delist_date']} 退市,"
f"该股票已无法交易,以下分析仅供参考。"
)
except Exception:
pass
return result
# ─────────────────────────────────────────────────────────────────────────────
# 功能二:遗传算法权重训练
# ─────────────────────────────────────────────────────────────────────────────
def _get_trading_dates(start_date: str, end_date: str) -> List[str]:
"""从数据库获取月度采样日期列表(用000001.SZ取交易日历,每月取第一个交易日)。"""
klines = query_daily_kline(codes=['000001.SZ'], start_date=start_date, end_date=end_date,
order_by='date ASC', limit=None)
dates = sorted(set(k.date for k in klines))
monthly: Dict[str, str] = {}
for d in dates:
ym = d[:7]
if ym not in monthly:
monthly[ym] = d
return list(monthly.values())
# ── 训练缓存结构 ──────────────────────────────────────────────────────────────
# cache[(code, date)] = {
# 'tech_raw': Dict[str, float], # 技术指标原始分
# 'fund_raw': Dict[str, float], # 基本面原始分
# 'behav_raw': Dict[str, float], # 行为原始分
# 'future_ret': float, # 5日后实际收益率(用于适应度)
# }
_TRAIN_CACHE: Dict[Tuple[str, str], Dict] = {}
# ── analyze() 单次调用级指标内存缓存(消除 _score_tech 的 80+ 次重复 DB 查询)──────
from sqlalchemy import text as _sa_text
from data_fetcher import getEngine as _getEngine
_TECH_MEM_CACHE: Dict[Tuple, Optional[str]] = {}
_orig_get_cached_fn = None
_orig_save_indicator_fn = None
def _setup_analyze_cache(code: str, date: str) -> None:
"""在 analyze() 开始时调用:一次 SQL 批量读取该 code+date 的所有缓存指标到内存,
并 monkey-patch ind 模块的缓存函数,让 _score_tech 的 80+ 次查询走内存 dict 而非 DB。"""
global _TECH_MEM_CACHE, _orig_get_cached_fn, _orig_save_indicator_fn
_TECH_MEM_CACHE = {}
try:
with _getEngine().connect() as conn:
rows = conn.execute(_sa_text(
"SELECT indicator_type, period, use_adjusted, value "
"FROM cached_indicators WHERE code=:code AND date=:date"
), {"code": code, "date": date}).fetchall()
for r in rows:
_TECH_MEM_CACHE[(code, r[0], r[1], r[2], date)] = r[3]
except Exception:
pass # 失败时退化到原始 DB 查询,无副作用
_orig_get_cached_fn = ind._get_cached_indicator
_orig_save_indicator_fn = ind._save_indicator
def _patched_get(c, itype, period, idate, use_adj=True):
k = (c, itype, period, 1 if use_adj else 0, idate)
if k in _TECH_MEM_CACHE:
return _TECH_MEM_CACHE[k]
return _orig_get_cached_fn(c, itype, period, idate, use_adj)
def _patched_save(c, itype, period, idate, value, use_adj=True):
k = (c, itype, period, 1 if use_adj else 0, idate)
_TECH_MEM_CACHE[k] = value
_orig_save_indicator_fn(c, itype, period, idate, value, use_adj)
ind._get_cached_indicator = _patched_get
ind._save_indicator = _patched_save
def _teardown_analyze_cache() -> None:
"""在 analyze() 结束时调用:恢复 ind 模块原始缓存函数,清空内存缓存。"""
global _TECH_MEM_CACHE
if _orig_get_cached_fn is not None:
ind._get_cached_indicator = _orig_get_cached_fn
if _orig_save_indicator_fn is not None:
ind._save_indicator = _orig_save_indicator_fn
_TECH_MEM_CACHE = {}
def _precompute_cache(train_codes: List[str], train_dates: List[str], hold_days: int = 5) -> None:
"""
预计算阶段:一次性算好所有 (code, date) 的原始指标分和未来收益,
存入内存缓存。遗传算法迭代时只做纯内存加权,不再查DB。
"""
global _TRAIN_CACHE
_TRAIN_CACHE = {}
total = len(train_codes) * len(train_dates)
done = 0
print(f'[预计算] 共 {len(train_codes)} 只股票 × {len(train_dates)} 个日期 = {total} 个样本点', flush=True)
# 批量预取未来收益:查询每只股票在训练区间内的K线,避免逐条查DB
# key: code -> {date -> (buy_price, sell_price)}
price_map: Dict[str, Dict[str, Tuple[float, float]]] = {}
print('[预计算] 批量加载K线价格...', flush=True)
start_dt = train_dates[0] if train_dates else '2025-09-01'
end_future = (datetime.strptime(train_dates[-1], '%Y-%m-%d') + timedelta(days=hold_days * 3)).strftime('%Y-%m-%d')
batch_size = 200
for i in range(0, len(train_codes), batch_size):
batch = train_codes[i:i+batch_size]
klines = query_daily_kline(codes=batch, start_date=start_dt, end_date=end_future,
order_by='date ASC', limit=None)
for kl in klines:
if kl.code not in price_map:
price_map[kl.code] = {}
price_map[kl.code][kl.date] = kl.close
if (i // batch_size) % 5 == 0:
print(f' K线加载: {min(i+batch_size, len(train_codes))}/{len(train_codes)} 只...', flush=True)
def _future_ret(code: str, date: str) -> Optional[float]:
"""取买入日后第 hold_days 个已有交易日的收盘价计算收益。"""
cdates = price_map.get(code)
if not cdates:
return None
sorted_dates = sorted(cdates.keys())
try:
idx = sorted_dates.index(date)
except ValueError:
# 找最近的日期
before = [d for d in sorted_dates if d <= date]
if not before:
return None
idx = sorted_dates.index(before[-1])
buy_price = cdates[sorted_dates[idx]]
sell_idx = min(idx + hold_days, len(sorted_dates) - 1)
if sell_idx == idx:
return None
sell_price = cdates[sorted_dates[sell_idx]]
if buy_price > 0:
return (sell_price - buy_price) / buy_price
return None
print('[预计算] 计算技术/基本面/行为指标...', flush=True)
for ci, code in enumerate(train_codes):
for date in train_dates:
key = (code, date)
try:
tech_r = _score_tech(code, date, weights=None)
fund_r = _score_fundamental(code, date, weights=None)
behav_r = _score_behavior(code, date, weights=None)
fret = _future_ret(code, date)
_TRAIN_CACHE[key] = {
'tech_raw': tech_r.get('_raw_scores', {}) if tech_r else {},
'fund_raw': fund_r.get('_raw_scores', {}) if fund_r else {},
'behav_raw': behav_r.get('_raw_scores', {}) if behav_r else {},
'future_ret': fret,
}
except Exception:
pass
done += 1
if (ci + 1) % 100 == 0 or ci == len(train_codes) - 1:
print(f' 指标预计算: {ci+1}/{len(train_codes)} 只,缓存={len(_TRAIN_CACHE)} 条', flush=True)
print(f'[预计算] 完成,共缓存 {len(_TRAIN_CACHE)} 个样本点', flush=True)
def _evaluate_weights_fast(wconfig: Dict[str, Any]) -> float:
"""
快速适应度函数:直接从内存缓存读取指标原始分,
按当前权重重新加权,统计 BUY 信号命中率(预测准确率 × 平均收益)。
"""
tech_w = wconfig.get('technical', {})
fund_w = wconfig.get('fundamental', {})
behav_w = wconfig.get('behavior', {})
expert_w = {k: v for k, v in wconfig['expert_weights'].items()}
expert_w['alpha'] = 0.0 # 训练阶段不用 alpha
buy_thresh = wconfig.get('signal_thresholds', {}).get('buy', 0.65)
sell_thresh = wconfig.get('signal_thresholds', {}).get('sell', 0.35)
total_ret = 0.0
buy_count = 0
for (code, date), cache in _TRAIN_CACHE.items():
future_ret = cache.get('future_ret')
if future_ret is None:
continue
# 纯内存加权
tech_score = _weighted_mean(cache['tech_raw'], tech_w) if cache['tech_raw'] else None
fund_score = _weighted_mean(cache['fund_raw'], fund_w) if cache['fund_raw'] else None
behav_score = _weighted_mean(cache['behav_raw'], behav_w) if cache['behav_raw'] else None
experts = {}
if tech_score is not None: experts['technical'] = tech_score
if fund_score is not None: experts['fundamental'] = fund_score
if behav_score is not None: experts['behavior'] = behav_score
if not experts:
continue
total_ew = sum(expert_w.get(k, 0.0) for k in experts)
if total_ew <= 0:
continue
final = sum(expert_w.get(k, 0.0) / total_ew * s for k, s in experts.items())
if final >= buy_thresh:
total_ret += future_ret
buy_count += 1
return (total_ret / buy_count) if buy_count > 0 else 0.0
def _mutate(wconfig: Dict[str, Any], mutation_rate: float = 0.15, mutation_strength: float = 0.3) -> Dict[str, Any]:
"""变异:随机扰动部分权重。"""
new = copy.deepcopy(wconfig)
ew = new['expert_weights']
for k in ew:
if random.random() < mutation_rate:
ew[k] = max(0.01, ew[k] + random.gauss(0, mutation_strength * ew[k]))
total = sum(ew.values())
for k in ew:
ew[k] = ew[k] / total
for section in ['technical', 'fundamental', 'behavior']:
sec = new.get(section, {})
keys = list(sec.keys())
n_mutate = max(1, int(len(keys) * mutation_rate))
for k in random.sample(keys, min(n_mutate, len(keys))):
sec[k] = max(0.0, sec[k] + random.gauss(0, mutation_strength))
thresh = new.get('signal_thresholds', {'buy': 0.65, 'sell': 0.35})
if random.random() < mutation_rate:
thresh['buy'] = _clamp(thresh['buy'] + random.gauss(0, 0.05), 0.55, 0.85)
if random.random() < mutation_rate:
thresh['sell'] = _clamp(thresh['sell'] + random.gauss(0, 0.05), 0.15, 0.45)
new['signal_thresholds'] = thresh
return new
def _crossover(p1: Dict[str, Any], p2: Dict[str, Any]) -> Dict[str, Any]:
"""交叉:每个 key 随机选一个亲本。"""
child = copy.deepcopy(p1)
for k in child['expert_weights']:
if random.random() < 0.5:
child['expert_weights'][k] = p2['expert_weights'].get(k, child['expert_weights'][k])
total = sum(child['expert_weights'].values())
for k in child['expert_weights']:
child['expert_weights'][k] /= total
for section in ['technical', 'alpha', 'fundamental', 'behavior']:
sec1 = child.get(section, {})
sec2 = p2.get(section, {})
for k in sec1:
if random.random() < 0.5 and k in sec2:
sec1[k] = sec2[k]
if random.random() < 0.5:
child['signal_thresholds'] = copy.deepcopy(p2.get('signal_thresholds', {'buy': 0.65, 'sell': 0.35}))
return child
def train_weights(
start_date: str,
end_date: str,
population_size: int = 20,
generations: int = 30,
elite_count: int = 4,
train_stock_count: int = 0,
) -> Dict[str, Any]:
"""
功能二:遗传算法训练最优权重,目标:最大化 BUY 信号后5日平均收益。
架构:两阶段
1. 预计算阶段(一次性):批量计算所有股票×日期的指标原始分 + 未来收益,存入内存
2. 迭代阶段(快速):遗传算法每代只做纯内存加权,不再查DB,速度极快
Args:
start_date: 训练开始日期
end_date: 训练结束日期
population_size: 种群大小(默认20)
generations: 迭代代数(默认30)
elite_count: 每代保留的精英数量
train_stock_count: 训练股票数量(0=全量)
Returns:
优化后的权重配置字典(已写入 moe_weights.json)
"""
print(f'\n[遗传算法] 开始训练 {start_date} ~ {end_date}')
print(f' 种群={population_size} 代数={generations} 精英={elite_count} 股票数={"全量" if train_stock_count <= 0 else train_stock_count}')
all_stocks = [b.ts_code for b in query_stock_basic() if b.ts_code]
random.seed(42)
if train_stock_count <= 0 or train_stock_count >= len(all_stocks):
train_codes = all_stocks
else:
train_codes = random.sample(all_stocks, train_stock_count)
print(f' 训练股票: {train_codes[:5]}... 共{len(train_codes)}只')
train_dates = _get_trading_dates(start_date, end_date)
print(f' 训练日期: {len(train_dates)}个月度采样点 {train_dates}')
if not train_dates:
print('[遗传算法] 没有找到训练日期,退出')
return load_weights()
# ── 阶段一:预计算(只跑一次)──────────────────────────────────────────────
_precompute_cache(train_codes, train_dates, hold_days=5)
if not _TRAIN_CACHE:
print('[遗传算法] 预计算缓存为空,退出')
return load_weights()
# ── 阶段二:遗传算法迭代(纯内存)──────────────────────────────────────────
base = load_weights()
population = [base]
for _ in range(population_size - 1):
population.append(_mutate(base, mutation_rate=0.3, mutation_strength=0.5))
best_config = base
best_fitness = -999.0
for gen in range(generations):
print(f'\n[遗传算法] 第 {gen+1}/{generations} 代 评估{len(population)}个个体...')
fitness_scores = []
for i, wconfig in enumerate(population):
try:
fit = _evaluate_weights_fast(wconfig)
except Exception:
fit = -1.0
fitness_scores.append(fit)
print(f' 个体{i+1:2d}: BUY平均收益={fit*100:.2f}%')
ranked = sorted(zip(fitness_scores, population), key=lambda x: x[0], reverse=True)
best_gen_fit, best_gen_cfg = ranked[0]
if best_gen_fit > best_fitness:
best_fitness = best_gen_fit
best_config = copy.deepcopy(best_gen_cfg)
print(f' ★ 新最优: {best_fitness*100:.2f}%')
elites = [cfg for _, cfg in ranked[:elite_count]]
new_population = list(elites)
while len(new_population) < population_size:
if random.random() < 0.6 and len(elites) >= 2:
p1, p2 = random.sample(elites, 2)
child = _crossover(p1, p2)
else:
child = copy.deepcopy(random.choice(elites))
child = _mutate(child, mutation_rate=0.15, mutation_strength=0.2)
new_population.append(child)
population = new_population
print(f'\n[遗传算法] 训练完成!最优BUY平均收益: {best_fitness*100:.2f}%')
save_weights(best_config, train_period=f'{start_date}~{end_date}')
return best_config
# ─────────────────────────────────────────────────────────────────────────────
# 命令行入口
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='MoE 混合专家买卖时机分析 + 权重训练')
subparsers = parser.add_subparsers(dest='cmd')
p_analyze = subparsers.add_parser('analyze', help='分析股票买卖时机(默认)')
p_analyze.add_argument('--code', required=True, help='股票代码,如 000001.SZ')
p_analyze.add_argument('--date', default=None, help='分析日期 YYYY-MM-DD,默认今天')
p_train = subparsers.add_parser('train', help='遗传算法训练权重')
p_train.add_argument('--start-date', default=None, help='训练开始日期')
p_train.add_argument('--end-date', default=None, help='训练结束日期')
p_train.add_argument('--population', type=int, default=20, help='种群大小')
p_train.add_argument('--generations', type=int, default=30, help='迭代代数')
p_train.add_argument('--stocks', type=int, default=30, help='训练股票数量')
# 兼容旧的直接参数
parser.add_argument('--code', default=None, help='股票代码(兼容)')
parser.add_argument('--date', default=None, help='分析日期(兼容)')
parser.add_argument('--train', action='store_true', help='训练模式(兼容)')
parser.add_argument('--start-date', default=None, dest='train_start')
parser.add_argument('--end-date', default=None, dest='train_end')
args = parser.parse_args()
init_indicators_db()
is_train = (args.cmd == 'train') or getattr(args, 'train', False)
if is_train:
_end = getattr(args, 'end_date', None) or getattr(args, 'train_end', None) \
or datetime.today().strftime('%Y-%m-%d')
_start = getattr(args, 'start_date', None) or getattr(args, 'train_start', None) \
or (datetime.today() - timedelta(days=180)).strftime('%Y-%m-%d')
pop = getattr(args, 'population', 20)
gens = getattr(args, 'generations', 30)
stocks = getattr(args, 'stocks', 30)
train_weights(_start, _end, population_size=pop, generations=gens, train_stock_count=stocks)
else:
code = (getattr(args, 'code', None) if args.cmd == 'analyze' else None) or args.code
if not code:
parser.print_help()
sys.exit(1)
date_val = (getattr(args, 'date', None) if args.cmd == 'analyze' else None) or args.date
analysis_date = date_val or datetime.today().strftime('%Y-%m-%d')
result = analyze(code, analysis_date)
print('\n' + '='*60)
out = {k: v for k, v in result.items() if not k.startswith('_')}
print(json.dumps(out, ensure_ascii=False, indent=2))
print('='*60)
if 'signal' in result:
sig = result['signal']
score = result['final_score']
conf = result['confidence']
sig_cn = {'BUY': '买入 ▲', 'SELL': '卖出 ▼', 'HOLD': '持有 —'}[sig]
print(f'\n {result["code"]} 综合评分: {score:.4f} → {sig_cn} (置信度: {conf})')
print(f' {result["reason"]}')
FILE:scripts/moe_weights.json
{
"_comment": "MoE权重配置文件。通过 train_weights 命令跑回测优化后自动更新。",
"_version": 1,
"_trained_at": "2026-03-19 14:58:16",
"_train_period": "2025-09-01~2026-03-19",
"expert_weights": {
"technical": 0.35717595519629336,
"alpha": 0.36057193372593965,
"fundamental": 0.15416369933688986,
"behavior": 0.12808841174087712
},
"signal_thresholds": {
"buy": 0.7313263753423049,
"sell": 0.35
},
"technical": {
"sma5": 1.181801288003714,
"sma10": 0.7570724714871282,
"sma20": 1.0,
"sma60": 1.1118537267876476,
"ema5": 1.0,
"ema12": 1.0,
"ema20": 0.27926056566514107,
"ema26": 1.0,
"ema_cross": 0.6575509109292674,
"wma20": 1.0,
"tema20": 1.0,
"dema20": 0.8608835251864292,
"kama": 1.0,
"bbi": 0.8636807483782426,
"trix": 1.833969089253665,
"dmi": 1.0,
"sar": 0.8951085513864419,
"linearreg_slope": 1.3211429094664284,
"linearreg": 1.0923904719425455,
"linearreg_angle": 0.5262131258427737,
"linearreg_intercept": 0.9885568378088866,
"aroon": 0.7826516778972384,
"tsf": 1.0,
"ht_trendmode": 0.8814204448925165,
"ht_dcphase": 1.0,
"ht_sine": 1.0,
"rsi14": 0.8662579600815672,
"rsi6": 1.0,
"cci": 1.0,
"mom10": 0.7352808685133152,
"mom20": 1.0,
"roc10": 1.0,
"rocp10": 1.7929313995073208,
"rocr10": 1.0,
"roc_r": 2.064310646291524,
"willr": 1.0,
"cmo": 1.0,
"bias": 1.0371024748469209,
"psycho": 1.0,
"dpo": 1.0,
"mass": 1.0,
"kdj_j": 1.0,
"kdj_kd": 1.0,
"stoch_k": 1.1291875450945286,
"stochf_k": 1.0,
"stochrsi": 1.0,
"ultosc": 0.727829745700426,
"macd_hist": 0.6826161537737307,
"macd_cross": 0.9130399186907544,
"ppo_hist": 0.9136754128716245,
"adosc": 1.0,
"obv": 1.2333726939245337,
"ad": 1.0,
"mfi": 1.1649303644243527,
"vwap": 0.32248302163927334,
"volume_ratio": 1.6461768542684325,
"vr": 1.0,
"pvi": 0.9351558341111518,
"nvi": 1.0,
"ar": 1.6225854372575186,
"br": 0.7882101600971572,
"brar": 1.0,
"asi": 1.0,
"bb_pos": 1.0778193939643872,
"bb_signal": 1.201047760357908,
"bbands_pct": 1.0219943929294277,
"bbands_width": 1.0074095539385972,
"ma_channel": 0.25496657546761314,
"donchian": 0.053137051875268626,
"keltner": 1.0,
"xue_channel": 1.0487232147691394,
"midpoint": 0.3385211409278348,
"midprice": 1.0,
"atr": 1.0792445125369383,
"natr": 1.0,
"tr": 1.2644451626865996,
"trange": 0.2911803623562474,
"stddev": 1.0,
"var": 1.0,
"correl": 1.0,
"beta": 0.7630957463073703,
"ht_dcperiod": 1.2793115495425174,
"typical": 1.0,
"median": 1.0,
"wclose": 1.0,
"avgp": 1.0,
"ht_phasor": 1.0826827348531094,
"consec_rise": 1.1666511139322437,
"consec_fall": 1.0,
"bomb_board": 1.0,
"bomb_board_count": 1.0,
"consec_limit_up": 1.0
},
"alpha": {
"alpha001": 1.0,
"alpha002": 1.0,
"alpha003": 1.0,
"alpha004": 1.0,
"alpha005": 1.0,
"alpha006": 1.0,
"alpha007": 1.0,
"alpha008": 1.0,
"alpha009": 1.0,
"alpha010": 1.0,
"alpha011": 1.0,
"alpha012": 1.0,
"alpha013": 1.0,
"alpha014": 1.0,
"alpha015": 1.0,
"alpha016": 1.0,
"alpha017": 1.0,
"alpha018": 1.0,
"alpha019": 1.0,
"alpha020": 1.0,
"alpha021": 1.0,
"alpha022": 1.0,
"alpha023": 1.0,
"alpha024": 1.0,
"alpha025": 1.0,
"alpha026": 1.0,
"alpha027": 1.0,
"alpha028": 1.0,
"alpha029": 1.0,
"alpha030": 1.0,
"alpha031": 1.0,
"alpha032": 1.0,
"alpha033": 1.0,
"alpha034": 1.0,
"alpha035": 1.0,
"alpha036": 1.0,
"alpha037": 1.0,
"alpha038": 1.0,
"alpha039": 1.0,
"alpha040": 1.0,
"alpha041": 1.0,
"alpha042": 1.0,
"alpha043": 1.0,
"alpha044": 1.0,
"alpha045": 1.0,
"alpha046": 1.0,
"alpha047": 1.0,
"alpha048": 1.0,
"alpha049": 1.0,
"alpha050": 1.0,
"alpha051": 1.0,
"alpha052": 1.0,
"alpha053": 1.0,
"alpha054": 1.0,
"alpha055": 1.0,
"alpha056": 1.0,
"alpha057": 1.0,
"alpha058": 1.0,
"alpha059": 1.0,
"alpha060": 1.0,
"alpha061": 1.0,
"alpha062": 1.0,
"alpha063": 1.0,
"alpha064": 1.0,
"alpha065": 1.0,
"alpha066": 1.0,
"alpha067": 1.0,
"alpha068": 1.0,
"alpha069": 1.0,
"alpha070": 1.0,
"alpha071": 1.0,
"alpha072": 1.0,
"alpha073": 1.0,
"alpha074": 1.0,
"alpha075": 1.0,
"alpha076": 1.0,
"alpha077": 1.0,
"alpha078": 1.0,
"alpha079": 1.0,
"alpha080": 1.0,
"alpha081": 1.0,
"alpha082": 1.0,
"alpha083": 1.0,
"alpha084": 1.0,
"alpha085": 1.0,
"alpha086": 1.0,
"alpha087": 1.0,
"alpha088": 1.0,
"alpha089": 1.0,
"alpha090": 1.0,
"alpha091": 1.0,
"alpha092": 1.0,
"alpha093": 1.0,
"alpha094": 1.0,
"alpha095": 1.0,
"alpha096": 1.0,
"alpha097": 1.0,
"alpha098": 1.0,
"alpha099": 1.0,
"alpha100": 1.0,
"alpha101": 1.0
},
"fundamental": {
"pe_ttm": 1.7102072285522736,
"pb": 1.0,
"turnover_rate": 1.0,
"volume_ratio": 0.7606881867266152,
"ps_ttm": 1.4705142753123992
},
"behavior": {
"limit_score": 1.2956770069079582,
"consecutive_limit_up": 1.0,
"bomb_board": 1.0768788113552823,
"top_list": 0.8377720620665754,
"pct_chg_5d": 1.0
}
}
FILE:scripts/realtime_data_featcher.py
import requests
import time
import json
import random
import re
from typing import Dict, List, Optional, Union, Any
from dataclasses import dataclass
from define import RealtimeStockQuote
class RealTimeDataFetcher:
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
self.last_request_time = {}
self.min_interval = 1.0 # Minimum int
def _convert_to_sina_symbol(self, ts_code: str) -> str:
# 000001.SZ -> sz000001
# 600519.SH -> sh600519
code, market = ts_code.split('.')
return f"{market.lower()}{code}"
def _wait_for_rate_limit(self, source: str):
current_time = time.time()
last_time = self.last_request_time.get(source, 0)
elapsed = current_time - last_time
if elapsed < self.min_interval:
sleep_time = self.min_interval - elapsed
time.sleep(sleep_time)
self.last_request_time[source] = time.time()
def request_stock_info(self, ts_code: str) -> Optional[RealtimeStockQuote]:
"""
查询实时股价信息
参数
ts_code 股票代码
返回
RealtimeStockQuote 实时股票报价信息数据结构
"""
self._wait_for_rate_limit('sina')
symbol = self._convert_to_sina_symbol(ts_code)
url = f"http://hq.sinajs.cn/list={symbol}"
headers = self.headers.copy()
headers['Referer'] = 'https://finance.sina.com.cn/'
try:
response = requests.get(url, headers=headers, timeout=5)
response.raise_for_status()
# Format: var hq_str_sh601006="大秦铁路, 27.55, 27.25, 26.91, 27.55, 26.20, 26.91, 26.92, ...";
content = response.text
match = re.search(r'="(.*)";', content)
if match:
data_str = match.group(1)
parts = data_str.split(',')
if len(parts) > 30:
pre_close = float(parts[2])
high = float(parts[4])
low = float(parts[5])
amplitude = 0.0
if pre_close > 0:
amplitude = (high - low) / pre_close * 100
return RealtimeStockQuote(
ts_code=ts_code,
name=parts[0],
open=float(parts[1]),
pre_close=pre_close,
price=float(parts[3]),
high=high,
low=low,
bid=float(parts[6]),
ask=float(parts[7]),
volume=int(parts[8]), # Sina returns shares
amount=float(parts[9]),
date=parts[30],
time=parts[31],
amplitude=round(amplitude, 2),
turnover_rate=None,
total_cap=None,
circ_cap=None,
pb=None,
pe_ttm=None,
total_shares=None,
circ_shares=None,
status="success"
)
return None
except Exception:
return None
if __name__ == "__main__":
RealTimeDataFetcher().fetch_sina("000001.SZ")
FILE:scripts/remote_api.py
from typing import List, Optional, Dict, Any
import define
import requests
from define import AppVersion,TokenCheckResult
import config
class PatchItem:
def __init__(self):
self.patch_date:str = ""
self.patch_name:str = ""
self.version:int = int
def request_patch_list() -> List[PatchItem]:
"""
获取所有表的patch列表
返回json说明:
key: 表名
value: 所有可用patch列表
"""
ret: List[PatchItem] = []
token = config.get_token()
response = requests.get(f"{define.BASE_URL}/api/patch_list/all", params={"token": token})
if response.status_code == 200:
rsp = response.json()
datas_json = rsp["data"]
for data_json in datas_json:
item: PatchItem = PatchItem()
item.patch_name = data_json["patch_name"]
item.patch_date = data_json["patch_date"]
item.version = int(float(data_json["version"]) * 10)
ret.append(item)
return ret
def request_decrypt_key(file_name:str, token_key:str) -> str:
url = f"{define.BASE_URL}/api/get_decryption_key"
params = {
"file_name": file_name,
"token_key": token_key
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
key = data.get("key")
return key
else:
return ""
except Exception as e:
return ""
def request_download_url(file_name: str, token_key: str) -> str:
"""
获取文件的下载链接。
参数:
file_name: 文件名(如 data_1.0.bin)
token_key: API 访问令牌
返回:
str: 下载链接,失败返回空字符串
"""
url = f"{define.BASE_URL}/api/download_file"
params = {
"file_name": file_name,
"token_key": token_key
}
try:
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
download_url = data.get("download_url", "")
return download_url
else:
return ""
except Exception as e:
print(f"request_download_url error: {e}")
return ""
def request_version() -> AppVersion:
url = f"{define.BASE_URL}/api/get_latest_version"
try:
params = {
"token": config.get_token()
}
response = requests.post(url, json=params)
if response.status_code == 200:
data = response.json()
ver = AppVersion.from_dict(data)
return ver
else:
return None
except Exception as e:
print(e)
return None
def request_check_token(token:str) -> TokenCheckResult:
url = f"{define.BASE_URL}/api/check_token"
params = {
"token": token
}
try:
response = requests.post(url, json=params)
data = response.json()
return TokenCheckResult.from_dict(data)
except Exception as e:
print(e)
return TokenCheckResult.from_dict({"status": "failure", "message": str(e)})
def request_get_benchmark(token: str) -> Optional[Dict[str, float]]:
"""
获取服务器配置的基准线阈值。
返回 dict,包含 total_yield / annualized_yield / max_drawdown / sharpe_ratio;
失败(token无效、网络错误)返回 None。
"""
url = f"{define.BASE_URL}/api/get_benchmark"
try:
response = requests.post(url, json={"token": token}, timeout=define.HTTP_TIMEOUT)
if response.status_code == 200:
return response.json()
else:
print(f"get_benchmark failed: {response.status_code} {response.text}")
return None
except Exception as e:
print(f"get_benchmark error: {e}")
return None
class SubmitYieldResult:
"""提交收益数据的结果"""
def __init__(self, success: bool, message: str, details: Optional[List[str]] = None):
self.success = success
self.message = message
self.details = details or []
def __repr__(self) -> str:
return f"SubmitYieldResult(success={self.success!r}, message={self.message!r}, details={self.details!r})"
def request_submit_yield(
token: str,
total_yield: float,
annualized_yield: float,
max_drawdown: float,
sharpe_ratio: float,
positions: Any = None,
) -> SubmitYieldResult:
"""
提交收益数据到排行榜。
服务器仅强制校验 total_yield 是否超过配置基准线,其余指标不做强制要求。
成功返回 SubmitYieldResult(success=True);
失败返回 SubmitYieldResult(success=False, message=..., details=...)。
"""
url = f"{define.BASE_URL}/api/submit_yield"
yield_data = {
"total_yield": total_yield,
"annualized_yield": annualized_yield,
"max_drawdown": max_drawdown,
"sharpe_ratio": sharpe_ratio,
"positions": positions if positions is not None else [],
}
body = {
"token": token,
"data": yield_data,
}
try:
response = requests.post(url, json=body, timeout=define.HTTP_TIMEOUT)
data = response.json()
if response.status_code == 200 and data.get("status") == "ok":
return SubmitYieldResult(success=True, message="提交成功")
else:
msg = data.get("message", f"HTTP {response.status_code}")
details = data.get("details", [])
return SubmitYieldResult(success=False, message=msg, details=details)
except Exception as e:
return SubmitYieldResult(success=False, message=str(e))
if __name__ == "__main__":
_token = "8ACw626fHId31d3OWwVE62yzGkA7p9vCyg1kIV9AKSiU"
print("=== check_token ===")
check_result = request_check_token(_token)
print(check_result)
print("\n=== get_benchmark ===")
bm = request_get_benchmark(_token)
print(bm)
print("\n=== submit_yield (应该被基准线拒绝) ===")
r1 = request_submit_yield(_token, total_yield=0.01, annualized_yield=0.05,
max_drawdown=-0.10, sharpe_ratio=0.8)
print(r1)
print("\n=== submit_yield (超过基准线,应该成功) ===")
r2 = request_submit_yield(_token, total_yield=0.50, annualized_yield=0.35,
max_drawdown=-0.08, sharpe_ratio=2.1,
positions=[{"code": "000001.SZ", "weight": 1.0}])
print(r2)
FILE:scripts/signals.py
"""
signal.py - 裸K形态信号模块
功能:
1. 识别经典K线形态,输出形态信号
2. 形态出现当日信号值为 1,未出现为 0
3. 计算结果缓存到数据库(cached_signals 表)
支持形态:
- 早晨之星 / 启明星(Morning Star) :底部三K线看涨反转
- 黄昏之星 / 黄昏星(Evening Star) :顶部三K线看跌反转
- 红三兵(Three White Soldiers) :底部三根连续阳线看涨确认
- 三只乌鸦(Three Black Crows) :顶部三根连续阴线看跌确认
- 乌云盖顶(Dark Cloud Cover) :顶部两K线看跌反转
- 圆弧底(Rounding Bottom) :多K线底部圆弧形态(默认20日)
- 上升三角形(Ascending Triangle) :整理后向上突破前夕(默认20日)
- 顶部形态(Top Pattern) :双重顶等多K线顶部反转结构(默认30日)
设计原则:
- 函数接口与 indicators.py 统一(code/date/params/use_adjusted)
- 查询优先使用缓存,计算后存入数据库
- 默认使用后复权价格,避免除权日跳空干扰形态识别
- 缓存键仅包含 period 参数;修改 body_ratio 等形态阈值时需手动清缓存
缓存表 cached_signals 结构:
UNIQUE(code, signal_type, param, use_adjusted, date)
value INTEGER:1 表示形态出现,0 表示未出现
"""
from typing import Dict, List, Optional
from sqlalchemy import text
from data_fetcher import getEngine
from data_fetcher import query_daily_kline, query_daily_basic
from define import DailyKline
# ── 缓存机制 ─────────────────────────────────────────────────────────────────
def init_signals_db() -> None:
"""初始化信号缓存数据库表
创建 cached_signals 表(如不存在),用于缓存形态信号计算结果,并建立查询索引。
每次计算前先查缓存,命中则直接返回,避免对相同参数重复计算。
"""
with getEngine().connect() as conn:
conn.execute(text("""
CREATE TABLE IF NOT EXISTS cached_signals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL,
signal_type TEXT NOT NULL,
param INTEGER NOT NULL DEFAULT 0,
use_adjusted INTEGER NOT NULL DEFAULT 1,
date TEXT NOT NULL,
value INTEGER NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(code, signal_type, param, use_adjusted, date)
)
"""))
conn.execute(text(
"CREATE INDEX IF NOT EXISTS idx_signals_lookup "
"ON cached_signals(code, signal_type, param, use_adjusted, date)"
))
conn.commit()
_signals_table_ready: bool = False
def _ensure_table() -> None:
global _signals_table_ready
if not _signals_table_ready:
init_signals_db()
_signals_table_ready = True
def _get_cached_signal(
code: str, signal_type: str, param: int, date: str, use_adjusted: bool
) -> Optional[int]:
"""从缓存表查询信号值
Args:
code: 股票代码
signal_type: 信号类型字符串,如 'MORNING_STAR'
param: 主要周期参数(无周期的形态传 0)
date: 查询日期,格式 'YYYY-MM-DD'
use_adjusted: 是否为复权计算结果
Returns:
int: 缓存的信号值(0 或 1),未命中返回 None
"""
_ensure_table()
with getEngine().connect() as conn:
cursor = conn.execute(text(
"SELECT value FROM cached_signals "
"WHERE code=:code AND signal_type=:st AND param=:p "
" AND use_adjusted=:adj AND date=:d"
), {"code": code, "st": signal_type, "p": param,
"adj": 1 if use_adjusted else 0, "d": date})
row = cursor.fetchone()
return int(row[0]) if row else None
def _save_signal(
code: str, signal_type: str, param: int, date: str, value: int, use_adjusted: bool
) -> None:
"""将信号值保存到缓存表
已存在则替换(INSERT OR REPLACE),确保缓存始终为最新值。
Args:
code: 股票代码
signal_type: 信号类型字符串
param: 主要周期参数(无周期的形态传 0)
date: 计算日期,格式 'YYYY-MM-DD'
value: 信号值,0 或 1
use_adjusted: 是否为复权计算结果
"""
_ensure_table()
with getEngine().connect() as conn:
conn.execute(text(
"INSERT OR REPLACE INTO cached_signals "
"(code, signal_type, param, use_adjusted, date, value) "
"VALUES (:code, :st, :p, :adj, :d, :v)"
), {"code": code, "st": signal_type, "p": param,
"adj": 1 if use_adjusted else 0, "d": date, "v": value})
conn.commit()
# ── K 线获取与复权(与 indicators.py 保持一致)─────────────────────────────
def _get_klines_before_date(code: str, date: str, limit: int) -> List[DailyKline]:
"""获取指定日期(含)前最近 limit 根K线,按时间升序排列"""
klines = query_daily_kline(codes=[code], end_date=date, limit=limit, order_by="date DESC")
return klines[::-1]
def _get_adj_factors_for_klines(klines: List[DailyKline]) -> Dict[str, float]:
"""批量获取K线覆盖日期范围内的复权因子"""
if not klines:
return {}
code = klines[0].code
start_date = klines[0].date
end_date = klines[-1].date
daily_basics = query_daily_basic(ts_codes=[code], start_date=start_date, end_date=end_date)
return {b.trade_date: b.adj_factor for b in daily_basics}
def _adjust_klines(klines: List[DailyKline], adj_factors: Dict[str, float]) -> List[DailyKline]:
"""后复权处理:adjusted_price = raw_price × adj_factor
成交量不做调整,价格字段(open/high/low/close/pre_close/change/amount)全部乘以因子。
"""
if not klines or not adj_factors:
return klines
result = []
for k in klines:
f = adj_factors.get(k.date) or 1.0
result.append(DailyKline(
date=k.date, code=k.code,
open=k.open * f,
high=k.high * f,
low=k.low * f,
close=k.close * f,
volume=k.volume,
amount=k.amount * f if k.amount else 0.0,
adjustflag=k.adjustflag,
turn=k.turn,
pctChg=k.pctChg,
pre_close=k.pre_close * f if k.pre_close else 0.0,
change=k.change * f if k.change else 0.0,
))
return result
# ── 内部辅助函数 ─────────────────────────────────────────────────────────────
def _body_ratio(k: DailyKline) -> float:
"""实体比:K线实体大小 / 总振幅(0~1);振幅为 0 时返回 0"""
rng = k.high - k.low
return abs(k.close - k.open) / rng if rng > 0 else 0.0
def _upper_shadow_ratio(k: DailyKline) -> float:
"""上影线比:上影线长度 / 总振幅(0~1);振幅为 0 时返回 0"""
rng = k.high - k.low
return (k.high - max(k.open, k.close)) / rng if rng > 0 else 0.0
def _lower_shadow_ratio(k: DailyKline) -> float:
"""下影线比:下影线长度 / 总振幅(0~1);振幅为 0 时返回 0"""
rng = k.high - k.low
return (min(k.open, k.close) - k.low) / rng if rng > 0 else 0.0
def _is_bullish(k: DailyKline) -> bool:
"""阳线判断:收盘价 > 开盘价"""
return k.close > k.open
def _is_bearish(k: DailyKline) -> bool:
"""阴线判断:收盘价 < 开盘价"""
return k.close < k.open
def _linear_slope(values: List[float]) -> float:
"""最小二乘线性回归斜率(每格的平均变化量);数据不足 2 个时返回 0"""
n = len(values)
if n < 2:
return 0.0
x_mean = (n - 1) / 2.0
y_mean = sum(values) / n
num = sum((i - x_mean) * (values[i] - y_mean) for i in range(n))
den = sum((i - x_mean) ** 2 for i in range(n))
return num / den if den > 0 else 0.0
def _find_local_highs(values: List[float], window: int = 3) -> List[int]:
"""返回局部极大值的索引列表
在 [i-window, i+window] 范围内,values[i] 是最大值则认为是局部极大值。
"""
result = []
n = len(values)
for i in range(window, n - window):
if values[i] == max(values[i - window: i + window + 1]):
result.append(i)
return result
def _find_local_lows(values: List[float], window: int = 3) -> List[int]:
"""返回局部极小值的索引列表
在 [i-window, i+window] 范围内,values[i] 是最小值则认为是局部极小值。
"""
result = []
n = len(values)
for i in range(window, n - window):
if values[i] == min(values[i - window: i + window + 1]):
result.append(i)
return result
# ── 形态信号函数 ─────────────────────────────────────────────────────────────
def get_morning_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""早晨之星 / 启明星(Morning Star)—— 底部三K线看涨反转信号
由三根连续K线构成的经典底部反转形态,信号在第三根K线(确认阳线)日期触发:
· 第1根:大阴线(实体比 >= large_body_ratio),确认此前下跌趋势;
· 第2根:十字星或小实体(实体比 <= doji_ratio),多空力量均衡,市场犹豫;
· 第3根:大阳线(实体比 >= large_body_ratio),收盘高于第1根阴线实体
底部向上 penetrate_ratio 处(默认刺穿中点),确认买方入场。
使用复权价格可避免除权日产生的价格跳空被误判为星线间隙。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期(信号触发日期),格式 'YYYY-MM-DD'
large_body_ratio: 大实体最小实体比阈值,默认 0.6
doji_ratio: 星线(第2根)最大实体比阈值,默认 0.3
penetrate_ratio: 第3根K线需从第1根阴线底部(close)向上穿透的比例,
默认 0.5(即收盘至少在第1根实体中点以上)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现早晨之星(启明星),0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'MORNING_STAR', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
# k1 阴线:open > close;实体从 close(底)到 open(顶)
k1_body = abs(k1.open - k1.close)
value = 1 if (
_is_bearish(k1) and _body_ratio(k1) >= large_body_ratio
and _body_ratio(k2) <= doji_ratio
and _is_bullish(k3) and _body_ratio(k3) >= large_body_ratio
and k3.close >= k1.close + penetrate_ratio * k1_body
) else 0
_save_signal(code, 'MORNING_STAR', 0, date, value, use_adjusted)
return value
def get_evening_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""黄昏之星 / 黄昏星(Evening Star)—— 顶部三K线看跌反转信号
早晨之星(启明星)的镜像形态,信号在第三根K线(确认阴线)日期触发:
· 第1根:大阳线(实体比 >= large_body_ratio),确认此前上涨趋势;
· 第2根:十字星或小实体(实体比 <= doji_ratio),多空犹豫;
· 第3根:大阴线(实体比 >= large_body_ratio),收盘低于第1根阳线实体
顶部向下 penetrate_ratio 处(默认刺穿中点),确认卖方入场。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期(信号触发日期),格式 'YYYY-MM-DD'
large_body_ratio: 大实体最小实体比阈值,默认 0.6
doji_ratio: 星线(第2根)最大实体比阈值,默认 0.3
penetrate_ratio: 第3根K线需从第1根阳线顶部(close)向下穿透的比例,
默认 0.5(即收盘至少在第1根实体中点以下)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现黄昏之星(黄昏星),0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'EVENING_STAR', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
# k1 阳线:close > open;实体从 open(底)到 close(顶)
k1_body = abs(k1.close - k1.open)
value = 1 if (
_is_bullish(k1) and _body_ratio(k1) >= large_body_ratio
and _body_ratio(k2) <= doji_ratio
and _is_bearish(k3) and _body_ratio(k3) >= large_body_ratio
and k3.close <= k1.close - penetrate_ratio * k1_body
) else 0
_save_signal(code, 'EVENING_STAR', 0, date, value, use_adjusted)
return value
def get_three_white_soldiers(
code: str,
date: str,
body_ratio: float = 0.5,
shadow_ratio: float = 0.2,
use_adjusted: bool = True,
) -> Optional[int]:
"""红三兵(Three White Soldiers)—— 底部连续三阳看涨确认信号
三根连续上涨的大实体阳线,表明多方持续主导:
· 三根全为阳线(close > open);
· 每根收盘价高于前一根;
· 每根开盘价在前一根阳线实体内(逐步递进,拒绝跳空);
· 实体比均 >= body_ratio;
· 上影线比均 <= shadow_ratio(收盘接近当日最高价,多方强势)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
body_ratio: 每根K线的最小实体比阈值,默认 0.5
shadow_ratio: 每根K线的最大上影线比阈值,默认 0.2
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现红三兵,0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'THREE_WHITE_SOLDIERS', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
value = 1 if (
_is_bullish(k1) and _is_bullish(k2) and _is_bullish(k3)
and _body_ratio(k1) >= body_ratio
and _body_ratio(k2) >= body_ratio
and _body_ratio(k3) >= body_ratio
and _upper_shadow_ratio(k1) <= shadow_ratio
and _upper_shadow_ratio(k2) <= shadow_ratio
and _upper_shadow_ratio(k3) <= shadow_ratio
and k2.close > k1.close and k3.close > k2.close # 逐步创新高
and k1.open <= k2.open <= k1.close # k2 开盘在 k1 实体内
and k2.open <= k3.open <= k2.close # k3 开盘在 k2 实体内
) else 0
_save_signal(code, 'THREE_WHITE_SOLDIERS', 0, date, value, use_adjusted)
return value
def get_three_black_crows(
code: str,
date: str,
body_ratio: float = 0.5,
shadow_ratio: float = 0.2,
use_adjusted: bool = True,
) -> Optional[int]:
"""三只乌鸦(Three Black Crows)—— 顶部连续三阴看跌确认信号
红三兵的镜像形态,三根连续下跌的大实体阴线,表明空方持续主导:
· 三根全为阴线(close < open);
· 每根收盘价低于前一根;
· 每根开盘价在前一根阴线实体内(逐步递进,拒绝跳空);
· 实体比均 >= body_ratio;
· 下影线比均 <= shadow_ratio(收盘接近当日最低价,空方强势)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
body_ratio: 每根K线的最小实体比阈值,默认 0.5
shadow_ratio: 每根K线的最大下影线比阈值,默认 0.2
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现三只乌鸦,0 表示未出现;数据不足 3 根时返回 None
"""
cached = _get_cached_signal(code, 'THREE_BLACK_CROWS', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 3)
if len(klines) < 3:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2, k3 = klines[-3], klines[-2], klines[-1]
value = 1 if (
_is_bearish(k1) and _is_bearish(k2) and _is_bearish(k3)
and _body_ratio(k1) >= body_ratio
and _body_ratio(k2) >= body_ratio
and _body_ratio(k3) >= body_ratio
and _lower_shadow_ratio(k1) <= shadow_ratio
and _lower_shadow_ratio(k2) <= shadow_ratio
and _lower_shadow_ratio(k3) <= shadow_ratio
and k2.close < k1.close and k3.close < k2.close # 逐步创新低
and k1.close <= k2.open <= k1.open # k2 开盘在 k1 实体内
and k2.close <= k3.open <= k2.open # k3 开盘在 k2 实体内
) else 0
_save_signal(code, 'THREE_BLACK_CROWS', 0, date, value, use_adjusted)
return value
def get_dark_cloud_cover(
code: str,
date: str,
body_ratio: float = 0.5,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""乌云盖顶(Dark Cloud Cover)—— 顶部两K线看跌反转信号
两根K线构成的顶部反转形态:
· 第1根:大阳线(实体比 >= body_ratio),确认此前上涨;
· 第2根:阴线,开盘价高于第1根收盘价(高位开盘),随后大幅回落,
收盘低于第1根阳线实体顶部向下 penetrate_ratio 处(默认中点),
且高于第1根开盘价(未完全吞没,否则为吞噬形态)。
乌云盖顶说明多方在高位遭遇强力抛压,多空转换信号明显。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
body_ratio: 第1根阳线的最小实体比阈值,默认 0.5
penetrate_ratio: 第2根阴线向下穿透第1根实体的最小比例,
默认 0.5(收盘须低于第1根实体中点)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现乌云盖顶,0 表示未出现;数据不足 2 根时返回 None
"""
cached = _get_cached_signal(code, 'DARK_CLOUD_COVER', 0, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, 2)
if len(klines) < 2:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
k1, k2 = klines[-2], klines[-1]
k1_body = k1.close - k1.open # k1 阳线:close > open,k1_body > 0
value = 1 if (
_is_bullish(k1) and _body_ratio(k1) >= body_ratio
and _is_bearish(k2)
and k2.open > k1.close # 向上跳空开盘(高于前收)
and k2.close < k1.close - penetrate_ratio * k1_body # 刺入 k1 实体下半段
and k2.close > k1.open # 未完全吞没 k1(区别于吞噬形态)
) else 0
_save_signal(code, 'DARK_CLOUD_COVER', 0, date, value, use_adjusted)
return value
def get_rounding_bottom(
code: str,
date: str,
period: int = 20,
symmetry_tol: float = 0.1,
use_adjusted: bool = True,
) -> Optional[int]:
"""圆弧底(Rounding Bottom)—— 多K线底部圆弧反转形态
收盘价在 period 根K线内呈平滑"U"形分布,反映多空力量的渐进转换:
· 左侧(前 1/3)平均收盘 > 中部(中 1/3)平均收盘(价格从高位缓慢下行);
· 右侧(后 1/3)平均收盘 > 中部平均收盘(价格从低位缓慢回升);
· 期间最低收盘价出现在中间三分之一区域(底部圆弧的谷底在中部);
· 右侧平均收盘 >= 左侧平均收盘 × (1 - symmetry_tol)(右侧已充分回升);
· 最近 3 根K线收盘价呈上升趋势(线性斜率 > 0,当前处于上升右侧)。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯K线根数,默认 20
symmetry_tol: 右侧回升相对左侧的最小比例容差,默认 0.1(即右侧须恢复至左侧 90% 以上)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日出现圆弧底,0 表示未出现;数据不足 period 根时返回 None
"""
cached = _get_cached_signal(code, 'ROUNDING_BOTTOM', period, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
closes = [k.close for k in klines]
n = len(closes)
t = n // 3
left = closes[:t]
mid = closes[t: 2 * t]
right = closes[2 * t:]
avg_left = sum(left) / len(left)
avg_mid = sum(mid) / len(mid)
avg_right = sum(right) / len(right)
min_idx = closes.index(min(closes))
value = 1 if (
avg_left > avg_mid # 左侧高于底部
and avg_right > avg_mid # 右侧高于底部
and t <= min_idx < 2 * t # 最低点在中间三分之一
and avg_right >= avg_left * (1 - symmetry_tol) # 右侧已充分回升
and _linear_slope(closes[-3:]) > 0 # 近期处于上升趋势
) else 0
_save_signal(code, 'ROUNDING_BOTTOM', period, date, value, use_adjusted)
return value
def get_ascending_triangle(
code: str,
date: str,
period: int = 20,
resistance_tol: float = 0.02,
use_adjusted: bool = True,
) -> Optional[int]:
"""上升三角形(Ascending Triangle)—— 突破前夕看涨整理形态
价格在一段时间内呈现"水平阻力 + 上升支撑"的三角形收敛结构:
· 阻力线:期间局部高点聚集在同一水平(局部极大值的标准差/均值 <= resistance_tol);
· 支撑线:期间局部低点依次抬高(局部极小值的线性回归斜率 > 0);
· 当前收盘价接近阻力位(>= 阻力位 × (1 - resistance_tol)),处于突破蓄势阶段。
判断局部极大/极小值时采用 window=3(左右各3根K线),period 至少应为 10 才能
检测到足够数量的极值点。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯K线根数,默认 20
resistance_tol: 阻力位水平性判断的最大变异系数(std/mean),默认 0.02(即 2%)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日存在上升三角形形态,0 表示未出现;数据不足 period 根时返回 None
"""
cached = _get_cached_signal(code, 'ASCENDING_TRIANGLE', period, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
highs = [k.high for k in klines]
lows = [k.low for k in klines]
closes = [k.close for k in klines]
# 阻力线:局部高点需聚集在同一水平(至少 2 个局部极大值)
peak_idxs = _find_local_highs(highs, window=3)
if len(peak_idxs) < 2:
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, 0, use_adjusted)
return 0
peak_vals = [highs[i] for i in peak_idxs]
mean_peak = sum(peak_vals) / len(peak_vals)
if mean_peak == 0:
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, 0, use_adjusted)
return 0
variance = sum((v - mean_peak) ** 2 for v in peak_vals) / len(peak_vals)
std_peak = variance ** 0.5
flat_resistance = (std_peak / mean_peak) <= resistance_tol
# 支撑线:局部低点斜率 > 0(至少 2 个局部极小值)
trough_idxs = _find_local_lows(lows, window=3)
if len(trough_idxs) < 2:
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, 0, use_adjusted)
return 0
trough_vals = [lows[i] for i in trough_idxs]
rising_support = _linear_slope(trough_vals) > 0
# 当前价格接近阻力位(突破前夕)
resistance = max(highs)
near_breakout = closes[-1] >= resistance * (1 - resistance_tol)
value = 1 if (flat_resistance and rising_support and near_breakout) else 0
_save_signal(code, 'ASCENDING_TRIANGLE', period, date, value, use_adjusted)
return value
def get_top_pattern(
code: str,
date: str,
period: int = 30,
tolerance: float = 0.03,
min_decline: float = 0.03,
use_adjusted: bool = True,
) -> Optional[int]:
"""顶部形态(Top Pattern)—— 双重顶等多K线顶部反转结构
以双重顶(Double Top)为核心,检测 period 根K线内的顶部反转结构:
· 在期间内找到至少 2 个局部高点;
· 取高度最高的两个局部高点,其价差 / 均值 <= tolerance(两顶高度相近);
· 两顶之间存在有意义的颈线回调:颈线最低价低于两顶均价的 min_decline 比例;
· 第二个高点出现在期间后半段;
· 当前收盘价已从第二个高点回落(< 第二高点 × (1 - tolerance)),确认顶部形成。
Args:
code: 股票代码,如 '000001.SZ'
date: 计算截止日期,格式 'YYYY-MM-DD'
period: 回溯K线根数,默认 30
tolerance: 两高点价格允许偏差比例 及 回落确认比例,默认 0.03(即 3%)
min_decline: 颈线相对两顶均价的最小回落比例,默认 0.03(即 3%)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
int: 1 表示当日存在顶部形态,0 表示未出现;数据不足 period 根时返回 None
"""
cached = _get_cached_signal(code, 'TOP_PATTERN', period, date, use_adjusted)
if cached is not None:
return cached
klines = _get_klines_before_date(code, date, period)
if len(klines) < period:
return None
if use_adjusted:
klines = _adjust_klines(klines, _get_adj_factors_for_klines(klines))
highs = [k.high for k in klines]
lows = [k.low for k in klines]
closes = [k.close for k in klines]
# 找局部高点(至少需要 2 个)
peak_idxs = _find_local_highs(highs, window=3)
if len(peak_idxs) < 2:
_save_signal(code, 'TOP_PATTERN', period, date, 0, use_adjusted)
return 0
# 取高度最高的两个局部高点,按时间排序(idx1 更早,idx2 更晚)
sorted_by_height = sorted(peak_idxs, key=lambda i: highs[i], reverse=True)
idx1, idx2 = sorted(sorted_by_height[:2]) # 按时间先后排序
peak1 = highs[idx1]
peak2 = highs[idx2]
avg_peak = (peak1 + peak2) / 2.0
# 两顶高度相近
if avg_peak == 0 or abs(peak1 - peak2) / avg_peak > tolerance:
_save_signal(code, 'TOP_PATTERN', period, date, 0, use_adjusted)
return 0
# 两顶之间的颈线(最低价)
neckline = min(lows[idx1: idx2 + 1])
if (avg_peak - neckline) / avg_peak < min_decline:
_save_signal(code, 'TOP_PATTERN', period, date, 0, use_adjusted)
return 0
# 第二高点在后半段,且当前价格已从第二顶回落
half = period // 2
value = 1 if (
idx2 >= half
and closes[-1] < peak2 * (1 - tolerance)
) else 0
_save_signal(code, 'TOP_PATTERN', period, date, value, use_adjusted)
return value
# ── 别名函数 ─────────────────────────────────────────────────────────────────
def get_qiming_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""启明星(早晨之星,Morning Star)—— get_morning_star 的别名
与 get_morning_star 完全等价,共享同一缓存键(MORNING_STAR)。
Returns:
int: 1 表示当日出现启明星(早晨之星),0 表示未出现;数据不足 3 根时返回 None
"""
return get_morning_star(code, date, large_body_ratio, doji_ratio, penetrate_ratio, use_adjusted)
def get_huanghun_star(
code: str,
date: str,
large_body_ratio: float = 0.6,
doji_ratio: float = 0.3,
penetrate_ratio: float = 0.5,
use_adjusted: bool = True,
) -> Optional[int]:
"""黄昏星(黄昏之星,Evening Star)—— get_evening_star 的别名
与 get_evening_star 完全等价,共享同一缓存键(EVENING_STAR)。
Returns:
int: 1 表示当日出现黄昏星(黄昏之星),0 表示未出现;数据不足 3 根时返回 None
"""
return get_evening_star(code, date, large_body_ratio, doji_ratio, penetrate_ratio, use_adjusted)
FILE:scripts/stock_api.py
"""
stock_api.py — 股票数据与回测API接口
策略逻辑可调用此模块中的所有函数来获取股票数据。
当前为模拟实现,真实环境中替换为实际数据源即可。
策略逻辑可调用此模块中的所有函数来获取股票数据和技术指标。
本模块是项目对外的唯一接口,其他模块的实现在内部4个独立文件中。
使用示例:
from stock_api import StockApi
api = StockApi()
# 获取日线行情表
klines = api.get_daily_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# 获取技术指标
sma = api.get_sma('600519.SH', '2026-03-01', 20)
rsi = api.get_rsi('600519.SH', '2026-03-01', 14)
# 获取性能指标
report = api.calculate_metrics([1000000, 1050000, 1020000], trades, 1000000, 30)
"""
import sys
from typing import Optional, List, Dict, Union
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from sqlalchemy import text
from realtime_data_featcher import (
RealtimeStockQuote,
RealTimeDataFetcher
)
from data_fetcher import (
query_stock_basic,
query_daily_kline,
query_hour_kline,
query_weekly_kline,
query_monthly_kline,
query_daily_basic,
query_income,
query_stock_limit,
query_daily_limit_list,
query_daily_bomb_list,
query_sector_stock_map,
query_top_list,
query_top_inst,
query_sector_flow_daily,
query_index_basic,
query_index_daily,
query_index_weekly,
query_index_monthly,
)
from define import (
DailyKline,
HourKline,
WeeklyKline,
MonthlyKline,
StockBasic,
DailyBasic,
Income,
StockLimit,
DailyLimitList,
DailyBombList,
SectorStockMap,
TopList,
TopInst,
SectorFlowDaily,
IndexBasic,
IndexDaily,
IndexWeekly,
IndexMonthly,
AppVersion,
TokenCheckResult
)
from data_fetcher import getEngine
from formulaicAlphas import AlphaDataLoader, Alpha101
from indicators import (
get_sma,
get_ema,
get_rsi,
get_bollinger_bands,
get_macd,
get_atr,
get_wma,
get_tema,
get_mom,
get_roc,
get_cci,
get_obv,
get_volume,
get_kdj,
get_dmi,
get_trix,
get_sar,
get_williams_r,
get_psycho,
get_bias,
get_tr,
get_natr,
get_vwap,
get_ad,
get_adosc,
get_mfi,
get_cmo,
get_rocp,
get_rocr,
get_aroon,
get_ultosc,
get_dema,
get_kama,
get_midpoint,
get_midprice,
get_pvi,
get_nvi,
get_ppo,
get_roc_r,
get_stoch,
get_stochf,
get_stochrsi,
get_trange,
get_ma_channel,
get_donchian,
get_keltner,
get_bbands_width,
get_bbands_pct,
get_linearreg,
get_linearreg_angle,
get_linearreg_intercept,
get_linearreg_slope,
get_stddev,
get_tsf,
get_var,
get_correl,
get_beta,
get_ht_dcperiod,
get_ht_dcphase,
get_ht_phasor,
get_ht_sine,
get_ht_trendmode,
get_typical_price,
get_median_price,
get_weighted_close,
get_avgp,
get_asi,
get_vr,
get_ar,
get_br,
get_brar,
get_dpo,
get_bbi,
get_mass,
get_xue_channel,
get_consecutive_rise,
get_consecutive_fall,
get_bomb_board,
get_bomb_board_count,
get_consecutive_limit_up,
init_indicators_db,
)
from signals import (
get_morning_star,
get_qiming_star,
get_evening_star,
get_huanghun_star,
get_three_white_soldiers,
get_three_black_crows,
get_dark_cloud_cover,
get_rounding_bottom,
get_ascending_triangle,
get_top_pattern,
init_signals_db,
)
from metrics import (
get_max_drawdown,
get_max_drawdown_pct,
get_annualized_return,
get_total_return,
get_sharpe_ratio,
get_win_rate,
get_profit_loss_ratio,
get_calmar_ratio,
get_volatility,
get_trade_stats,
generate_report,
)
from backtest_tools import (
Position,
simulate_trade,
calculate_trade_cost,
create_position,
update_position,
get_position_value,
get_position_profit,
calculate_portfolio_value,
get_portfolio_positions,
build_equity_curve,
calculate_daily_returns,
should_buy,
should_sell,
calculate_drawdown,
buy,
sell,
)
import data_fetcher, config
from track_logger import TrackLogger
class StockApi:
"""
股票数据与回测API接口
本类是项目对外提供的唯一接口,封装了以下功能:
- 股票基础信息查询
- K线数据获取
- 技术指标计算(带缓存)
- 性能指标计算
- 回测工具函数
"""
def __init__(self, logger:TrackLogger = None):
if logger is None:
import os as _os
_log_path = _os.path.join(config.get_cache_dir(), 'track.log')
logger = TrackLogger(_log_path)
self.track_logger = logger
# ============================================================
# 工具类
# ============================================================
@staticmethod
def get_user_token() -> str:
"""
获取用户当前token
返回值: 用户token(从环境变量 BITSOUL_TOKEN 或 BITSOUL_TOKEN_ENV_FILE 获取)
"""
return config.get_token()
@staticmethod
def set_user_token(token: str):
"""
设置用户当前token
参数:
token 设置的token
"""
return config.set_token(token=token)
# ============================================================
# 初始化
# ============================================================
def initialSetup(self):
self.track_logger.write("initialSetup()")
data_fetcher.init_db()
data_fetcher.syn_table_datas()
init_indicators_db()
init_signals_db()
def update_vip_basic_data(self):
"""
更新vip基础数据包
"""
data_fetcher.syn_vip_basic_data()
def update_data(self):
"""
更新本地数据库,获取最新的增量数据。
会对比服务器上的 patch 列表,下载并导入缺失的数据。
"""
self.track_logger.write("update_data()")
data_fetcher.syn_table_datas()
# ============================================================
# 股票基础信息类接口
# ============================================================
def get_all_symbols(self) -> List[str]:
"""
获取所有股票代码列表。
:return: 股票代码列表,格式如 ['000001.SZ', '600519.SH', ...]
"""
self.track_logger.write("get_all_symbols()")
stocks = query_stock_basic()
return [s.ts_code for s in stocks]
def get_symbol_basic_infomation(self, ts_code: str) -> Optional[StockBasic]:
"""
根据股票代码获取股票基础信息
:param ts_code: 股票代码,如 000001.SZ
:return: 股票基础信息数据结构,没查询到则返回None
"""
self.track_logger.write(f"get_symbol_basic_infomation(ts_code={ts_code!r})")
stocks = query_stock_basic(ts_code=ts_code)
if len(stocks) > 0:
return stocks[0]
else:
return None
# ─────────────────────────────────────────────
# 价格行情类接口
# ─────────────────────────────────────────────
def get_realtime_stock_info(self, code:str) -> RealtimeStockQuote:
"""
获取指定股票代码的股票实时信息
参数:
code 股票代码,如000001.SZ
返回:
RealtimeStockQuote 实时股票报价信息
"""
self.track_logger.write(f"get_realtime_stock_info(code={code!r})")
return RealTimeDataFetcher().request_stock_info(code)
def query_income(
self,
ts_codes: List[str] = [],
report_type: Optional[str] = None,
end_date: Optional[str] = None,
start_end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "end_date ASC",
) -> List[Income]:
"""
根据条件获取利润信息。
参数:
ts_codes 按股票代码列表过滤
report_type 按报告类型精确过滤(如 "1" 表示合并报表)
end_date 按报告期结束日期精确过滤,格式 YYYY-MM-DD
start_end_date 按报告期结束日期范围过滤下限(含),格式 YYYY-MM-DD
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "end_date ASC"
返回:
List[Income] 符合条件的利润表对象列表
示例:
# 查询某只股票全部利润表(合并报表)
records = query_income(ts_codes=["000001.SZ"], report_type="1")
# 查询某报告期全市场数据
records = query_income(end_date="20231231")
# 查询最新一期
records = query_income(ts_codes=["000001.SZ"], order_by="end_date DESC", limit=1)
"""
self.track_logger.write(f"query_income(ts_codes={ts_codes!r}, report_type={report_type!r}, end_date={end_date!r}, start_end_date={start_end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_income(
ts_codes=ts_codes,
report_type=report_type,
end_date=end_date,
start_end_date=start_end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_basic(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBasic]:
"""
查询每日基本面指标列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBasic] 符合条件的每日基本面指标对象列表
示例:
# 查询某只股票全部历史基本面数据
basics = query_daily_basic(ts_codes=["000001.SZ"])
# 查询某天全市场基本面数据
basics = query_daily_basic(trade_date="2024-06-03")
"""
self.track_logger.write(f"get_daily_basic(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_daily_basic(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_stock_limit(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[StockLimit]:
"""
查询每日涨跌停价格列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[StockLimit] 符合条件的每日涨跌停价格对象列表
示例:
# 查询某只股票的涨跌停价格历史
limits = api.get_stock_limit(ts_codes=["000001.SZ"])
# 查询某天全市场涨跌停价格
limits = api.get_stock_limit(trade_date="2024-06-03")
"""
self.track_logger.write(f"get_stock_limit(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_stock_limit(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_limit_list(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyLimitList]:
"""
查询每日涨跌停榜单列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit_type 按榜单类型过滤(U=涨停, D=跌停)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyLimitList] 符合条件的每日涨跌停榜单对象列表
示例:
# 查询某天所有涨停股
records = api.get_daily_limit_list(trade_date="2024-06-03", limit_type="U")
# 查询某只股票历史上榜记录
records = api.get_daily_limit_list(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_daily_limit_list(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit_type={limit_type!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_daily_limit_list(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit_type=limit_type,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_bomb_list(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
bomb_type: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[DailyBombList]:
"""
查询每日炸板榜单列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
bomb_type 按炸板类型过滤(U=曾涨停, D=曾跌停/撬板)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
返回:
List[DailyBombList] 符合条件的每日炸板榜单对象列表
示例:
# 查询某天所有炸板(曾涨停)股票
records = api.get_daily_bomb_list(trade_date="2024-06-03", bomb_type="U")
# 查询某只股票历史炸板记录
records = api.get_daily_bomb_list(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_daily_bomb_list(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, bomb_type={bomb_type!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_daily_bomb_list(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
bomb_type=bomb_type,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_sector_stock_map(
self,
sector_codes: List[str] = [],
stock_codes: List[str] = [],
source: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[SectorStockMap]:
"""
查询板块成分股映射列表
参数:
sector_codes 按板块代码列表过滤
stock_codes 按股票代码列表过滤
source 按数据来源精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询某个板块下的所有股票
records = api.get_sector_stock_map(sector_codes=["BK0475"])
# 查询某只股票归属的所有板块
records = api.get_sector_stock_map(stock_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_sector_stock_map(sector_codes={sector_codes!r}, stock_codes={stock_codes!r}, source={source!r}, limit={limit!r}, offset={offset!r})")
return query_sector_stock_map(
sector_codes=sector_codes,
stock_codes=stock_codes,
source=source,
limit=limit,
offset=offset,
)
def get_top_list(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopList]:
"""
查询龙虎榜每日明细列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天龙虎榜数据
records = api.get_top_list(trade_date="2024-06-03")
# 查询某只股票历史上榜记录
records = api.get_top_list(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_top_list(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_top_list(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_top_inst(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
side: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[TopInst]:
"""
查询龙虎榜机构交易明细列表
参数:
ts_codes 按股票代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
side 按买卖类型过滤("0"=买入最大的前5名, "1"=卖出最大的前5名)
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天机构交易明细
records = api.get_top_inst(trade_date="2024-06-03")
# 查询某只股票历史机构上榜记录
records = api.get_top_inst(ts_codes=["000001.SZ"])
"""
self.track_logger.write(f"get_top_inst(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, side={side!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_top_inst(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
side=side,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_sector_flow_daily(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[SectorFlowDaily]:
"""
查询板块资金流向列表
参数:
ts_codes 按板块代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询某天所有板块资金流向
records = api.get_sector_flow_daily(trade_date="2024-06-03")
# 查询某个板块历史资金流向
records = api.get_sector_flow_daily(ts_codes=["BK0475"])
"""
self.track_logger.write(f"get_sector_flow_daily(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_sector_flow_daily(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_index_basic(
self,
ts_code: Optional[str] = None,
market: Optional[str] = None,
publisher: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
) -> List[IndexBasic]:
"""
查询指数基础信息列表
参数:
ts_code 按指数代码精确过滤
market 按市场精确过滤
publisher 按发布方精确过滤
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
示例:
# 查询所有指数
records = api.get_index_basic()
# 查询上证指数信息
records = api.get_index_basic(ts_code="000001.SH")
"""
self.track_logger.write(f"get_index_basic(ts_code={ts_code!r}, market={market!r}, publisher={publisher!r}, limit={limit!r}, offset={offset!r})")
return query_index_basic(
ts_code=ts_code,
market=market,
publisher=publisher,
limit=limit,
offset=offset,
)
def get_index_daily(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexDaily]:
"""
查询指数日线行情列表
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体交易日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数历史日线
records = api.get_index_daily(ts_codes=["000001.SH"])
# 查询某天所有指数行情
records = api.get_index_daily(trade_date="2024-06-03")
"""
self.track_logger.write(f"get_index_daily(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_index_daily(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_index_weekly(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexWeekly]:
"""
查询指数周线行情列表
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数周线
records = api.get_index_weekly(ts_codes=["000001.SH"])
"""
self.track_logger.write(f"get_index_weekly(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_index_weekly(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_index_monthly(
self,
ts_codes: List[str] = [],
trade_date: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
limit: Optional[int] = None,
offset: int = 0,
order_by: str = "trade_date ASC",
) -> List[IndexMonthly]:
"""
查询指数月线行情列表
参数:
ts_codes 按指数代码列表过滤
trade_date 按具体日期精确过滤,格式 "YYYY-MM-DD"
start_date 按日期范围过滤下限(含),格式 "YYYY-MM-DD"
end_date 按日期范围过滤上限(含),格式 "YYYY-MM-DD"
limit 返回最大记录数;为 None 表示不限
offset 分页偏移量,默认 0
order_by 排序表达式,默认 "trade_date ASC"
示例:
# 查询上证指数月线
records = api.get_index_monthly(ts_codes=["000001.SH"])
"""
self.track_logger.write(f"get_index_monthly(ts_codes={ts_codes!r}, trade_date={trade_date!r}, start_date={start_date!r}, end_date={end_date!r}, limit={limit!r}, offset={offset!r}, order_by={order_by!r})")
return query_index_monthly(
ts_codes=ts_codes,
trade_date=trade_date,
start_date=start_date,
end_date=end_date,
limit=limit,
offset=offset,
order_by=order_by,
)
def get_daily_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[DailyKline]:
"""
获取指定日期范围内的股票日线行情(按日期升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: 收盘价列表,无数据返回空列表
"""
self.track_logger.write(f"get_daily_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = query_daily_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC",
)
return klines
def get_hour_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[HourKline]:
"""
获取指定日期范围内的股票小时线行情(按日期和时间升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: HourKline 列表,无数据返回空列表
"""
self.track_logger.write(f"get_hour_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
return query_hour_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC, time ASC",
)
def get_weekly_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[WeeklyKline]:
"""
获取指定日期范围内的股票周线行情(按日期升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: WeeklyKline 列表,无数据返回空列表
"""
self.track_logger.write(f"get_weekly_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
return query_weekly_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC",
)
def get_monthly_kline(self, symbols: List[str], start_date: str, end_date: str) -> List[MonthlyKline]:
"""
获取指定日期范围内的股票月线行情(按日期升序)。
:param symbols: 股票代码列表,可以为空,空表示获取所有股票行情
:param start_date: 起始日期,格式 YYYY-MM-DD
:param end_date: 结束日期,格式 YYYY-MM-DD
:return: MonthlyKline 列表,无数据返回空列表
"""
self.track_logger.write(f"get_monthly_kline(symbols={symbols!r}, start_date={start_date!r}, end_date={end_date!r})")
return query_monthly_kline(
codes=symbols,
start_date=start_date, end_date=end_date,
order_by="date ASC",
)
def get_daily_close_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线收盘价列表(按日期升序)。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
收盘价列表
Example:
prices = api.get_daily_close_prices('600519.SH', '2026-01-01', '2026-03-01')
"""
self.track_logger.write(f"get_daily_close_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.close for k in klines]
def get_daily_open_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线开盘价列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线开盘价列表
"""
self.track_logger.write(f"get_daily_open_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.open for k in klines]
def get_daily_high_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线最高价列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线最高价列表
"""
self.track_logger.write(f"get_daily_high_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.high for k in klines]
def get_daily_low_prices(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线最低价列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
最低价列表
"""
self.track_logger.write(f"get_daily_low_prices(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.low for k in klines]
def get_daily_volumes(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线成交量列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线成交量列表
"""
self.track_logger.write(f"get_daily_volumes(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.volume for k in klines]
def get_daily_pct_chg(self, code: str, start_date: str, end_date: str) -> List[float]:
"""
获取指定股票的日线涨跌幅列表。
Args:
code: 股票代码
start_date: 起始日期
end_date: 结束日期
Returns:
日线涨跌幅列表(%)
"""
self.track_logger.write(f"get_daily_pct_chg(code={code!r}, start_date={start_date!r}, end_date={end_date!r})")
klines = self.get_daily_kline(code, start_date, end_date)
return [k.pctChg for k in klines]
# ============================================================
# 技术指标类接口(带缓存)
# ============================================================
def get_sma(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取简单移动平均SMA。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 周期,默认20
Returns:
SMA值,若数据不足返回None
Example:
sma = api.get_sma('600519.SH', '2026-03-01', 20)
"""
self.track_logger.write(f"get_sma(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_sma(code, date, period, use_adjusted)
def get_ema(self, code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""
获取指数移动平均EMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认12
Returns:
EMA值,若数据不足返回None
"""
self.track_logger.write(f"get_ema(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ema(code, date, period, use_adjusted)
def get_rsi(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取相对强弱指标RSI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
RSI值(0-100),若数据不足返回None
Example:
rsi = api.get_rsi('600519.SH', '2026-03-01', 14)
if rsi and rsi < 30:
print('超卖')
"""
self.track_logger.write(f"get_rsi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rsi(code, date, period, use_adjusted)
def get_bollinger_bands(self, code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取布林带指标。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
std_dev: 标准差倍数,默认2
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
Example:
bb = api.get_bollinger_bands('600519.SH', '2026-03-01')
if bb and close > bb['upper']:
print('突破上轨')
"""
self.track_logger.write(f"get_bollinger_bands(code={code!r}, date={date!r}, period={period!r}, std_dev={std_dev!r}, use_adjusted={use_adjusted!r})")
return get_bollinger_bands(code, date, period, std_dev, use_adjusted)
def get_macd(self, code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取MACD指标。
Args:
code: 股票代码
date: 计算日期
fast: 快线周期,默认12
slow: 慢线周期,默认26
signal: 信号线周期,默认9
Returns:
字典 {'macd': MACD线, 'signal': 信号线, 'histogram': 柱状图},若数据不足返回None
Example:
macd = api.get_macd('600519.SH', '2026-03-01')
if macd and macd['histogram'] > 0:
print('多头')
"""
self.track_logger.write(f"get_macd(code={code!r}, date={date!r}, fast={fast!r}, slow={slow!r}, signal={signal!r}, use_adjusted={use_adjusted!r})")
return get_macd(code, date, fast, slow, signal, use_adjusted)
def get_atr(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取平均真实波幅ATR。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
ATR值,若数据不足返回None
"""
self.track_logger.write(f"get_atr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_atr(code, date, period, use_adjusted)
def get_wma(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取加权移动平均WMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
WMA值,若数据不足返回None
"""
self.track_logger.write(f"get_wma(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_wma(code, date, period, use_adjusted)
def get_tema(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取三重指数移动平均TEMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
TEMA值,若数据不足返回None
"""
self.track_logger.write(f"get_tema(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_tema(code, date, period, use_adjusted)
def get_mom(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取动量指标MOM。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
MOM值,若数据不足返回None
"""
self.track_logger.write(f"get_mom(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_mom(code, date, period, use_adjusted)
def get_roc(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取变动率指标ROC(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROC值(%),若数据不足返回None
"""
self.track_logger.write(f"get_roc(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_roc(code, date, period, use_adjusted)
def get_cci(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取顺势指标CCI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
CCI值,若数据不足返回None
"""
self.track_logger.write(f"get_cci(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_cci(code, date, period, use_adjusted)
def get_obv(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取能量潮OBV。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
OBV值,若数据不足返回None
"""
self.track_logger.write(f"get_obv(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_obv(code, date, period, use_adjusted)
def get_volume(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取成交量指标。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
字典 {'current': 当前成交量, 'sma': 成交量均线},若数据不足返回None
"""
self.track_logger.write(f"get_volume(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_volume(code, date, period, use_adjusted)
def get_kdj(self, code: str, date: str, n: int = 9, m1: int = 3, m2: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取随机指标KDJ。
Args:
code: 股票代码
date: 计算日期
n: 周期,默认9
m1: 平滑参数1,默认3
m2: 平滑参数2,默认3
Returns:
字典 {'k': K值, 'd': D值, 'j': J值},若数据不足返回None
"""
self.track_logger.write(f"get_kdj(code={code!r}, date={date!r}, n={n!r}, m1={m1!r}, m2={m2!r}, use_adjusted={use_adjusted!r})")
return get_kdj(code, date, n, m1, m2, use_adjusted)
def get_dmi(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取趋向指标DMI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
字典 {'pdi': +DI, 'mdi': -DI, 'adx': ADX},若数据不足返回None
"""
self.track_logger.write(f"get_dmi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_dmi(code, date, period, use_adjusted)
def get_trix(self, code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""
获取三重指数平滑移动平均TRIX(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认12
Returns:
TRIX值(%),若数据不足返回None
"""
self.track_logger.write(f"get_trix(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_trix(code, date, period, use_adjusted)
def get_sar(self, code: str, date: str, af_start: float = 0.02, af_max: float = 0.2, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取抛物线转向SAR。
Args:
code: 股票代码
date: 计算日期
af_start: 加速因子起始值,默认0.02
af_max: 加速因子最大值,默认0.2
Returns:
字典 {'sar': SAR值, 'trend': 趋势},若数据不足返回None
"""
self.track_logger.write(f"get_sar(code={code!r}, date={date!r}, af_start={af_start!r}, af_max={af_max!r}, use_adjusted={use_adjusted!r})")
return get_sar(code, date, af_start, af_max, use_adjusted)
def get_williams_r(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取威廉指标WR(0-100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
WR值(0-100),0表示超买,100表示超卖,若数据不足返回None
"""
self.track_logger.write(f"get_williams_r(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_williams_r(code, date, period, use_adjusted)
def get_psycho(self, code: str, date: str, period: int = 12, use_adjusted: bool = True) -> Optional[float]:
"""
获取心理线PSY(0-100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认12
Returns:
PSY值(0-100),若数据不足返回None
"""
self.track_logger.write(f"get_psycho(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_psycho(code, date, period, use_adjusted)
def get_bias(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取乖离率BIAS(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
BIAS值(%),若数据不足返回None
"""
self.track_logger.write(f"get_bias(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_bias(code, date, period, use_adjusted)
def get_tr(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取真实波幅TR。
Args:
code: 股票代码
date: 计算日期
Returns:
TR值,若数据不足返回None
"""
self.track_logger.write(f"get_tr(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_tr(code, date, use_adjusted)
def get_natr(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取归一化平均真实波幅NATR(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
NATR值(%),若数据不足返回None
"""
self.track_logger.write(f"get_natr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_natr(code, date, period, use_adjusted)
def get_vwap(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取成交量加权平均价VWAP。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
VWAP值,若数据不足返回None
"""
self.track_logger.write(f"get_vwap(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_vwap(code, date, period, use_adjusted)
def get_ad(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取累积/派发线AD。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
AD值,若数据不足返回None
"""
self.track_logger.write(f"get_ad(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ad(code, date, period, use_adjusted)
def get_adosc(self, code: str, date: str, fast: int = 3, slow: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取震荡指标ADOSC。
Args:
code: 股票代码
date: 计算日期
fast: 快线周期,默认3
slow: 慢线周期,默认10
Returns:
ADOSC值,若数据不足返回None
"""
self.track_logger.write(f"get_adosc(code={code!r}, date={date!r}, fast={fast!r}, slow={slow!r}, use_adjusted={use_adjusted!r})")
return get_adosc(code, date, fast, slow, use_adjusted)
def get_mfi(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取资金流量指标MFI(0-100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
MFI值(0-100),若数据不足返回None
"""
self.track_logger.write(f"get_mfi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_mfi(code, date, period, use_adjusted)
def get_cmo(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取钱德动量摆动指标CMO(-100 to 100)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
CMO值(-100 to 100),若数据不足返回None
"""
self.track_logger.write(f"get_cmo(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_cmo(code, date, period, use_adjusted)
def get_rocp(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取价格变动率ROCP。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROCP值,若数据不足返回None
"""
self.track_logger.write(f"get_rocp(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rocp(code, date, period, use_adjusted)
def get_rocr(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取价格变动率比ROCR。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROCR值,若数据不足返回None
"""
self.track_logger.write(f"get_rocr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rocr(code, date, period, use_adjusted)
def get_aroon(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取阿隆指标AROON。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
字典 {'up': AROON_UP, 'down': AROON_DOWN, 'osc': AROON_OSC},若数据不足返回None
"""
self.track_logger.write(f"get_aroon(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_aroon(code, date, period, use_adjusted)
def get_ultosc(self, code: str, date: str, period1: int = 7, period2: int = 14, period3: int = 28, use_adjusted: bool = True) -> Optional[float]:
"""
获取终极振荡器ULTOSC(0-100)。
Args:
code: 股票代码
date: 计算日期
period1: 周期1,默认7
period2: 周期2,默认14
period3: 周期3,默认28
Returns:
ULTOSC值(0-100),若数据不足返回None
"""
self.track_logger.write(f"get_ultosc(code={code!r}, date={date!r}, period1={period1!r}, period2={period2!r}, period3={period3!r}, use_adjusted={use_adjusted!r})")
return get_ultosc(code, date, period1, period2, period3, use_adjusted)
def get_dema(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取双重指数移动平均DEMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
DEMA值,若数据不足返回None
"""
self.track_logger.write(f"get_dema(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_dema(code, date, period, use_adjusted)
def get_kama(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取考夫曼自适应移动平均KAMA。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
KAMA值,若数据不足返回None
"""
self.track_logger.write(f"get_kama(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_kama(code, date, period, use_adjusted)
def get_midpoint(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取中点价格MIDPOINT。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
MIDPOINT值,若数据不足返回None
"""
self.track_logger.write(f"get_midpoint(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_midpoint(code, date, period, use_adjusted)
def get_midprice(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取中点价格MIDPRICE。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
MIDPRICE值,若数据不足返回None
"""
self.track_logger.write(f"get_midprice(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_midprice(code, date, period, use_adjusted)
def get_pvi(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取正成交量指标PVI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
PVI值,若数据不足返回None
"""
self.track_logger.write(f"get_pvi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_pvi(code, date, period, use_adjusted)
def get_nvi(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取负成交量指标NVI。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
NVI值,若数据不足返回None
"""
self.track_logger.write(f"get_nvi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_nvi(code, date, period, use_adjusted)
def get_ppo(self, code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取价格震荡指标PPO。
Args:
code: 股票代码
date: 计算日期
fast: 快线周期,默认12
slow: 慢线周期,默认26
signal: 信号线周期,默认9
Returns:
字典 {'ppo': PPO线, 'signal': 信号线, 'histogram': 柱状图},若数据不足返回None
"""
self.track_logger.write(f"get_ppo(code={code!r}, date={date!r}, fast={fast!r}, slow={slow!r}, signal={signal!r}, use_adjusted={use_adjusted!r})")
return get_ppo(code, date, fast, slow, signal, use_adjusted)
def get_roc_r(self, code: str, date: str, period: int = 10, use_adjusted: bool = True) -> Optional[float]:
"""
获取变动率ROC_R。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认10
Returns:
ROC_R值,若数据不足返回None
"""
self.track_logger.write(f"get_roc_r(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_roc_r(code, date, period, use_adjusted)
def get_stoch(self, code: str, date: str, fastk_period: int = 14, slowk_period: int = 3, slowd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取随机指标STOCH。
Args:
code: 股票代码
date: 计算日期
fastk_period: 快速K周期,默认14
slowk_period: 慢速K周期,默认3
slowd_period: 慢速D周期,默认3
Returns:
字典 {'slowk': 慢速K, 'slowd': 慢速D},若数据不足返回None
"""
self.track_logger.write(f"get_stoch(code={code!r}, date={date!r}, fastk_period={fastk_period!r}, slowk_period={slowk_period!r}, slowd_period={slowd_period!r}, use_adjusted={use_adjusted!r})")
return get_stoch(code, date, fastk_period, slowk_period, slowd_period, use_adjusted)
def get_stochf(self, code: str, date: str, fastk_period: int = 14, fastd_period: int = 3, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取快速随机指标STOCHF。
Args:
code: 股票代码
date: 计算日期
fastk_period: 快速K周期,默认14
fastd_period: 快速D周期,默认3
Returns:
字典 {'fastk': 快速K, 'fastd': 快速D},若数据不足返回None
"""
self.track_logger.write(f"get_stochf(code={code!r}, date={date!r}, fastk_period={fastk_period!r}, fastd_period={fastd_period!r}, use_adjusted={use_adjusted!r})")
return get_stochf(code, date, fastk_period, fastd_period, use_adjusted)
def get_stochrsi(self, code: str, date: str, rsi_period: int = 14, stoch_period: int = 14, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取随机RSI指标STOCHRSI。
Args:
code: 股票代码
date: 计算日期
rsi_period: RSI周期,默认14
stoch_period: 随机周期,默认14
Returns:
字典 {'fastk': K, 'fastd': D},若数据不足返回None
"""
self.track_logger.write(f"get_stochrsi(code={code!r}, date={date!r}, rsi_period={rsi_period!r}, stoch_period={stoch_period!r}, use_adjusted={use_adjusted!r})")
return get_stochrsi(code, date, rsi_period, stoch_period, use_adjusted)
def get_trange(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取真实波幅TRANGE。
Args:
code: 股票代码
date: 计算日期
Returns:
TRANGE值,若数据不足返回None
"""
self.track_logger.write(f"get_trange(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_trange(code, date, use_adjusted)
def get_ma_channel(self, code: str, date: str, period: int = 20, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取移动平均通道。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
multiplier: 倍数,默认2.0
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
"""
self.track_logger.write(f"get_ma_channel(code={code!r}, date={date!r}, period={period!r}, multiplier={multiplier!r}, use_adjusted={use_adjusted!r})")
return get_ma_channel(code, date, period, multiplier, use_adjusted)
def get_donchian(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取唐奇安通道。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
"""
self.track_logger.write(f"get_donchian(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_donchian(code, date, period, use_adjusted)
def get_keltner(self, code: str, date: str, ma_period: int = 20, atr_period: int = 10, multiplier: float = 2.0, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取凯尔特纳通道。
Args:
code: 股票代码
date: 计算日期
ma_period: MA周期,默认20
atr_period: ATR周期,默认10
multiplier: 倍数,默认2.0
Returns:
字典 {'upper': 上轨, 'middle': 中轨, 'lower': 下轨},若数据不足返回None
"""
self.track_logger.write(f"get_keltner(code={code!r}, date={date!r}, ma_period={ma_period!r}, atr_period={atr_period!r}, multiplier={multiplier!r}, use_adjusted={use_adjusted!r})")
return get_keltner(code, date, ma_period, atr_period, multiplier, use_adjusted)
def get_bbands_width(self, code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""
获取布林带宽度BBANDS_WIDTH(%)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
std_dev: 标准差倍数,默认2
Returns:
BBANDS_WIDTH值(%),若数据不足返回None
"""
self.track_logger.write(f"get_bbands_width(code={code!r}, date={date!r}, period={period!r}, std_dev={std_dev!r}, use_adjusted={use_adjusted!r})")
return get_bbands_width(code, date, period, std_dev, use_adjusted)
def get_bbands_pct(self, code: str, date: str, period: int = 20, std_dev: int = 2, use_adjusted: bool = True) -> Optional[float]:
"""
获取布林带百分比位置BBANDS_PCT(0-1)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
std_dev: 标准差倍数,默认2
Returns:
BBANDS_PCT值(0-1),若数据不足返回None
"""
self.track_logger.write(f"get_bbands_pct(code={code!r}, date={date!r}, period={period!r}, std_dev={std_dev!r}, use_adjusted={use_adjusted!r})")
return get_bbands_pct(code, date, period, std_dev, use_adjusted)
def get_linearreg(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归预测值LINEARREG。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg(code, date, period, use_adjusted)
def get_linearreg_angle(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归角度LINEARREG_ANGLE。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG_ANGLE值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg_angle(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg_angle(code, date, period, use_adjusted)
def get_linearreg_intercept(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归截距LINEARREG_INTERCEPT。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG_INTERCEPT值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg_intercept(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg_intercept(code, date, period, use_adjusted)
def get_linearreg_slope(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取线性回归斜率LINEARREG_SLOPE。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
LINEARREG_SLOPE值,若数据不足返回None
"""
self.track_logger.write(f"get_linearreg_slope(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_linearreg_slope(code, date, period, use_adjusted)
def get_stddev(self, code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""
获取标准差STDDEV。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
nbdev: 标准差倍数,默认1
Returns:
STDDEV值,若数据不足返回None
"""
self.track_logger.write(f"get_stddev(code={code!r}, date={date!r}, period={period!r}, nbdev={nbdev!r}, use_adjusted={use_adjusted!r})")
return get_stddev(code, date, period, nbdev, use_adjusted)
def get_tsf(self, code: str, date: str, period: int = 14, use_adjusted: bool = True) -> Optional[float]:
"""
获取时间序列预测TSF。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认14
Returns:
TSF值,若数据不足返回None
"""
self.track_logger.write(f"get_tsf(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_tsf(code, date, period, use_adjusted)
def get_var(self, code: str, date: str, period: int = 20, nbdev: int = 1, use_adjusted: bool = True) -> Optional[float]:
"""
获取方差VAR。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
nbdev: 倍数,默认1
Returns:
VAR值,若数据不足返回None
"""
self.track_logger.write(f"get_var(code={code!r}, date={date!r}, period={period!r}, nbdev={nbdev!r}, use_adjusted={use_adjusted!r})")
return get_var(code, date, period, nbdev, use_adjusted)
def get_correl(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取相关系数CORREL(固定返回1.0)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
CORREL值(固定1.0)
"""
self.track_logger.write(f"get_correl(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_correl(code, date, period, use_adjusted)
def get_beta(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取贝塔系数BETA(固定返回1.0)。
Args:
code: 股票代码
date: 计算日期
period: 周期,默认20
Returns:
BETA值(固定1.0)
"""
self.track_logger.write(f"get_beta(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_beta(code, date, period, use_adjusted)
def get_ht_dcperiod(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取希尔伯特变换-主导周期HT_DCPERIOD。
Args:
code: 股票代码
date: 计算日期
Returns:
HT_DCPERIOD值,若数据不足返回None
"""
self.track_logger.write(f"get_ht_dcperiod(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_dcperiod(code, date, use_adjusted)
def get_ht_dcphase(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取希尔伯特变换-主导相位HT_DCPHASE。
Args:
code: 股票代码
date: 计算日期
Returns:
HT_DCPHASE值,若数据不足返回None
"""
self.track_logger.write(f"get_ht_dcphase(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_dcphase(code, date, use_adjusted)
def get_ht_phasor(self, code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取希尔伯特变换-相位分量HT_PHASOR。
Args:
code: 股票代码
date: 计算日期
Returns:
字典 {'inphase': 同相, 'quadrature': 正交},若数据不足返回None
"""
self.track_logger.write(f"get_ht_phasor(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_phasor(code, date, use_adjusted)
def get_ht_sine(self, code: str, date: str, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取希尔伯特变换-正弦波HT_SINE。
Args:
code: 股票代码
date: 计算日期
Returns:
字典 {'sine': 正弦, 'leadsine': 超前正弦},若数据不足返回None
"""
self.track_logger.write(f"get_ht_sine(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_sine(code, date, use_adjusted)
def get_ht_trendmode(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
获取希尔伯特变换-趋势模式HT_TRENDMODE。
Args:
code: 股票代码
date: 计算日期
Returns:
1=趋势, 0=周期,若数据不足返回None
"""
self.track_logger.write(f"get_ht_trendmode(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_ht_trendmode(code, date, use_adjusted)
def get_typical_price(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取典型价格TP = (High + Low + Close) / 3。
Args:
code: 股票代码
date: 计算日期
Returns:
典型价格,若数据不足返回None
"""
self.track_logger.write(f"get_typical_price(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_typical_price(code, date, use_adjusted)
def get_median_price(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取中位数价格 = (High + Low) / 2。
Args:
code: 股票代码
date: 计算日期
Returns:
中位数价格,若数据不足返回None
"""
self.track_logger.write(f"get_median_price(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_median_price(code, date, use_adjusted)
def get_weighted_close(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取加权收盘价 = (High + Low + 2 * Close) / 4。
Args:
code: 股票代码
date: 计算日期
Returns:
加权收盘价,若数据不足返回None
"""
self.track_logger.write(f"get_weighted_close(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_weighted_close(code, date, use_adjusted)
def get_avgp(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取平均价格 = (Open + High + Low + Close) / 4。
Args:
code: 股票代码
date: 计算日期
Returns:
平均价格,若数据不足返回None
"""
self.track_logger.write(f"get_avgp(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_avgp(code, date, use_adjusted)
def get_asi(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取累积摆动指数 ASI(Accumulative Swing Index)。
ASI 基于开高低收四价构造,用于衡量价格摆动的累积强度,
值域无固定范围,正值表示多头动能积累,负值表示空头动能积累。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 历史K线根数,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
ASI值(float),数据不足时返回None
Example:
asi = api.get_asi('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_asi(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_asi(code, date, period, use_adjusted)
def get_vr(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取成交量比率指标 VR(Volume Ratio)。
VR = (上涨日成交量之和 + 0.5 * 平盘日成交量) / (下跌日成交量之和 + 0.5 * 平盘日成交量)。
VR > 1 表示量价配合偏多,VR < 1 表示量价配合偏空,正常区间约 0.5 ~ 1.5。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
VR值(float),数据不足时返回None
Example:
vr = api.get_vr('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_vr(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_vr(code, date, period, use_adjusted)
def get_ar(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取人气指标 AR(Atmosphere/Rally)。
AR = sum(High - Open) / sum(Open - Low) * 100,衡量多空双方相对强弱,
100 为均衡,> 100 多头占优,< 100 空头占优,一般正常范围 50 ~ 150。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
AR值(float),数据不足时返回None
Example:
ar = api.get_ar('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_ar(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ar(code, date, period, use_adjusted)
def get_br(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[float]:
"""
获取意愿指标 BR(Buyer/Seller Ratio)。
BR = sum(High - Close_prev) / sum(Close_prev - Low) * 100,衡量多空力量对比,
与 AR 配合使用:BR > AR 多头强势,BR < AR 空头强势。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
BR值(float),数据不足时返回None
Example:
br = api.get_br('600519.SH', '2026-03-01', 26)
"""
self.track_logger.write(f"get_br(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_br(code, date, period, use_adjusted)
def get_brar(self, code: str, date: str, period: int = 26, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
同时获取人气指标 AR 和意愿指标 BR。
BRAR 是 AR 与 BR 的组合指标,二者结合判断市场多空力量:
- AR 衡量当日多空(基于开盘价)
- BR 衡量跨日多空(基于前收价)
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 统计周期,默认26
use_adjusted: 是否使用后复权价格,默认True
Returns:
字典 {'ar': float, 'br': float},数据不足时返回None
Example:
brar = api.get_brar('600519.SH', '2026-03-01', 26)
ar, br = brar['ar'], brar['br']
"""
self.track_logger.write(f"get_brar(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_brar(code, date, period, use_adjusted)
def get_dpo(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[float]:
"""
获取去趋势震荡指标 DPO(Detrended Price Oscillator)。
DPO = Close - SMA(Close, period)[-(period/2 + 1)],通过去除长期趋势
来识别价格的短中期周期性波动,穿越零轴可作为买卖参考信号。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
DPO值(float),数据不足时返回None
Example:
dpo = api.get_dpo('600519.SH', '2026-03-01', 20)
"""
self.track_logger.write(f"get_dpo(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_dpo(code, date, period, use_adjusted)
def get_bbi(self, code: str, date: str, use_adjusted: bool = True) -> Optional[float]:
"""
获取多空指标 BBI(Bull and Bear Index)。
BBI = (MA3 + MA6 + MA12 + MA24) / 4,是四条均线的算术平均值,
价格上穿 BBI 为多头信号,下穿为空头信号。固定使用 3/6/12/24 周期。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
BBI值(float),数据不足时返回None
Example:
bbi = api.get_bbi('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_bbi(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_bbi(code, date, use_adjusted)
def get_mass(self, code: str, date: str, period: int = 25, use_adjusted: bool = True) -> Optional[float]:
"""
获取梅斯线 MASS Index(Mass Index)。
MASS = sum(EMA(H-L, p) / EMA(EMA(H-L, p), p), period),通过高低价差
的双重 EMA 比值累加来识别价格反转,值升破 27 后回落至 26.5 以下为
"反转鼓"信号。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
ema_period: EMA平滑周期,默认9
period: 累积周期,默认25
use_adjusted: 是否使用后复权价格,默认True
Returns:
MASS值(float),数据不足时返回None
Example:
mass = api.get_mass('600519.SH', '2026-03-01', 9, 25)
"""
self.track_logger.write(f"get_mass(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_mass(code, date, period, use_adjusted)
def get_xue_channel(self, code: str, date: str, period: int = 20, use_adjusted: bool = True) -> Optional[Dict[str, float]]:
"""
获取雪球通道(薛斯通道)。
雪球通道由中轨(MA)、上轨(MA + k*ATR)、下轨(MA - k*ATR)构成,
价格突破上轨为超买,跌破下轨为超卖,常用于趋势跟踪和止损设置。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
period: 均线和ATR周期,默认20
use_adjusted: 是否使用后复权价格,默认True
Returns:
字典 {'upper': float, 'middle': float, 'lower': float},数据不足时返回None
Example:
ch = api.get_xue_channel('600519.SH', '2026-03-01', 20)
upper, middle, lower = ch['upper'], ch['middle'], ch['lower']
"""
self.track_logger.write(f"get_xue_channel(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_xue_channel(code, date, period, use_adjusted)
def get_consecutive_rise(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
获取截至指定日期连续上涨的天数。
从指定日期向前追溯,统计收盘价连续高于前一日的天数,
0 表示当日未上涨,1 表示仅当日上涨,依此类推。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
连续上涨天数(int ≥ 0),数据不足时返回None
Example:
n = api.get_consecutive_rise('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_consecutive_rise(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_consecutive_rise(code, date, use_adjusted)
def get_consecutive_fall(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
获取截至指定日期连续下跌的天数。
从指定日期向前追溯,统计收盘价连续低于前一日的天数,
0 表示当日未下跌,1 表示仅当日下跌,依此类推。
Args:
code: 股票代码
date: 计算日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
连续下跌天数(int ≥ 0),数据不足时返回None
Example:
n = api.get_consecutive_fall('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_consecutive_fall(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_consecutive_fall(code, date, use_adjusted)
def get_bomb_board(self, code: str, date: str) -> Optional[int]:
"""
判断指定日期是否发生炸板(曾涨停但收盘未封板)。
炸板意味着多头动能不足,当日虽冲击涨停但尾盘筹码松动,
可作为规避追高或观察多空博弈的参考信号。
不使用复权价格——炸板基于市场实际涨停价判断。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
Returns:
1=当日炸板,0=当日未炸板,None=非交易日或数据缺失
Example:
is_bomb = api.get_bomb_board('000001.SZ', '2026-03-01')
"""
self.track_logger.write(f"get_bomb_board(code={code!r}, date={date!r})")
return get_bomb_board(code, date)
def get_bomb_board_count(self, code: str, date: str, period: int = 20) -> Optional[int]:
"""
统计近N个交易日内的炸板次数。
炸板频繁说明个股多次冲板失败,多头信心不足;高频炸板的股票追高风险较大,
可结合连板数综合评估封板质量。
不使用复权价格——炸板基于市场实际涨停价判断。
Args:
code: 股票代码
date: 统计截止日期,格式 YYYY-MM-DD
period: 回溯交易日天数,默认20
Returns:
近period个交易日内炸板次数(int ≥ 0),数据不足时返回None
Example:
cnt = api.get_bomb_board_count('000001.SZ', '2026-03-01', 20)
"""
self.track_logger.write(f"get_bomb_board_count(code={code!r}, date={date!r}, period={period!r})")
return get_bomb_board_count(code, date, period)
def get_consecutive_limit_up(self, code: str, date: str) -> Optional[int]:
"""
获取截至指定日期的连续涨停天数(连板数)。
直接读取数据源维护的 limit_streak 字段,含一字板等特殊情形,
比自行计算更准确。连板数 >= 3 通常视为强势股,高连板存在高位风险。
不使用复权价格——涨停判断基于市场实际价格。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
Returns:
连板数(int ≥ 0,0表示当日未涨停),非交易日或数据缺失返回None
Example:
streak = api.get_consecutive_limit_up('000001.SZ', '2026-03-01')
"""
self.track_logger.write(f"get_consecutive_limit_up(code={code!r}, date={date!r})")
return get_consecutive_limit_up(code, date)
# ============================================================
# 裸K形态信号类接口(带缓存)
# ============================================================
def get_morning_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测早晨之星(Morning Star)形态。
早晨之星是底部反转信号,由三根K线构成:第一根大阴线、第二根十字星
(开低收于阴线实体下方)、第三根大阳线并收回阴线实体一半以上。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_morning_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_morning_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_morning_star(code, date, use_adjusted)
def get_qiming_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测启明星形态(早晨之星别名)。
启明星即早晨之星(Morning Star),共享同一缓存键 MORNING_STAR,
结果与 get_morning_star 完全一致。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_qiming_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_qiming_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_qiming_star(code, date, use_adjusted)
def get_evening_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测黄昏之星(Evening Star)形态。
黄昏之星是顶部反转信号,与早晨之星相反:第一根大阳线、第二根十字星
(开高收于阳线实体上方)、第三根大阴线并收回阳线实体一半以上。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_evening_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_evening_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_evening_star(code, date, use_adjusted)
def get_huanghun_star(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测黄昏星形态(黄昏之星别名)。
黄昏星即黄昏之星(Evening Star),共享同一缓存键 EVENING_STAR,
结果与 get_evening_star 完全一致。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_huanghun_star('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_huanghun_star(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_huanghun_star(code, date, use_adjusted)
def get_three_white_soldiers(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测红三兵(Three White Soldiers)形态。
红三兵是强势上涨信号,由连续三根阳线构成,每根实体占比≥50%,
上影线≤20%,且每根K线的开盘价在前一根实体范围内(逐步跳空上行)。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_three_white_soldiers('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_three_white_soldiers(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_three_white_soldiers(code, date, use_adjusted)
def get_three_black_crows(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测三只乌鸦(Three Black Crows)形态。
三只乌鸦是强势下跌信号,由连续三根阴线构成,每根实体占比≥50%,
下影线≤20%,且每根K线的开盘价在前一根实体范围内(逐步跳空下行)。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第三根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_three_black_crows('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_three_black_crows(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_three_black_crows(code, date, use_adjusted)
def get_dark_cloud_cover(self, code: str, date: str, use_adjusted: bool = True) -> Optional[int]:
"""
检测乌云盖顶(Dark Cloud Cover)形态。
乌云盖顶是顶部反转信号,由两根K线构成:第一根大阳线,第二根阴线
高开(开盘高于前收)后下跌,收盘深入阳线实体一半以上但不低于阳线开盘。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD(以该日为第二根K线)
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_dark_cloud_cover('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_dark_cloud_cover(code={code!r}, date={date!r}, use_adjusted={use_adjusted!r})")
return get_dark_cloud_cover(code, date, use_adjusted)
def get_rounding_bottom(self, code: str, date: str, period: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""
检测圆弧底(Rounding Bottom / Saucer)形态。
圆弧底是长期底部反转形态,价格在 period 根K线内呈现 U 形走势:
左侧缓慢下跌,底部盘整,右侧缓慢回升,最低点出现在中间三分之一区段。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
period: 观察窗口(K线根数),默认60
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_rounding_bottom('600519.SH', '2026-03-01', 60)
"""
self.track_logger.write(f"get_rounding_bottom(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_rounding_bottom(code, date, period, use_adjusted)
def get_ascending_triangle(self, code: str, date: str, period: int = 30, use_adjusted: bool = True) -> Optional[int]:
"""
检测上升三角形(Ascending Triangle)形态。
上升三角形是整理后向上突破的形态:水平阻力位保持不变,
支撑位持续上移(低点逐步抬高),是多头蓄力信号。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
period: 观察窗口(K线根数),默认30
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_ascending_triangle('600519.SH', '2026-03-01', 30)
"""
self.track_logger.write(f"get_ascending_triangle(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_ascending_triangle(code, date, period, use_adjusted)
def get_top_pattern(self, code: str, date: str, period: int = 60, use_adjusted: bool = True) -> Optional[int]:
"""
检测顶部形态(双顶 / M头)。
顶部形态由两个相近高点和中间颈线构成:两个高点高度接近(误差在容忍范围内),
颈线低点跌幅达到一定比例,当前价格已从第二高点回落,确认顶部。
结果会缓存到 cached_signals 表,相同参数直接读缓存。
Args:
code: 股票代码
date: 判断日期,格式 YYYY-MM-DD
period: 观察窗口(K线根数),默认60
use_adjusted: 是否使用后复权价格,默认True
Returns:
1 表示出现形态,0 表示未出现,None 表示数据不足
Example:
signal = api.get_top_pattern('600519.SH', '2026-03-01', 60)
"""
self.track_logger.write(f"get_top_pattern(code={code!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r})")
return get_top_pattern(code, date, period, use_adjusted)
# ============================================================
# 复合筛选与分析类接口
# ============================================================
def get_stocks_by_industry_keyword(
self,
keyword: str,
market: Optional[str] = None,
limit: Optional[int] = None,
) -> List[StockBasic]:
"""
按行业关键词模糊搜索股票(industry LIKE %keyword%)。
支持不精确的行业名称,如"半导体"可匹配"半导体及元件"、"集成电路"等。
可叠加 market 过滤(主板/创业板/科创板)。
Args:
keyword: 行业关键词,如"半导体"、"芯片"、"银行"、"新能源"
market: 市场类型过滤,如"主板"、"创业板"、"科创板",None 表示不限
limit: 最大返回数量,None 表示不限
Returns:
匹配的 StockBasic 列表
Example:
chip = api.get_stocks_by_industry_keyword('半导体')
kechuang = api.get_stocks_by_industry_keyword('半导体', market='科创板')
"""
self.track_logger.write(f"get_stocks_by_industry_keyword(keyword={keyword!r}, market={market!r}, limit={limit!r})")
return query_stock_basic(industry_keyword=keyword, market=market, limit=limit)
def get_latest_income(self, code: str) -> Optional[Income]:
"""
获取股票最新一期财务数据(合并报表)。
从 income 表取 end_date 最新的一条合并报表记录,
包含 ROE、ROA、毛利率、净利率、净利润增速等常用财务指标。
Args:
code: 股票代码,如 '000001.SZ'
Returns:
最新一期 Income 对象,无数据返回 None
Example:
inc = api.get_latest_income('600519.SH')
print(f"ROE={inc.roe:.2f}% 毛利率={inc.gross_margin:.2f}%")
"""
self.track_logger.write(f"get_latest_income(code={code!r})")
records = query_income(ts_codes=[code], report_type="1",
order_by="end_date DESC", limit=1)
return records[0] if records else None
def filter_stocks_by_fundamentals(
self,
date: str,
pe_ttm_max: Optional[float] = None,
roe_min: Optional[float] = None,
mv_min_yi: Optional[float] = None,
top_n: Optional[int] = None,
order_by: str = "total_mv DESC",
) -> List[Dict]:
"""
按基本面条件多维筛选股票,支持 PE/ROE/市值三重过滤。
跨 daily_basic(PE、市值)与 income(ROE)两张表联合筛选,
全部过滤在 Python 侧完成,结果按指定字段排序后返回。
pe_ttm <= 0(亏损股)始终被排除在外。
Args:
date: 基本面数据日期,格式 YYYY-MM-DD(取当日 daily_basic 快照)
pe_ttm_max: PE(TTM) 上限(不含),None 表示不过滤
roe_min: ROE 下限(%,不含),None 表示不过滤
mv_min_yi: 总市值下限(亿元),None 表示不过滤
top_n: 最终返回数量,None 表示全部
order_by: 排序字段,可选 'total_mv DESC'/'pe_ttm ASC'/'roe DESC' 等
Returns:
字典列表,每项含 ts_code / pe_ttm / total_mv_yi / roe(亿元,roe 仅在
roe_min 不为 None 时出现),按 order_by 排序后取前 top_n 条
Example:
results = api.filter_stocks_by_fundamentals(
'2026-03-14', pe_ttm_max=20, roe_min=15, mv_min_yi=100, top_n=20
)
for r in results:
print(r['ts_code'], r['pe_ttm'], r['total_mv_yi'], r.get('roe'))
"""
self.track_logger.write(f"filter_stocks_by_fundamentals(date={date!r}, pe_ttm_max={pe_ttm_max!r}, roe_min={roe_min!r}, mv_min_yi={mv_min_yi!r}, top_n={top_n!r}, order_by={order_by!r})")
# ── Step 1: 全市场当日基本面快照 ───────────────────────────────────
basics = query_daily_basic(trade_date=date)
if not basics:
return []
# ── Step 2: PE + 市值过滤(数据来源 daily_basic)───────────────────
mv_min_wan = (mv_min_yi * 10000) if mv_min_yi is not None else None
candidates = []
for b in basics:
if b.pe_ttm <= 0: # 亏损股排除
continue
if pe_ttm_max is not None and b.pe_ttm > pe_ttm_max:
continue
if mv_min_wan is not None and b.total_mv < mv_min_wan:
continue
candidates.append(b)
# ── Step 3: ROE 过滤(数据来源 income,批量查询后 Python 聚合)─────
if roe_min is not None and candidates:
codes = [b.ts_code for b in candidates]
all_income = query_income(ts_codes=codes, report_type="1")
# 每只股票取 end_date 最新的一期
latest: Dict[str, Income] = {}
for inc in sorted(all_income, key=lambda x: x.end_date):
latest[inc.ts_code] = inc
result = []
for b in candidates:
inc = latest.get(b.ts_code)
if inc is None or inc.roe < roe_min:
continue
result.append({
'ts_code': b.ts_code,
'pe_ttm': b.pe_ttm,
'total_mv_yi': round(b.total_mv / 10000, 2),
'roe': inc.roe,
})
else:
result = [{
'ts_code': b.ts_code,
'pe_ttm': b.pe_ttm,
'total_mv_yi': round(b.total_mv / 10000, 2),
} for b in candidates]
# ── Step 4: 排序 + 截取 ────────────────────────────────────────────
field_map = {
'total_mv': 'total_mv_yi',
'total_mv_yi': 'total_mv_yi',
'pe_ttm': 'pe_ttm',
'roe': 'roe',
}
parts = order_by.strip().split()
sort_key = field_map.get(parts[0].lower(), 'total_mv_yi')
descending = len(parts) < 2 or parts[1].upper() == 'DESC'
result.sort(key=lambda x: x.get(sort_key) or 0, reverse=descending)
return result[:top_n] if top_n is not None else result
def scan_all_signals(
self,
signal_type: str,
date: str,
period: int = 0,
use_adjusted: bool = True,
codes: Optional[List[str]] = None,
) -> Dict[str, int]:
"""
全市场(或指定股票池)扫描指定裸K形态信号。
对每只股票调用对应信号函数,返回所有触发信号(值=1)的股票代码→信号值字典。
结果已缓存到 cached_signals 表,重复扫描同一日期无额外计算开销。
Args:
signal_type: 信号类型,不区分大小写,可选值:
MORNING_STAR / QIMING_STAR / EVENING_STAR / HUANGHUN_STAR /
THREE_WHITE_SOLDIERS / THREE_BLACK_CROWS / DARK_CLOUD_COVER /
ROUNDING_BOTTOM / ASCENDING_TRIANGLE / TOP_PATTERN
date: 扫描日期,格式 YYYY-MM-DD
period: 窗口周期(仅对 ROUNDING_BOTTOM/ASCENDING_TRIANGLE/TOP_PATTERN 有效),
0 表示使用各信号默认值(60/30/60)
use_adjusted: 是否使用后复权价格,默认 True
codes: 待扫描股票代码列表,None 表示全市场扫描
Returns:
Dict[str, int]:{股票代码: 1},仅包含信号值为 1 的股票;
若某股票数据不足(返回 None)则不计入结果
Example:
hits = api.scan_all_signals('EVENING_STAR', '2026-03-14')
print(f"黄昏之星: {list(hits.keys())}")
hits = api.scan_all_signals('ROUNDING_BOTTOM', '2026-03-14', period=60)
"""
self.track_logger.write(f"scan_all_signals(signal_type={signal_type!r}, date={date!r}, period={period!r}, use_adjusted={use_adjusted!r}, codes={codes!r})")
_DISPATCH: Dict[str, any] = {
'MORNING_STAR': lambda c: get_morning_star(c, date, use_adjusted),
'QIMING_STAR': lambda c: get_qiming_star(c, date, use_adjusted),
'EVENING_STAR': lambda c: get_evening_star(c, date, use_adjusted),
'HUANGHUN_STAR': lambda c: get_huanghun_star(c, date, use_adjusted),
'THREE_WHITE_SOLDIERS':lambda c: get_three_white_soldiers(c, date, use_adjusted),
'THREE_BLACK_CROWS': lambda c: get_three_black_crows(c, date, use_adjusted),
'DARK_CLOUD_COVER': lambda c: get_dark_cloud_cover(c, date, use_adjusted),
'ROUNDING_BOTTOM': lambda c: get_rounding_bottom(c, date, period or 60, use_adjusted),
'ASCENDING_TRIANGLE': lambda c: get_ascending_triangle(c, date, period or 30, use_adjusted),
'TOP_PATTERN': lambda c: get_top_pattern(c, date, period or 60, use_adjusted),
}
fn = _DISPATCH.get(signal_type.upper())
if fn is None:
raise ValueError(
f"未知 signal_type: {signal_type!r}。"
f"可选值: {list(_DISPATCH.keys())}"
)
scan_codes = codes if codes is not None else self.get_all_symbols()
return {code: 1 for code in scan_codes if fn(code) == 1}
def get_period_return(
self,
code: str,
start_date: str,
end_date: str,
use_adjusted: bool = True,
) -> Optional[float]:
"""
计算股票在指定区间内的期间收益率(%)。
取区间内第一个和最后一个有效交易日的收盘价,使用后复权价格消除分红/送股影响。
Args:
code: 股票代码,如 '600519.SH'
start_date: 区间起始日期,格式 YYYY-MM-DD(取当日或之后第一个交易日)
end_date: 区间结束日期,格式 YYYY-MM-DD(取当日或之前最后一个交易日)
use_adjusted: 是否使用后复权价格,默认 True
Returns:
收益率(%,保留4位小数),区间内交易日不足2天返回 None
Example:
ret = api.get_period_return('600519.SH', '2026-01-01', '2026-03-14')
print(f"贵州茅台近3个月收益率: {ret:.2f}%")
"""
self.track_logger.write(f"get_period_return(code={code!r}, start_date={start_date!r}, end_date={end_date!r}, use_adjusted={use_adjusted!r})")
klines = query_daily_kline(
codes=[code], start_date=start_date, end_date=end_date,
order_by="date ASC",
)
if len(klines) < 2:
return None
if use_adjusted:
basics = query_daily_basic(
ts_codes=[code],
start_date=klines[0].date,
end_date=klines[-1].date,
)
adj_map = {b.trade_date: b.adj_factor for b in basics}
adj_s = adj_map.get(klines[0].date, 1.0)
adj_e = adj_map.get(klines[-1].date, 1.0)
close_s = klines[0].close * adj_s
close_e = klines[-1].close * adj_e
else:
close_s = klines[0].close
close_e = klines[-1].close
if close_s == 0:
return None
return round((close_e - close_s) / close_s * 100, 4)
# ============================================================
# 性能指标类接口
# ============================================================
def get_max_drawdown(self, equity_curve: List[float]) -> tuple:
"""
计算最大回撤。
Args:
equity_curve: 权益曲线,资产列表[初始值, ..., 最终值]
Returns:
元组 (最大回撤比例, 最高点索引, 最低点索引)
Example:
dd, peak_idx, drawdown_idx = api.get_max_drawdown([1000000, 1100000, 950000])
print(f'最大回撤: {dd:.2%}')
"""
self.track_logger.write(f"get_max_drawdown(equity_curve={equity_curve!r})")
return get_max_drawdown(equity_curve)
def get_max_drawdown_pct(self, equity_curve: List[float]) -> float:
"""
获取最大回撤百分比。
Args:
equity_curve: 权益曲线
Returns:
最大回撤比例,如 0.15 表示 15%
"""
self.track_logger.write(f"get_max_drawdown_pct(equity_curve={equity_curve!r})")
return get_max_drawdown_pct(equity_curve)
def get_annualized_return(self, total_return: float, days: int) -> float:
"""
计算年化收益率。
Args:
total_return: 总收益率,如 0.15 表示 15%
days: 交易天数
Returns:
年化收益率
Example:
annualized = api.get_annualized_return(0.15, 60)
"""
self.track_logger.write(f"get_annualized_return(total_return={total_return!r}, days={days!r})")
return get_annualized_return(total_return, days)
def get_total_return(self, initial_value: float, final_value: float) -> float:
"""
计算总收益率。
Args:
initial_value: 初始资金
final_value: 最终资金
Returns:
总收益率
"""
self.track_logger.write(f"get_total_return(initial_value={initial_value!r}, final_value={final_value!r})")
return get_total_return(initial_value, final_value)
def get_sharpe_ratio(self, equity_curve: List[float], risk_free_rate: float = 0.03) -> float:
"""
计算夏普比率。
Args:
equity_curve: 权益曲线
risk_free_rate: 无风险利率(年化),默认0.03
Returns:
夏普比率
Example:
sharpe = api.get_sharpe_ratio([1000000, 1050000, 1020000])
"""
self.track_logger.write(f"get_sharpe_ratio(equity_curve={equity_curve!r}, risk_free_rate={risk_free_rate!r})")
return get_sharpe_ratio(equity_curve, risk_free_rate)
def get_win_rate(self, trades: List[Dict]) -> float:
"""
计算胜率。
Args:
trades: 交易记录列表,每条包含 {'profit': 盈亏金额}
Returns:
胜率(0-100)
Example:
trades = [{'profit': 1000}, {'profit': -500}, {'profit': 800}]
win_rate = api.get_win_rate(trades)
"""
self.track_logger.write(f"get_win_rate(trades={trades!r})")
return get_win_rate(trades)
def get_profit_loss_ratio(self, trades: List[Dict]) -> float:
"""
计算盈亏比。
Args:
trades: 交易记录列表
Returns:
盈亏比(平均盈利/平均亏损)
"""
self.track_logger.write(f"get_profit_loss_ratio(trades={trades!r})")
return get_profit_loss_ratio(trades)
def get_calmar_ratio(self, equity_curve: List[float], days: int) -> float:
"""
计算卡尔玛比率(年化收益/最大回撤)。
Args:
equity_curve: 权益曲线
days: 交易天数
Returns:
卡尔玛比率
"""
self.track_logger.write(f"get_calmar_ratio(equity_curve={equity_curve!r}, days={days!r})")
return get_calmar_ratio(equity_curve, days)
def get_volatility(self, equity_curve: List[float]) -> float:
"""
计算收益波动率(年化)。
Args:
equity_curve: 权益曲线
Returns:
年化波动率
"""
self.track_logger.write(f"get_volatility(equity_curve={equity_curve!r})")
return get_volatility(equity_curve)
def get_trade_stats(self, trades: List[Dict]) -> Dict:
"""
获取交易统计信息。
Args:
trades: 交易记录列表
Returns:
统计信息字典,包含:
- total_trades: 总交易次数
- wins: 盈利次数
- losses: 亏损次数
- win_rate: 胜率
- profit_loss_ratio: 盈亏比
- total_profit: 总盈利
- total_loss: 总亏损
- avg_profit: 平均盈利
- avg_loss: 平均亏损
"""
self.track_logger.write(f"get_trade_stats(trades={trades!r})")
return get_trade_stats(trades)
def calculate_metrics(self, equity_curve: List[float], trades: List[Dict], initial_cash: float, days: int) -> Dict:
"""
生成完整的回测报告。
Args:
equity_curve: 权益曲线
trades: 交易记录列表
initial_cash: 初始资金
days: 交易天数
Returns:
回测报告字典,包含:
- initial_cash: 初始资金
- final_value: 最终资金
- total_return: 总收益率
- total_return_pct: 总收益率(%)
- annualized_return: 年化收益率
- annualized_return_pct: 年化收益率(%)
- max_drawdown: 最大回撤
- max_drawdown_pct: 最大回撤(%)
- sharpe_ratio: 夏普比率
- calmar_ratio: 卡尔玛比率
- volatility: 波动率
- trading_days: 交易天数
- trade_stats: 交易统计
Example:
equity = [1000000, 1050000, 1020000]
trades = [{'profit': 5000}, {'profit': -3000}]
report = api.calculate_metrics(equity, trades, 1000000, 30)
print(f"收益率: {report['total_return_pct']:.2f}%")
print(f"夏普比率: {report['sharpe_ratio']:.2f}")
"""
self.track_logger.write(f"calculate_metrics(equity_curve={equity_curve!r}, trades={trades!r}, initial_cash={initial_cash!r}, days={days!r})")
return generate_report(equity_curve, trades, initial_cash, days, self.track_logger)
# ============================================================
# 回测工具类接口
# ============================================================
def simulate_trade(self, action: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Dict:
"""
模拟单笔交易,计算成本和手续费。
Args:
action: 交易方向,'BUY' 或 'SELL'
price: 成交价格
quantity: 成交数量
fee_rate: 手续费率,默认0.0003(万三)
Returns:
字典 {'cost': 成本, 'fee': 手续费, 'net_proceeds': 净收款(卖出)}
Example:
result = api.simulate_trade('BUY', 100.0, 100)
print(f"成本: {result['cost']}, 手续费: {result['fee']}")
"""
self.track_logger.write(f"simulate_trade(action={action!r}, price={price!r}, quantity={quantity!r}, fee_rate={fee_rate!r})")
return simulate_trade(action, price, quantity, fee_rate)
def calculate_trade_cost(self, action: str, price: float, quantity: int, fee_rate: float = 0.0003, slippage: float = 0.0) -> float:
"""
计算交易成本(含手续费和滑点)。
Args:
action: 交易方向
price: 价格
quantity: 数量
fee_rate: 手续费率
slippage: 滑点比例
Returns:
交易成本
"""
self.track_logger.write(f"calculate_trade_cost(action={action!r}, price={price!r}, quantity={quantity!r}, fee_rate={fee_rate!r}, slippage={slippage!r})")
return calculate_trade_cost(action, price, quantity, fee_rate, slippage)
def create_position(self, code: str, shares: int, price: float, date: str) -> Position:
"""
创建持仓对象。
Args:
code: 股票代码
shares: 股数
price: 买入价格
date: 买入日期
Returns:
Position对象
Example:
pos = api.create_position('600519.SH', 100, 1800.0, '2026-01-01')
"""
self.track_logger.write(f"create_position(code={code!r}, shares={shares!r}, price={price!r}, date={date!r})")
return create_position(code, shares, price, date)
def get_position_value(self, position: Position, current_price: float) -> float:
"""
计算持仓市值。
Args:
position: Position对象
current_price: 当前价格
Returns:
市值
"""
self.track_logger.write(f"get_position_value(position={position!r}, current_price={current_price!r})")
return get_position_value(position, current_price)
def get_position_profit(self, position: Position, current_price: float) -> tuple:
"""
计算持仓盈亏。
Args:
position: Position对象
current_price: 当前价格
Returns:
元组 (盈亏金额, 盈亏比例)
Example:
profit, pct = api.get_position_profit(position, 2000.0)
print(f"盈利: {profit}, 比例: {pct:.2%}")
"""
self.track_logger.write(f"get_position_profit(position={position!r}, current_price={current_price!r})")
return get_position_profit(position, current_price)
def calculate_portfolio_value(self, cash: float, positions: Dict[str, Position], prices: Dict[str, float]) -> float:
"""
计算组合总价值。
Args:
cash: 现金
positions: 持仓字典 {code: Position}
prices: 当前价格字典 {code: price}
Returns:
总资产
Example:
value = api.calculate_portfolio_value(500000, positions, current_prices)
"""
self.track_logger.write(f"calculate_portfolio_value(cash={cash!r}, positions={positions!r}, Position={Position!r}, prices={prices!r}, float={float!r})")
return calculate_portfolio_value(cash, positions, prices)
def get_portfolio_positions(self, positions: Dict[str, Position]) -> List[Dict]:
"""
获取组合持仓详情列表。
Args:
positions: 持仓字典
Returns:
持仓详情列表
"""
self.track_logger.write(f"get_portfolio_positions(positions={positions!r}, Position]={Position!r})")
return get_portfolio_positions(positions)
def build_equity_curve(self, daily_values: List[tuple]) -> List[float]:
"""
从每日资产构建权益曲线。
Args:
daily_values: [(日期, 资产), ...] 按日期升序
Returns:
权益曲线列表
Example:
values = [('2026-01-01', 1000000), ('2026-01-02', 1005000)]
curve = api.build_equity_curve(values)
"""
self.track_logger.write(f"build_equity_curve(daily_values={daily_values!r})")
return build_equity_curve(daily_values)
def calculate_daily_returns(self, equity_curve: List[float]) -> List[float]:
"""
计算日收益率序列。
Args:
equity_curve: 权益曲线
Returns:
日收益率列表
"""
self.track_logger.write(f"calculate_daily_returns(equity_curve={equity_curve!r})")
return calculate_daily_returns(equity_curve)
def should_buy(self, current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_oversold: float = 30) -> bool:
"""
买入信号判断(MA金叉 + RSI超卖)。
Args:
current_price: 当前价格
ma_short: 短期均线
ma_long: 长期均线
rsi: RSI值
rsi_oversold: RSI超卖阈值
Returns:
是否买入
Example:
if api.should_buy(close, ma5, ma20, rsi, 30):
print('买入信号')
"""
self.track_logger.write(f"should_buy(current_price={current_price!r}, ma_short={ma_short!r}, ma_long={ma_long!r}, rsi={rsi!r}, rsi_oversold={rsi_oversold!r})")
return should_buy(current_price, ma_short, ma_long, rsi, rsi_oversold)
def should_sell(self, current_price: float, ma_short: float, ma_long: float, rsi: float = 50, rsi_overbought: float = 70) -> bool:
"""
卖出信号判断(MA死叉或RSI超买)。
Args:
current_price: 当前价格
ma_short: 短期均线
ma_long: 长期均线
rsi: RSI值
rsi_overbought: RSI超买阈值
Returns:
是否卖出
Example:
if api.should_sell(close, ma5, ma20, rsi, 70):
print('卖出信号')
"""
self.track_logger.write(f"should_sell(current_price={current_price!r}, ma_short={ma_short!r}, ma_long={ma_long!r}, rsi={rsi!r}, rsi_overbought={rsi_overbought!r})")
return should_sell(current_price, ma_short, ma_long, rsi, rsi_overbought)
def calculate_drawdown(self, equity_curve: List[float]) -> List[float]:
"""
计算回撤序列。
Args:
equity_curve: 权益曲线
Returns:
回撤序列列表
Example:
drawdowns = api.calculate_drawdown([1000000, 1100000, 950000])
"""
self.track_logger.write(f"calculate_drawdown(equity_curve={equity_curve!r})")
return calculate_drawdown(equity_curve)
# ============================================================
# Tick级数据接口(模拟级Tick)
# ============================================================
def get_tick_data(self, code: str, date: str) -> Optional[Dict]:
"""
获取指定日期的Tick级数据(模拟级)。
Args:
code: 股票代码
date: 日期,格式 YYYY-MM-DD
Returns:
Tick数据字典,包含:
- time: 时间
- open: 开盘价
- high: 最高价
- low: 最低价
- close: 收盘价
- volume: 成交量
- amount: 成交额
若无数据返回None
Example:
tick = api.get_tick_data('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_tick_data(code={code!r}, date={date!r})")
klines = query_daily_kline(codes=[code], start_date=date, end_date=date, order_by="date ASC")
if not klines:
return None
k = klines[0]
return {
'time': k.date,
'open': k.open,
'high': k.high,
'low': k.low,
'close': k.close,
'volume': k.volume,
'amount': k.amount,
}
def get_realtime_bar(self, code: str, date: str) -> Dict:
"""
获取实时Bar数据(用于实盘级Tick)。
Args:
code: 股票代码
date: 日期
Returns:
Bar数据字典
Example:
bar = api.get_realtime_bar('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_realtime_bar(code={code!r}, date={date!r})")
return self.get_tick_data(code, date)
# ============================================================
# 订单管理接口
# ============================================================
def create_order(self, code: str, action: str, price: float, quantity: int) -> Dict:
"""
创建订单(本地模拟,非真实下单)。
Args:
code: 股票代码
action: 'BUY' 或 'SELL'
price: 价格
quantity: 数量(股)
Returns:
订单字典,包含:
- order_id: 订单ID
- code: 股票代码
- action: 方向
- price: 价格
- quantity: 数量
- status: 状态 'PENDING'
- create_time: 创建时间
Example:
order = api.create_order('600519.SH', 'BUY', 1800.0, 100)
"""
self.track_logger.write(f"create_order(code={code!r}, action={action!r}, price={price!r}, quantity={quantity!r})")
import time
return {
'order_id': f"ORDER_{int(time.time()*1000)}",
'code': code,
'action': action.upper(),
'price': price,
'quantity': quantity,
'status': 'PENDING',
'create_time': time.strftime('%Y-%m-%d %H:%M:%S'),
}
def cancel_order(self, order: Dict) -> bool:
"""
取消订单。
Args:
order: 订单字典
Returns:
是否取消成功
Example:
api.cancel_order(order)
"""
self.track_logger.write(f"cancel_order(order={order!r})")
if order.get('status') == 'PENDING':
order['status'] = 'CANCELLED'
return True
return False
def get_order_status(self, order: Dict) -> str:
"""
获取订单状态。
Args:
order: 订单字典
Returns:
状态: PENDING, FILLED, CANCELLED, REJECTED
Example:
status = api.get_order_status(order)
"""
self.track_logger.write(f"get_order_status(order={order!r})")
return order.get('status', 'UNKNOWN')
def close_position(self, position: Position, price: float, date: str) -> Dict:
"""
平仓(卖出股票结束多头持仓)。
Args:
position: Position对象
price: 平仓价格
date: 平仓日期
Returns:
平仓结果字典,包含:
- profit: 盈亏金额
- profit_pct: 盈亏比例
- hold_days: 持有天数
Example:
result = api.close_position(position, 1900.0, '2026-01-15')
print(f"盈利: {result['profit']}")
"""
self.track_logger.write(f"close_position(position={position!r}, price={price!r}, date={date!r})")
profit, profit_pct = get_position_profit(position, price)
hold_days = (date_to_num(date) - date_to_num(position.entry_date))
return {
'profit': profit,
'profit_pct': profit_pct,
'hold_days': hold_days,
}
def update_position_price(self, position: Position, current_price: float) -> None:
"""
更新持仓的当前价格(用于市价计算)。
Args:
position: Position对象
current_price: 当前价格
"""
self.track_logger.write(f"update_position_price(position={position!r}, current_price={current_price!r})")
update_position(position, current_price)
# ============================================================
# 回测引擎控制接口
# ============================================================
def init_backtest(self, initial_cash: float = 1000000.0, fee_rate: float = 0.0003) -> Dict:
"""
初始化回测环境。
Args:
initial_cash: 初始资金,默认100万
fee_rate: 手续费率,默认万三
Returns:
回测环境字典
Example:
env = api.init_backtest(1000000, 0.0003)
"""
self.track_logger.write(f"init_backtest(initial_cash={initial_cash!r}, fee_rate={fee_rate!r})")
return {
'initial_cash': initial_cash,
'fee_rate': fee_rate,
'cash': initial_cash,
'positions': {},
'orders': [],
'trades': [],
'equity_curve': [],
}
def execute_buy(self, env: Dict, code: str, price: float, quantity: int, date: str) -> Dict:
"""
执行买入操作。
Args:
env: 回测环境字典
code: 股票代码
price: 价格
quantity: 数量
date: 交易日期
Returns:
执行结果字典
"""
self.track_logger.write(f"execute_buy(env={env!r}, code={code!r}, price={price!r}, quantity={quantity!r}, date={date!r})")
fee_rate = env.get('fee_rate', 0.0003)
new_cash, new_positions, result = buy(
env['cash'], env['positions'], code, price, quantity, date, fee_rate
)
env['cash'] = new_cash
env['positions'] = new_positions
if result.success:
env['trades'].append({
'code': code,
'action': 'BUY',
'price': price,
'quantity': quantity,
'cost': result.cost,
'fee': result.fee,
})
return {
'success': result.success,
'code': code,
'action': 'BUY',
'price': price,
'quantity': quantity,
'cost': result.cost if result.success else 0,
'fee': result.fee if result.success else 0,
'reason': result.reason,
}
def execute_sell(self, env: Dict, code: str, price: float, quantity: int) -> Dict:
"""
执行卖出操作。
Args:
env: 回测环境字典
code: 股票代码
price: 价格
quantity: 数量
Returns:
执行结果字典
"""
self.track_logger.write(f"execute_sell(env={env!r}, code={code!r}, price={price!r}, quantity={quantity!r})")
fee_rate = env.get('fee_rate', 0.0003)
new_cash, new_positions, result = sell(
env['cash'], env['positions'], code, price, quantity, fee_rate
)
env['cash'] = new_cash
env['positions'] = new_positions
if result.success:
env['trades'].append({
'code': code,
'action': 'SELL',
'price': price,
'quantity': quantity,
'net_proceeds': result.net_proceeds,
'fee': result.fee,
})
return {
'success': result.success,
'code': code,
'action': 'SELL',
'price': price,
'quantity': quantity,
'net_proceeds': result.net_proceeds if result.success else 0,
'fee': result.fee if result.success else 0,
'reason': result.reason,
}
def get_equity(self, env: Dict, current_prices: Dict[str, float]) -> float:
"""
获取当前权益(现金+持仓市值)。
Args:
env: 回测环境字典
current_prices: 当前价格字典 {code: price}
Returns:
总权益
"""
self.track_logger.write(f"get_equity(env={env!r}, current_prices={current_prices!r}, float]={float!r})")
return calculate_portfolio_value(env['cash'], env['positions'], current_prices)
def record_equity(self, env: Dict, date: str, current_prices: Dict[str, float]) -> None:
"""
记录每日权益到权益曲线。
Args:
env: 回测环境字典
date: 日期
current_prices: 当前价格字典
"""
self.track_logger.write(f"record_equity(env={env!r}, date={date!r}, current_prices={current_prices!r}, float]={float!r})")
equity = self.get_equity(env, current_prices)
env['equity_curve'].append((date, equity))
# ============================================================
# 策略辅助函数
# ============================================================
def get_price_change_rate(self, code: str, date: str, days: int = 3) -> Optional[float]:
"""
计算近N日平均涨幅。
Args:
code: 股票代码
date: 日期
days: 天数,默认3
Returns:
平均涨跌幅(%),若数据不足返回None
Example:
avg_change = api.get_price_change_rate('600519.SH', '2026-03-01', 3)
"""
self.track_logger.write(f"get_price_change_rate(code={code!r}, date={date!r}, days={days!r})")
import datetime
start_dt = datetime.datetime.strptime(date, '%Y-%m-%d')
end_dt = start_dt - datetime.timedelta(days=days * 2)
start = end_dt.strftime('%Y-%m-%d')
klines = query_daily_kline(codes=[code], start_date=start, end_date=date, order_by="date ASC")
if len(klines) < days:
return None
klines.sort(key=lambda x: x.date, reverse=True)
pct_sum = sum(k.pctChg for k in klines[:days])
return pct_sum / days
def get_top_performers(self, codes: List[str], date: str, days: int = 3, top_n: int = 3) -> List[tuple]:
"""
获取近N日涨幅最高的股票。
Args:
codes: 股票代码列表
date: 日期
days: 计算天数
top_n: 返回前N只
Returns:
[(股票代码, 平均涨幅), ...] 按涨幅降序
Example:
top_stocks = api.get_top_performers(codes, '2026-03-01', 3, 3)
"""
self.track_logger.write(f"get_top_performers(codes={codes!r}, date={date!r}, days={days!r}, top_n={top_n!r})")
results = []
for code in codes:
avg_change = self.get_price_change_rate(code, date, days)
if avg_change is not None:
results.append((code, avg_change))
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_n]
def get_price_at_date(self, code: str, date: str) -> Optional[float]:
"""
获取指定日期的收盘价。
Args:
code: 股票代码
date: 日期
Returns:
收盘价,若无数据返回None
Example:
price = api.get_price_at_date('600519.SH', '2026-03-01')
"""
self.track_logger.write(f"get_price_at_date(code={code!r}, date={date!r})")
klines = query_daily_kline(codes=[code], start_date=date, end_date=date, order_by="date ASC")
return klines[0].close if klines else None
def get_prices_at_dates(self, code: str, dates: List[str]) -> List[Optional[float]]:
"""
获取多个日期的收盘价。
Args:
code: 股票代码
dates: 日期列表
Returns:
收盘价列表(按日期升序)
Example:
prices = api.get_prices_at_dates('600519.SH', ['2026-01-01', '2026-01-02'])
"""
self.track_logger.write(f"get_prices_at_dates(code={code!r}, dates={dates!r})")
if not dates:
return []
start = dates[0]
end = dates[-1]
klines = query_daily_kline(codes=[code], start_date=start, end_date=end, order_by="date ASC")
price_map = {k.date: k.close for k in klines}
return [price_map.get(d) for d in dates]
# ============================================================
# 数据库维护接口
# ============================================================
def init_databases(self) -> None:
"""
初始化所有数据库(指标库等)。
Example:
api.init_databases()
"""
self.track_logger.write("init_databases()")
init_indicators_db()
def clear_indicator_cache(self, code: str = None) -> None:
"""
清除技术指标缓存。
Args:
code: 股票代码,None表示清除所有
Example:
api.clear_indicator_cache('600519.SH') # 清除指定股票
api.clear_indicator_cache() # 清除所有
"""
self.track_logger.write(f"clear_indicator_cache(code={code!r})")
with getEngine().connect() as conn:
if code:
conn.execute(text("DELETE FROM cached_indicators WHERE code=:code"), {"code": code})
else:
conn.execute(text("DELETE FROM cached_indicators"))
conn.commit()
# ── Alpha101 因子接口 ────────────────────────────────────────────────────
def load_alpha_data(
self,
codes: List[str],
start_date: str,
end_date: str,
fill_method: str = "ffill",
) -> dict:
"""
加载 Alpha 因子计算所需的面板数据。
Args:
codes: 股票代码列表,如 ['000001.SZ', '600519.SH']
start_date: 起始日期,格式 'YYYY-MM-DD'
end_date: 截止日期,格式 'YYYY-MM-DD'
fill_method: 缺失值填充方式('ffill' / 'bfill' / None)
Returns:
字典,key 为字段名,value 为 DataFrame(行=日期,列=股票代码):
open, high, low, close, volume, amount, vwap, returns, ind
Example:
data = api.load_alpha_data(['000001.SZ', '600519.SH'], '2025-01-01', '2026-03-01')
"""
self.track_logger.write(f"load_alpha_data(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, fill_method={fill_method!r})")
loader = AlphaDataLoader()
return loader.load(codes=codes, start_date=start_date, end_date=end_date, fill_method=fill_method)
def compute_alpha(
self,
codes: List[str],
start_date: str,
end_date: str,
alpha_num: int,
fill_method: str = "ffill",
):
"""
计算单个 Alpha 因子面板。
Args:
codes: 股票代码列表
start_date: 起始日期
end_date: 截止日期
alpha_num: 因子编号(1~101)
fill_method: 缺失值填充方式
Returns:
pd.DataFrame(行=日期,列=股票代码),或 None(数据为空时)
Example:
df = api.compute_alpha(['000001.SZ', '600519.SH'], '2025-01-01', '2026-03-01', 1)
latest = df.iloc[-1].dropna().sort_values(ascending=False)
"""
self.track_logger.write(f"compute_alpha(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, alpha_num={alpha_num!r}, fill_method={fill_method!r})")
data = self.load_alpha_data(codes, start_date, end_date, fill_method)
if not data:
return None
a = Alpha101(data)
method_name = f"alpha{alpha_num:03d}"
method = getattr(a, method_name, None)
if method is None:
raise ValueError(f"Alpha101 不存在因子 {method_name}(编号需在 1~101 范围内)")
return method()
def compute_alphas(
self,
codes: List[str],
start_date: str,
end_date: str,
alphas: Optional[List[int]] = None,
fill_method: str = "ffill",
) -> Dict:
"""
批量计算多个 Alpha 因子面板。
Args:
codes: 股票代码列表
start_date: 起始日期
end_date: 截止日期
alphas: 因子编号列表(如 [1, 5, 12]),None 表示计算全部 101 个
fill_method: 缺失值填充方式
Returns:
Dict[str, pd.DataFrame],key 为 'alpha001' 等,value 为面板 DataFrame。
计算出错的因子会被跳过并打印警告,不影响其他因子。
Example:
results = api.compute_alphas(
['000001.SZ', '600519.SH'],
'2025-01-01', '2026-03-01',
alphas=[1, 5, 12, 101],
)
for name, df in results.items():
print(name, df.iloc[-1].describe())
"""
self.track_logger.write(f"compute_alphas(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, alphas={alphas!r}, fill_method={fill_method!r})")
data = self.load_alpha_data(codes, start_date, end_date, fill_method)
if not data:
return {}
a = Alpha101(data)
return a.compute_all(alphas=alphas)
def get_alpha_latest(
self,
codes: List[str],
start_date: str,
end_date: str,
alpha_num: int,
fill_method: str = "ffill",
):
"""
计算单个 Alpha 因子并返回最新一日横截面(已去 NaN,按因子值降序排列)。
Args:
codes: 股票代码列表
start_date: 起始日期(建议给出足够的历史数据,通常 1 年以上)
end_date: 截止日期(即"最新日")
alpha_num: 因子编号(1~101)
fill_method: 缺失值填充方式
Returns:
pd.Series(index=股票代码,values=因子值,降序排列),或 None(数据为空时)
Example:
latest = api.get_alpha_latest(
['000001.SZ', '600519.SH', '000858.SZ'],
'2025-01-01', '2026-03-14',
alpha_num=1,
)
print(latest.head(10)) # 因子值最高的 10 只股票
"""
self.track_logger.write(f"get_alpha_latest(codes={codes!r}, start_date={start_date!r}, end_date={end_date!r}, alpha_num={alpha_num!r}, fill_method={fill_method!r})")
df = self.compute_alpha(codes, start_date, end_date, alpha_num, fill_method)
if df is None:
return None
return df.iloc[-1].dropna().sort_values(ascending=False)
def random_alpha_backtest(
self,
codes: Optional[List[str]] = None,
max_screen_factors: int = 3,
max_signal_factors: int = 3,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
initial_cash: float = 1_000_000.0,
warmup_days: int = 90,
random_seed: Optional[int] = None,
top_n_stocks: int = 5,
max_pool_size: int = 30,
max_holdings: int = 5,
) -> Dict:
"""
因子挖矿接口(两阶段:选股 + 交易信号)。
流程:
1. 随机抽取 k_screen 个选股因子 + k_signal 个信号因子
2. 选股阶段:以 start_date 为截面日,每个选股因子随机保留 5%~20% 的股票
3. 若过滤后股票数仍超过 max_pool_size,按各选股因子综合得分再截取前 max_pool_size 只
4. 信号阶段:逐日计算信号因子横截面分位排名,综合排名 >= buy_thresh 时买入,
<= sell_thresh 时卖出(阈值在合理范围内随机生成)
5. 每日最多同时持仓 max_holdings 只,优先买入综合排名最高的股票
6. 输出 Top N 个股的每笔交易时的具体因子值与排名
Args:
codes: 股票池;None 时取全市场
max_screen_factors: 选股因子最大数量(默认 5)
max_signal_factors: 信号因子最大数量(默认 7)
start_date: 回测起始日,None 取 end_date 前 90 天
end_date: 回测截止日,None 取今日
initial_cash: 初始资金(默认 100 万)
warmup_days: 因子预热天数(默认 90)
random_seed: 随机种子,None 不固定
top_n_stocks: 输出详细交易记录的个股数量(默认 5)
max_pool_size: 最终候选池上限,超过时按综合得分截取(默认 30)
max_holdings: 最大同时持仓数,优先持有综合排名最高的股票(默认 5)
"""
self.track_logger.write(f"random_alpha_backtest(codes={codes!r}, max_screen_factors={max_screen_factors!r}, max_signal_factors={max_signal_factors!r}, start_date={start_date!r}, end_date={end_date!r}, initial_cash={initial_cash!r}, warmup_days={warmup_days!r}, random_seed={random_seed!r}, top_n_stocks={top_n_stocks!r}, max_pool_size={max_pool_size!r}, max_holdings={max_holdings!r})")
from factor_mining import run_random_alpha_backtest
return run_random_alpha_backtest(
api=self,
codes=codes,
max_screen_factors=max_screen_factors,
max_signal_factors=max_signal_factors,
start_date=start_date,
end_date=end_date,
initial_cash=initial_cash,
warmup_days=warmup_days,
random_seed=random_seed,
top_n_stocks=top_n_stocks,
max_pool_size=max_pool_size,
max_holdings=max_holdings,
)
# ============================================================
# ★ MoE 混合专家买卖决策接口(Agent 调用优先级最高)
# ============================================================
def get_trade_signal(
self,
code: str,
date: str = None,
) -> Dict:
"""
★★★ 优先使用此接口判断股票买卖时机 ★★★
基于 MoE(混合专家模型)综合评估一只股票的买卖信号,整合以下四类专家的分析:
- 技术指标专家:80+ 个技术指标(均线、动量、振荡、成交量、通道等全覆盖)
- Alpha因子专家:WorldQuant 101个Alpha量化因子,基于全市场截面排名
- 基本面专家:PE_TTM、PB、换手率、量比、市销率等估值指标
- 量价行为专家:涨跌停、连板、炸板、龙虎榜净买入、近期涨跌幅
各专家权重从 moe_weights.json 动态加载,可通过 train_moe_weights() 跑回测优化。
当某类专家数据不足时自动降权,其余专家权重等比重新归一化。
⚠️ 调用场景:
- 用户询问某只股票"能不能买"、"该不该卖"、"现在适合持有吗"时,调用此接口
- 用户询问某只股票"当前信号"、"买卖时机"、"操作建议"时,调用此接口
- 多股票比较时,可多次调用后按 final_score 排序
Args:
code (str): 股票代码,格式如 '000001.SZ'、'600519.SH'
date (str, optional): 分析日期,格式 'YYYY-MM-DD'。
默认为今天。历史回溯时可指定过去日期。
Returns:
Dict,包含以下字段:
{
"code": "000001.SZ", # 股票代码
"date": "2026-03-18", # 分析日期
"signal": "BUY", # 信号:BUY=买入 / SELL=卖出 / HOLD=持有
"final_score": 0.72, # 综合评分 0~1,越高越看多
"confidence": "高", # 置信度:高/中/低(专家间分歧程度)
"reason": "技术面看多(0.71),Alpha因子看多(0.73),量价行为看多(0.68)",
"experts": {
"technical": {"score": 0.71, "weight": 0.41, "valid_count": 90},
"alpha": {"score": 0.73, "weight": 0.41, "valid_count": 98},
"fundamental": {"score": null, "weight": 0.0, "note": "数据不足"},
"behavior": {"score": 0.68, "weight": 0.18}
}
}
signal 取值说明:
- "BUY" → final_score >= buy_thresh(默认0.65),建议买入
- "SELL" → final_score <= sell_thresh(默认0.35),建议卖出
- "HOLD" → 介于两者之间,建议持有观望
使用示例:
api = StockApi()
result = api.get_trade_signal('000001.SZ')
if result['signal'] == 'BUY':
print(f"建议买入,综合评分 {result['final_score']}")
# 历史日期分析
result = api.get_trade_signal('600519.SH', date='2026-01-15')
"""
from datetime import datetime as _dt
_date = date or _dt.today().strftime('%Y-%m-%d')
# 延迟导入,避免循环依赖
import importlib, os as _os
_moe_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'moe_signal.py')
import importlib.util
_spec = importlib.util.spec_from_file_location('moe_signal', _moe_path)
_moe = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_moe)
init_indicators_db()
return _moe.analyze(code, _date)
def train_moe_weights(
self,
start_date: str = None,
end_date: str = None,
population_size: int = 20,
generations: int = 30,
train_stock_count: int = 30,
) -> Dict:
"""
通过遗传算法在指定历史区间跑回测,训练 MoE 各指标权重,目标:最大化平均持仓收益。
训练完成后自动将最优权重写入 moe_weights.json,下次调用 get_trade_signal() 时自动生效。
建议每半年重新训练一次,以适应最新行情风格。
⚠️ 调用场景:
- 用户说"优化权重"、"重新训练"、"适配最新行情"时调用
- 默认训练区间为最近半年(约180天)
Args:
start_date (str, optional): 训练开始日期 'YYYY-MM-DD',默认今天前180天
end_date (str, optional): 训练结束日期 'YYYY-MM-DD',默认今天
population_size (int): 遗传算法种群大小,默认20(越大越精准但越慢)
generations (int): 迭代代数,默认30
train_stock_count (int): 参与训练的随机采样股票数量,默认30
Returns:
Dict: 优化后的完整权重配置(同时已写入 moe_weights.json)
{
"expert_weights": {"technical": 0.38, "alpha": 0.32, ...},
"signal_thresholds": {"buy": 0.67, "sell": 0.33},
"technical": {"sma5": 1.2, "rsi14": 0.9, ...},
...
"_trained_at": "2026-03-18 12:00:00",
"_train_period": "2025-09-18~2026-03-18"
}
使用示例:
api = StockApi()
# 用最近半年数据训练(默认)
weights = api.train_moe_weights()
# 指定区间,快速训练(小种群+少代数)
weights = api.train_moe_weights(
start_date='2025-06-01', end_date='2025-12-31',
population_size=10, generations=15, train_stock_count=20
)
"""
from datetime import datetime as _dt, timedelta as _td
import importlib.util, os as _os
_end = end_date or _dt.today().strftime('%Y-%m-%d')
_start = start_date or (_dt.today() - _td(days=180)).strftime('%Y-%m-%d')
_moe_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), 'moe_signal.py')
_spec = importlib.util.spec_from_file_location('moe_signal', _moe_path)
_moe = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_moe)
init_indicators_db()
return _moe.train_weights(
start_date=_start,
end_date=_end,
population_size=population_size,
generations=generations,
train_stock_count=train_stock_count,
)
def date_to_num(date_str: str) -> int:
"""日期字符串转数字(用于计算天数差)"""
import datetime
try:
return int(datetime.datetime.strptime(date_str, '%Y-%m-%d').strftime('%Y%m%d'))
except:
return 0
FILE:scripts/stock_crawler.py
import requests
import time
import json
import random
import re
from typing import Dict, List, Optional, Union, Any
from dataclasses import dataclass
@dataclass
class RealtimeStockQuote:
"""
实时股票报价信息
"""
ts_code: str # 股票代码(如 000001.SZ)
name: str # 股票名称
open: float # 今日开盘价
pre_close: float # 昨日收盘价
price: float # 当前最新价
high: float # 今日最高价
low: float # 今日最低价
bid: float # 买一价
ask: float # 卖一价
volume: int # 成交量(股)
amount: float # 成交额(元)
date: str # 交易日期(YYYY-MM-DD)
time: str # 最新报价时间(HH:MM:SS)
amplitude: float # 振幅(%)
turnover_rate: Optional[float] # 换手率(%),可能为空
total_cap: Optional[float] # 总市值(元),可能为空
circ_cap: Optional[float] # 流通市值(元),可能为空
pb: Optional[float] # 市净率,可能为空
pe_ttm: Optional[float] # 市盈率(TTM),可能为空
total_shares: Optional[float] # 总股本(股),可能为空
circ_shares: Optional[float] # 流通股本(股),可能为空
status: str # 请求状态(success / error)
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "RealtimeStockQuote":
return cls(**data)
class StockCrawler:
"""
StockCrawler provides interfaces to fetch real-time stock data from multiple sources:
- Sina Finance (新浪财经)
- East Money (东方财富)
- Tonghuashun (同花顺)
It handles rate limiting to avoid IP bans and standardizes the output format.
"""
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
self.last_request_time = {}
self.min_interval = 1.0 # Minimum interval between requests in seconds per source
def _wait_for_rate_limit(self, source: str):
current_time = time.time()
last_time = self.last_request_time.get(source, 0)
elapsed = current_time - last_time
if elapsed < self.min_interval:
sleep_time = self.min_interval - elapsed
time.sleep(sleep_time)
self.last_request_time[source] = time.time()
def _convert_to_sina_symbol(self, ts_code: str) -> str:
# 000001.SZ -> sz000001
# 600519.SH -> sh600519
code, market = ts_code.split('.')
return f"{market.lower()}{code}"
def _convert_to_eastmoney_secid(self, ts_code: str) -> str:
# 000001.SZ -> 0.000001
# 600519.SH -> 1.600519
code, market = ts_code.split('.')
if market == 'SH':
return f"1.{code}"
else:
return f"0.{code}"
def _convert_to_tonghuashun_code(self, ts_code: str) -> str:
# 000001.SZ -> 000001
# 600519.SH -> 600519
return ts_code.split('.')[0]
def fetch_sina(self, ts_code: str) -> Optional[RealtimeStockQuote]:
"""
Fetch real-time data from Sina Finance.
URL: http://hq.sinajs.cn/list={symbol}
"""
self._wait_for_rate_limit('sina')
symbol = self._convert_to_sina_symbol(ts_code)
url = f"http://hq.sinajs.cn/list={symbol}"
headers = self.headers.copy()
headers['Referer'] = 'https://finance.sina.com.cn/'
try:
response = requests.get(url, headers=headers, timeout=5)
response.raise_for_status()
# Format: var hq_str_sh601006="大秦铁路, 27.55, 27.25, 26.91, 27.55, 26.20, 26.91, 26.92, ...";
content = response.text
print(content)
match = re.search(r'="(.*)";', content)
if match:
data_str = match.group(1)
parts = data_str.split(',')
if len(parts) > 30:
pre_close = float(parts[2])
high = float(parts[4])
low = float(parts[5])
amplitude = 0.0
if pre_close > 0:
amplitude = (high - low) / pre_close * 100
return RealtimeStockQuote(
ts_code=ts_code,
name=parts[0],
open=float(parts[1]),
pre_close=pre_close,
price=float(parts[3]),
high=high,
low=low,
bid=float(parts[6]),
ask=float(parts[7]),
volume=int(parts[8]), # Sina returns shares
amount=float(parts[9]),
date=parts[30],
time=parts[31],
amplitude=round(amplitude, 2),
turnover_rate=None,
total_cap=None,
circ_cap=None,
pb=None,
pe_ttm=None,
total_shares=None,
circ_shares=None,
status="success"
)
return None
except Exception:
return None
def fetch_eastmoney(self, ts_code: str) -> Dict[str, Any]:
"""
Fetch real-time data from East Money.
"""
self._wait_for_rate_limit('eastmoney')
secid = self._convert_to_eastmoney_secid(ts_code)
url = "http://push2.eastmoney.com/api/qt/stock/get"
# f43:price, f44:high, f45:low, f46:open, f47:volume, f48:amount, f57:code, f58:name, f60:pre_close, f168:turnover, f170:change_pct
params = {
"secid": secid,
"fields": "f43,f44,f45,f46,f47,f48,f50,f51,f52,f57,f58,f60,f116,f117,f162,f167,f168,f169,f170,f7,f84,f85",
"invt": "2",
"fltt": "2",
"ut": "fa5fd1943c7b386f172d6893dbfba10b"
}
try:
response = requests.get(url, params=params, headers=self.headers, timeout=5)
response.raise_for_status()
data = response.json()
if data and data.get("data"):
d = data["data"]
# EastMoney volume is in 'shou' (100 shares), convert to shares
volume = d.get("f47")
if volume is not None:
volume = float(volume) * 100
# Calculate amplitude if not provided
amplitude = d.get("f7")
if amplitude is None:
try:
high = float(d.get("f44"))
low = float(d.get("f45"))
pre_close = float(d.get("f60"))
if pre_close > 0:
amplitude = (high - low) / pre_close * 100
amplitude = round(amplitude, 2)
except (TypeError, ValueError):
amplitude = None
return {
"source": "eastmoney",
"ts_code": ts_code,
"name": d.get("f58"),
"price": d.get("f43"),
"high": d.get("f44"),
"low": d.get("f45"),
"open": d.get("f46"),
"pre_close": d.get("f60"),
"volume": volume,
"amount": d.get("f48"),
"turnover_rate": d.get("f168"),
"change_pct": d.get("f170"),
"change_amount": d.get("f169"),
"pe_ttm": d.get("f162"),
"pb": d.get("f167"),
"total_cap": d.get("f116"),
"circ_cap": d.get("f117"),
"amplitude": amplitude,
"total_shares": d.get("f84"),
"circ_shares": d.get("f85"),
"status": "success"
}
return {"source": "eastmoney", "ts_code": ts_code, "status": "failed", "message": "No data"}
except Exception as e:
return {"source": "eastmoney", "ts_code": ts_code, "status": "failed", "message": str(e)}
def fetch_tonghuashun(self, ts_code: str) -> Dict[str, Any]:
"""
Fetch data from Tonghuashun (10jqka).
Using the 'last.js' endpoint which provides the latest kline data.
Note: This returns daily K-line data, which might be the latest available trading day.
"""
self._wait_for_rate_limit('tonghuashun')
code = self._convert_to_tonghuashun_code(ts_code)
url = f"http://d.10jqka.com.cn/v6/line/hs_{code}/01/last.js"
try:
response = requests.get(url, headers=self.headers, timeout=5)
response.raise_for_status()
content = response.text
# content format: quotebridge_v6_line_hs_000001_01_last({"rt":"...","data":"date,open,high,low,close,vol,amt,..."})
match = re.search(r'\((.*)\)', content)
if match:
json_str = match.group(1)
d = json.loads(json_str)
data_str = d.get("data", "")
if data_str:
lines = data_str.split(';')
if lines:
latest = lines[-1].split(',')
if len(latest) >= 7:
# Format: Date, Open, High, Low, Close, Volume, Amount, Turnover...
return {
"source": "tonghuashun",
"ts_code": ts_code,
"date": latest[0],
"open": float(latest[1]),
"high": float(latest[2]),
"low": float(latest[3]),
"price": float(latest[4]),
"volume": float(latest[5]), # Volume is likely in shares for kline data
"amount": float(latest[6]),
"turnover_rate": float(latest[7]) if len(latest) > 7 and latest[7] else None,
"pre_close": None, # Needs calculation from previous day or another source
"amplitude": None, # Needs pre_close
"total_cap": None,
"circ_cap": None,
"pb": None,
"pe_ttm": None,
"total_shares": None,
"circ_shares": None,
"status": "success"
}
return {"source": "tonghuashun", "ts_code": ts_code, "status": "failed", "message": "Parse error or no data"}
except Exception as e:
return {"source": "tonghuashun", "ts_code": ts_code, "status": "failed", "message": str(e)}
def fetch(self, ts_code: str, source: str = 'sina') -> Dict[str, Any]:
"""
Fetch stock data from specified source.
Args:
ts_code (str): Stock code (e.g., '000001.SZ')
source (str): Source name ('sina', 'eastmoney', 'tonghuashun')
Returns:
Dict: Stock data
"""
if source == 'sina':
return self.fetch_sina(ts_code)
elif source == 'eastmoney':
return self.fetch_eastmoney(ts_code)
elif source == 'tonghuashun':
return self.fetch_tonghuashun(ts_code)
else:
# Default to sina if unknown
return self.fetch_sina(ts_code)
if __name__ == "__main__":
crawler = StockCrawler()
# Test examples
print(crawler.fetch("000001.SZ", "sina"))
# print(crawler.fetch("600519.SH", "eastmoney"))
# print(crawler.fetch("000001.SZ", "tonghuashun"))
FILE:scripts/track_logger.py
import os
from time import time
from define import BASE_URL
import config
import requests
from logger import log
class TrackLogger:
def __init__(self, file_path):
self.logger_file = file_path
self.f = open(file_path, mode="a", encoding="utf-8")
def write(self, message:str):
self.f.write(f"{message}\n")
self.f.flush()
FILE:scripts/utils.py
import tempfile
import zipfile
import os
import shutil
import requests
def get_skill_work_dir():
"""获取/创建skill专属的自定义临时目录"""
# 1. 获取系统临时目录路径(原生方法,跨平台)
system_temp_dir = tempfile.gettempdir()
# 2. 创建skill专属子目录(命名如:BitSoulStockSkill)
skill_temp_dir = os.path.join(system_temp_dir, "BitSoulStockSkill")
# 目录不存在则创建
if not os.path.exists(skill_temp_dir):
os.makedirs(skill_temp_dir, exist_ok=True)
return skill_temp_dir
def get_skill_dir():
current_file_path = os.path.abspath(__file__)
dir = os.path.dirname(os.path.dirname(current_file_path))
return dir
def get_skill_assets_dir():
return os.path.join(get_skill_dir(), "assets")
def scan_files_in_dir(dir:str):
file_list = []
# scandir 返回可迭代的 DirEntry 对象,包含文件信息
with os.scandir(dir) as entries:
for entry in entries:
# is_file(follow_symlinks=False):排除符号链接,仅判断真实文件
if entry.is_file(follow_symlinks=False):
file_list.append(entry.path) # entry.path 直接返回完整路径
return file_list
def unzip_file(zip_file_path, target_dir):
"""
将zip文件解压到指定目录
Args:
zip_file_path (str): zip压缩文件的路径
target_dir (str): 解压目标目录
Returns:
bool: 解压成功返回True,失败返回False
"""
# 确保目标目录存在,如果不存在则创建
os.makedirs(target_dir, exist_ok=True)
try:
# 以只读模式打开zip文件
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
# 解压所有文件到指定目录
zip_ref.extractall(target_dir)
return True
except zipfile.BadZipFile:
return False
except FileNotFoundError:
return False
except PermissionError:
return False
except Exception as e:
return False
def compare_version(v1: str, v2: str) -> int:
"""
比较两个版本号的大小。
支持任意段数的版本号,如 1.0、1.0.1、1.19.0 等。
每段均按整数比较,不做字符串排序。
Args:
v1 (str): 版本号A
v2 (str): 版本号B
Returns:
int: 1 表示 v1 > v2
0 表示 v1 == v2
-1 表示 v1 < v2
"""
parts1 = [int(x) for x in v1.split(".")]
parts2 = [int(x) for x in v2.split(".")]
# 补齐短版本号,末尾补 0
length = max(len(parts1), len(parts2))
parts1 += [0] * (length - len(parts1))
parts2 += [0] * (length - len(parts2))
for a, b in zip(parts1, parts2):
if a > b:
return 1
if a < b:
return -1
return 0
def download_file(url: str, dest_path: str) -> bool:
"""
从指定URL下载文件到目标路径
Args:
url (str): 文件下载链接
dest_path (str): 保存文件的完整路径(含文件名)
Returns:
bool: 下载成功返回True,失败返回False
"""
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
try:
with requests.get(url, stream=True, timeout=60) as resp:
resp.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
return True
except Exception:
return False
if __name__ == "__main__":
print(get_skill_work_dir())
FILE:scripts/formulaicAlphas/alpha101.py
"""
alpha101.py — WorldQuant《101 Formulaic Alphas》完整实现
使用方法:
from formulaicAlphas.data_loader import AlphaDataLoader
from formulaicAlphas.alpha101 import Alpha101
loader = AlphaDataLoader()
data = loader.load(codes, start_date='2025-01-01', end_date='2026-03-14')
a = Alpha101(data)
alpha1 = a.alpha001() # DataFrame: 行=日期, 列=股票代码
alpha50 = a.alpha050()
# 取最新一日横截面
latest = alpha1.iloc[-1].dropna().sort_values()
设计说明:
- 每个 alpha 方法返回与输入面板同形的 DataFrame(行=日期,列=股票)
- 参数中的小数(如 3.65595)来自机器优化,实现时直接取整
- 带 IndNeutralize 的 alpha 需传入 ind(行业面板),否则跳过中性化直接使用原值
- sum / max / min 在公式中均指时间序列滚动操作(ts_sum / ts_max / ts_min)
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from typing import Optional
from .operators import (
rank, scale, ind_neutralize,
delay, delta, ts_sum, mean, stddev,
ts_max, ts_min, ts_rank, ts_argmax, ts_argmin,
correlation, covariance, decay_linear, product,
signedpower, log, sign, abs_, adv, where,
)
Panel = pd.DataFrame
class Alpha101:
"""
WorldQuant Alpha 101 因子库。
Args:
data: AlphaDataLoader.load() 返回的字段字典,需包含:
open, high, low, close, volume, vwap, returns
可选:ind(行业面板,带 IndNeutralize 的 alpha 所需)
"""
def __init__(self, data: dict[str, Panel]):
self.open = data["open"].astype(float)
self.high = data["high"].astype(float)
self.low = data["low"].astype(float)
self.close = data["close"].astype(float)
self.volume = data["volume"].astype(float)
self.vwap = data["vwap"].astype(float)
self.returns = data["returns"].astype(float)
self.ind: Optional[Panel] = data.get("ind")
self._adv_cache: dict[int, Panel] = {}
# ── 内部辅助 ─────────────────────────────────────────────────────────────
def _adv(self, n: int) -> Panel:
"""缓存各 n 日平均成交量。"""
if n not in self._adv_cache:
self._adv_cache[n] = adv(self.volume, n)
return self._adv_cache[n]
def _ind_neu(self, x: Panel) -> Panel:
"""如有行业数据则中性化,否则原样返回。"""
if self.ind is not None:
return ind_neutralize(x, self.ind)
return x
# ── Alpha 001 ── Alpha 010 ───────────────────────────────────────────────
def alpha001(self) -> Panel:
"""rank(ts_argmax(signedpower(where(returns<0,stddev(returns,20),close),2),5)) - 0.5"""
cond = self.returns < 0
base = where(cond, stddev(self.returns, 20), self.close)
return rank(ts_argmax(signedpower(base, 2), 5)) - 0.5
def alpha002(self) -> Panel:
"""(-1) * correlation(rank(delta(log(volume),2)), rank((close-open)/open), 6)"""
x = rank(delta(log(self.volume), 2))
y = rank((self.close - self.open) / self.open)
return -1 * correlation(x, y, 6)
def alpha003(self) -> Panel:
"""(-1) * correlation(rank(open), rank(volume), 10)"""
return -1 * correlation(rank(self.open), rank(self.volume), 10)
def alpha004(self) -> Panel:
"""(-1) * ts_rank(rank(low), 9)"""
return -1 * ts_rank(rank(self.low), 9)
def alpha005(self) -> Panel:
"""rank(open - ts_sum(vwap,10)/10) * (-abs(rank(close - vwap)))"""
return rank(self.open - ts_sum(self.vwap, 10) / 10) * (-abs_(rank(self.close - self.vwap)))
def alpha006(self) -> Panel:
"""(-1) * correlation(open, volume, 10)"""
return -1 * correlation(self.open, self.volume, 10)
def alpha007(self) -> Panel:
"""if adv20 < volume: (-1)*ts_rank(abs(delta(close,7)),60)*sign(delta(close,7)) else -1"""
adv20 = self._adv(20)
d7 = delta(self.close, 7)
hit = -1 * ts_rank(abs_(d7), 60) * sign(d7)
return where(adv20 < self.volume, hit, -1)
def alpha008(self) -> Panel:
"""(-1) * rank(ts_sum(open,5)*ts_sum(returns,5) - delay(ts_sum(open,5)*ts_sum(returns,5),10))"""
prod = ts_sum(self.open, 5) * ts_sum(self.returns, 5)
return -1 * rank(prod - delay(prod, 10))
def alpha009(self) -> Panel:
"""顺势/逆势:5日内单调上涨→顺势;单调下跌→顺势;震荡→逆势"""
d1 = delta(self.close, 1)
cond_up = ts_min(d1, 5) > 0
cond_down = ts_max(d1, 5) < 0
return where(cond_up, d1, where(cond_down, d1, -d1))
def alpha010(self) -> Panel:
"""rank(alpha009_logic_with_4day_window)"""
d1 = delta(self.close, 1)
cond_up = ts_min(d1, 4) > 0
cond_down = ts_max(d1, 4) < 0
return rank(where(cond_up, d1, where(cond_down, d1, -d1)))
# ── Alpha 011 ── Alpha 020 ───────────────────────────────────────────────
def alpha011(self) -> Panel:
"""(rank(ts_max(vwap-close,3)) + rank(ts_min(vwap-close,3))) * rank(delta(volume,3))"""
vc = self.vwap - self.close
return (rank(ts_max(vc, 3)) + rank(ts_min(vc, 3))) * rank(delta(self.volume, 3))
def alpha012(self) -> Panel:
"""sign(delta(volume,1)) * (-delta(close,1))"""
return sign(delta(self.volume, 1)) * (-delta(self.close, 1))
def alpha013(self) -> Panel:
"""(-1) * rank(covariance(rank(close), rank(volume), 5))"""
return -1 * rank(covariance(rank(self.close), rank(self.volume), 5))
def alpha014(self) -> Panel:
"""(-1)*rank(delta(returns,3)) * correlation(open, volume, 10)"""
return -1 * rank(delta(self.returns, 3)) * correlation(self.open, self.volume, 10)
def alpha015(self) -> Panel:
"""(-1) * ts_sum(rank(correlation(rank(high), rank(volume), 3)), 3)"""
return -1 * ts_sum(rank(correlation(rank(self.high), rank(self.volume), 3)), 3)
def alpha016(self) -> Panel:
"""(-1) * rank(covariance(rank(high), rank(volume), 5))"""
return -1 * rank(covariance(rank(self.high), rank(self.volume), 5))
def alpha017(self) -> Panel:
"""(-1)*rank(ts_rank(close,10)) * rank(delta(delta(close,1),1)) * rank(ts_rank(vol/adv20,5))"""
adv20 = self._adv(20)
return (
-1 * rank(ts_rank(self.close, 10))
* rank(delta(delta(self.close, 1), 1))
* rank(ts_rank(self.volume / adv20, 5))
)
def alpha018(self) -> Panel:
"""(-1)*rank(stddev(abs(close-open),5) + (close-open) + correlation(close,open,10))"""
return -1 * rank(
stddev(abs_(self.close - self.open), 5)
+ (self.close - self.open)
+ correlation(self.close, self.open, 10)
)
def alpha019(self) -> Panel:
"""(-1)*sign(2*delta(close,7)) * (1 + rank(1 - ts_sum(returns,250)))"""
# (close - delay(close,7)) + delta(close,7) = 2*delta(close,7)
return -1 * sign(delta(self.close, 7)) * (1 + rank(1 - ts_sum(self.returns, 250)))
def alpha020(self) -> Panel:
"""(-1)*rank(open-delay(high,1)) * rank(open-delay(close,1)) * rank(open-delay(low,1))"""
return (
-1
* rank(self.open - delay(self.high, 1))
* rank(self.open - delay(self.close, 1))
* rank(self.open - delay(self.low, 1))
)
# ── Alpha 021 ── Alpha 030 ───────────────────────────────────────────────
def alpha021(self) -> Panel:
"""布林带判断:价格突破上轨→看空,跌破下轨→看多,中间→排名。"""
ma8 = ts_sum(self.close, 8) / 8
std8 = stddev(self.close, 8)
ma2 = ts_sum(self.close, 2) / 2
cond_up = (ma8 + std8) < ma2
cond_down = ma2 < (ma8 - std8)
middle = 1 - rank(std8 + ts_max(self.close, 8))
return where(cond_up, -1, where(cond_down, 1, middle))
def alpha022(self) -> Panel:
"""(-1) * delta(correlation(high, volume, 5), 5) * rank(stddev(close, 20))"""
return -1 * delta(correlation(self.high, self.volume, 5), 5) * rank(stddev(self.close, 20))
def alpha023(self) -> Panel:
"""if ts_mean(high,20) < high: -delta(high,2) else 0"""
cond = ts_sum(self.high, 20) / 20 < self.high
return where(cond, -delta(self.high, 2), 0)
def alpha024(self) -> Panel:
"""价格斜率<=5%: -(close - ts_min(close,100)); 否则 -delta(close,3)"""
slope = delta(ts_sum(self.close, 100) / 100, 100) / delay(self.close, 100)
cond = slope <= 0.05
return where(cond, -(self.close - ts_min(self.close, 100)), -delta(self.close, 3))
def alpha025(self) -> Panel:
"""复杂量价位置因子,结合 adv40、高低价相关性和7日价格变化。"""
adv40 = self._adv(40)
price = (2 * self.close + self.low + self.high) / 4
part1 = rank(price * (adv40 / self.volume + 1))
part2 = (
(rank(correlation(self.high, self.close, 5)) + rank(correlation(self.low, self.close, 5))) / 2
+ rank(delta(self.close, 7))
)
return part1 * part2
def alpha026(self) -> Panel:
"""(-1) * (rank(ts_rank(exp(close),1)) - rank(rank(correlation(high,volume,5))))"""
return -1 * (rank(ts_rank(np.exp(self.close), 1)) - rank(rank(correlation(self.high, self.volume, 5))))
def alpha027(self) -> Panel:
"""if rank(mean(correlation(rank(volume),rank(vwap),6),2)) > 0.5: -1 else 1"""
cond = rank(mean(correlation(rank(self.volume), rank(self.vwap), 6), 2)) > 0.5
return where(cond, -1, 1)
def alpha028(self) -> Panel:
"""scale(correlation(adv20, low, 5) + (high+low)/2 - close)"""
adv20 = self._adv(20)
return scale(correlation(adv20, self.low, 5) + (self.high + self.low) / 2 - self.close)
def alpha029(self) -> Panel:
"""ts_min(rank(rank(scale(log(ts_sum(rank(rank(-rank(delta(close,5)))),2))))),5)
+ ts_rank(delay(-returns,6),5)"""
inner = -rank(delta(self.close, 5))
part1 = ts_min(rank(rank(scale(log(ts_sum(rank(rank(inner)), 2))))), 5)
part2 = ts_rank(delay(-self.returns, 6), 5)
return part1 + part2
def alpha030(self) -> Panel:
"""(1 - rank(sign_sum_3days)) * ts_sum(vol,5) / ts_sum(vol,20)"""
s = (
sign(self.close - delay(self.close, 1))
+ sign(delay(self.close, 1) - delay(self.close, 2))
+ sign(delay(self.close, 2) - delay(self.close, 3))
)
return (1 - rank(s)) * ts_sum(self.volume, 5) / ts_sum(self.volume, 20)
# ── Alpha 031 ── Alpha 040 ───────────────────────────────────────────────
def alpha031(self) -> Panel:
"""(-1)*(rank(correlation(close,adv10,5)) - rank(delta(delta(close,1),1)))
* rank(close - ts_min(close,10) + 0.001)"""
adv10 = self._adv(10)
return (
-1
* (rank(correlation(self.close, adv10, 5)) - rank(delta(delta(self.close, 1), 1)))
* rank(self.close - ts_min(self.close, 10) + 0.001)
)
def alpha032(self) -> Panel:
"""scale(rank((ts_sum(close,7)/7 - close))) + 2*rank(correlation(vwap,delay(close,5),230))"""
return (
scale(rank(ts_sum(self.close, 7) / 7 - self.close))
+ 2 * rank(correlation(self.vwap, delay(self.close, 5), 230))
)
def alpha033(self) -> Panel:
"""rank(-(1 - open/close)^2)"""
return rank(-signedpower(1 - self.open / self.close, 2))
def alpha034(self) -> Panel:
"""(-1)*((rank(open - ts_max(close,2)) + rank(open - ts_min(close,2)))
* rank((open - delay(close,1)) + (delay(close,1) - close)))"""
return -1 * (
(rank(self.open - ts_max(self.close, 2)) + rank(self.open - ts_min(self.close, 2)))
* rank((self.open - delay(self.close, 1)) + (delay(self.close, 1) - self.close))
)
def alpha035(self) -> Panel:
"""(-1)*(rank(volume)*(1-rank(close-low)) + rank(open-low)*(1-rank(returns)))"""
return -1 * (
rank(self.volume) * (1 - rank(self.close - self.low))
+ rank(self.open - self.low) * (1 - rank(self.returns))
)
def alpha036(self) -> Panel:
"""2.21*rank(correlation(open,volume,5)) - 0.01*rank(delta(returns,3))
+ 1.54*rank(open - low)"""
# Note: paper also includes vwap & low terms; using best-effort interpretation
adv20 = self._adv(20)
return (
2.21 * rank(correlation(self.open, self.volume, 5))
- 0.01 * rank(delta(self.returns, 3))
+ 1.54 * rank(self.open - self.low)
)
def alpha037(self) -> Panel:
"""rank(correlation(delay(open-close,1), close, 200)) + rank(open-close)"""
return rank(correlation(delay(self.open - self.close, 1), self.close, 200)) + rank(self.open - self.close)
def alpha038(self) -> Panel:
"""(-1)*rank(ts_rank(close,10)) * rank(close/open)"""
return -1 * rank(ts_rank(self.close, 10)) * rank(self.close / self.open)
def alpha039(self) -> Panel:
"""(-1)*rank(delta(close,7) * (1 - rank(decay_linear(volume/adv20,9))))
* (1 + rank(ts_sum(returns,250)))"""
adv20 = self._adv(20)
return (
-1 * rank(delta(self.close, 7) * (1 - rank(decay_linear(self.volume / adv20, 9))))
* (1 + rank(ts_sum(self.returns, 250)))
)
def alpha040(self) -> Panel:
"""(-1)*rank(stddev(high,10)) * correlation(high, volume, 10)"""
return -1 * rank(stddev(self.high, 10)) * correlation(self.high, self.volume, 10)
# ── Alpha 041 ── Alpha 050 ───────────────────────────────────────────────
def alpha041(self) -> Panel:
"""(high*low)^0.5 - vwap"""
return (self.high * self.low) ** 0.5 - self.vwap
def alpha042(self) -> Panel:
"""(vwap - close) / (vwap + close) * 0.5"""
return (self.vwap - self.close) / (self.vwap + self.close) * 0.5
def alpha043(self) -> Panel:
"""ts_rank(volume/adv20, 20) * ts_rank(-delta(close,7), 8)"""
adv20 = self._adv(20)
return ts_rank(self.volume / adv20, 20) * ts_rank(-delta(self.close, 7), 8)
def alpha044(self) -> Panel:
"""(-1) * correlation(high, rank(volume), 5)"""
return -1 * correlation(self.high, rank(self.volume), 5)
def alpha045(self) -> Panel:
"""(-1) * rank(ts_sum(delay(close,5),20)/20) * correlation(close,volume,2)
* rank(correlation(ts_sum(close,5), ts_sum(close,20), 2))"""
return (
-1
* rank(ts_sum(delay(self.close, 5), 20) / 20)
* correlation(self.close, self.volume, 2)
* rank(correlation(ts_sum(self.close, 5), ts_sum(self.close, 20), 2))
)
def alpha046(self) -> Panel:
"""比较两段价格斜率:(d20-d10)/10 vs (d10-now)/10"""
slope_far = (delay(self.close, 20) - delay(self.close, 10)) / 10
slope_near = (delay(self.close, 10) - self.close) / 10
diff = slope_far - slope_near
cond_sell = diff > 0.25
cond_buy = diff < 0
return where(cond_sell, -1, where(cond_buy, 1, -delta(self.close, 1)))
def alpha047(self) -> Panel:
"""复杂多因子:价格倒数×量/adv20 × 高价位置 / 5日均高 - rank(vwap变化)"""
adv20 = self._adv(20)
part1 = (
(1 / self.close)
* self.volume
/ adv20
* self.high
* rank(self.high - self.close)
/ (ts_sum(self.high, 5) / 5)
)
return rank(part1) - rank(delta(self.vwap, 5))
def alpha048(self) -> Panel:
"""类似alpha030但加行业中性化"""
s = (
sign(self.close - delay(self.close, 1))
+ sign(delay(self.close, 1) - delay(self.close, 2))
+ sign(delay(self.close, 2) - delay(self.close, 3))
)
return (
-1 * rank(s) * ts_sum(self.volume, 5) / ts_sum(self.volume, 20)
* self._ind_neu(self.close)
)
def alpha049(self) -> Panel:
"""价格斜率差 < -1: 1; 否则 -delta(close,1)"""
slope_far = (delay(self.close, 20) - delay(self.close, 10)) / 10
slope_near = (delay(self.close, 10) - self.close) / 10
cond = (slope_far - slope_near) < -1
return where(cond, 1, -delta(self.close, 1))
def alpha050(self) -> Panel:
"""(-1) * ts_max(rank(correlation(rank(volume), rank(vwap), 5)), 5)"""
return -1 * ts_max(rank(correlation(rank(self.volume), rank(self.vwap), 5)), 5)
# ── Alpha 051 ── Alpha 060 ───────────────────────────────────────────────
def alpha051(self) -> Panel:
"""价格斜率差 < -0.05: 1; 否则 -1"""
slope_far = (delay(self.close, 20) - delay(self.close, 10)) / 10
slope_near = (delay(self.close, 10) - self.close) / 10
cond = (slope_far - slope_near) < -0.05
return where(cond, 1, -1)
def alpha052(self) -> Panel:
"""(ts_min(low,5)变化量) * rank(长期收益加速) * ts_rank(volume,5)"""
low_chg = -ts_min(self.low, 5) + delay(ts_min(self.low, 5), 5)
ret_accel = rank((ts_sum(self.returns, 240) - ts_sum(self.returns, 20)) / 220)
return low_chg * ret_accel * ts_rank(self.volume, 5)
def alpha053(self) -> Panel:
"""(-1) * delta((close-low - (high-close)) / (close-low+1e-8), 9)"""
ratio = (self.close - self.low - (self.high - self.close)) / (self.close - self.low + 1e-8)
return -1 * delta(ratio, 9)
def alpha054(self) -> Panel:
"""(-1) * (low-close)*(open^5) / ((low-high)*(close^5) + 1e-8)"""
denom = (self.low - self.high) * (self.close ** 5) + 1e-8
return -1 * (self.low - self.close) * (self.open ** 5) / denom
def alpha055(self) -> Panel:
"""(-1) * correlation(rank((close-ts_min(low,12))/(ts_max(high,12)-ts_min(low,12)+1e-8)),
rank(volume), 6)"""
stoch = (self.close - ts_min(self.low, 12)) / (ts_max(self.high, 12) - ts_min(self.low, 12) + 1e-8)
return -1 * correlation(rank(stoch), rank(self.volume), 6)
def alpha056(self) -> Panel:
"""(-1)*(rank(ts_sum(returns,5)-ts_sum(returns,20)) + rank(close-ts_min(close,5)))
* IndNeutralize(close, ind)"""
part = rank(ts_sum(self.returns, 5) - ts_sum(self.returns, 20)) + rank(self.close - ts_min(self.close, 5))
return -1 * part * self._ind_neu(self.close)
def alpha057(self) -> Panel:
"""0 - (1 - rank((close-ts_min(close,5))/(ts_max(close,5)-ts_min(close,5)+1e-8)))"""
stoch = (self.close - ts_min(self.close, 5)) / (ts_max(self.close, 5) - ts_min(self.close, 5) + 1e-8)
return 0 - (1 - rank(stoch))
def alpha058(self) -> Panel:
"""(-1)*ts_rank(decay_linear(correlation(IndNeutralize(vwap,ind),volume,4),8),6)"""
vwap_n = self._ind_neu(self.vwap)
return -1 * ts_rank(decay_linear(correlation(vwap_n, self.volume, 4), 8), 6)
def alpha059(self) -> Panel:
"""类似alpha058,vwap经加权(0.728317)后中性化"""
vwap_w = self.vwap * 0.728317 + self.vwap * (1 - 0.728317) # == self.vwap
vwap_n = self._ind_neu(vwap_w)
return -1 * ts_rank(decay_linear(correlation(vwap_n, self.volume, 4), 16), 8)
def alpha060(self) -> Panel:
"""(-1)*rank(ts_rank(correlation(rank(high),rank(volume),9),14))
* rank(ts_rank(correlation(rank(low),rank(volume),7),8))"""
c1 = rank(ts_rank(correlation(rank(self.high), rank(self.volume), 9), 14))
c2 = rank(ts_rank(correlation(rank(self.low), rank(self.volume), 7), 8))
return -1 * c1 * c2
# ── Alpha 061 ── Alpha 070 ───────────────────────────────────────────────
def alpha061(self) -> Panel:
"""(-1)*rank(correlation(rank(vwap),rank(volume),4))
* rank(correlation(rank(low),rank(volume),5))"""
c1 = rank(correlation(rank(self.vwap), rank(self.volume), 4))
c2 = rank(correlation(rank(self.low), rank(self.volume), 5))
return -1 * c1 * c2
def alpha062(self) -> Panel:
"""if rank(corr(vwap,adv20,10)) < rank(corr(rank(low),rank(adv50),17)): -1 else 1"""
adv20 = self._adv(20)
adv50 = self._adv(50)
cond = rank(correlation(self.vwap, adv20, 10)) < rank(correlation(rank(self.low), rank(adv50), 17))
return where(cond, -1, 1)
def alpha063(self) -> Panel:
"""类似alpha062,vwap行业中性化后比较"""
adv50 = self._adv(50)
vwap_n = self._ind_neu(self.vwap * 0.724108 + self.vwap * (1 - 0.724108))
c1 = rank(correlation(vwap_n, rank(self.volume), 6))
c2 = rank(correlation(rank(self.low), rank(adv50), 20))
return where(c1 < c2, -1, 1)
def alpha064(self) -> Panel:
"""比较复合量的相关性排名"""
adv40 = self._adv(40)
adv50 = self._adv(50)
price = self.open * 0.178404 + self.low * (1 - 0.178404)
c1 = rank(correlation(ts_sum(price, 13), ts_sum(adv40, 13), 5))
c2 = rank(correlation(rank(self.low), rank(adv50), 12))
return where(c1 < c2, -1, 1)
def alpha065(self) -> Panel:
"""if rank(corr(open,vol,11)) < rank(corr(rank(low),rank(adv30),15)): -1 else 1"""
adv30 = self._adv(30)
c1 = rank(correlation(self.open, self.volume, 11))
c2 = rank(correlation(rank(self.low), rank(adv30), 15))
return where(c1 < c2, -1, 1)
def alpha066(self) -> Panel:
"""if rank(corr(open,vol,5)) < rank(corr(rank(vwap),rank(vol),6)): -1 else 1"""
c1 = rank(correlation(self.open, self.volume, 5))
c2 = rank(correlation(rank(self.vwap), rank(self.volume), 6))
return where(c1 < c2, -1, 1)
def alpha067(self) -> Panel:
"""if rank(corr(IndNeutralize(high,ind),vol,8)) < rank(corr(rank(low),rank(adv30),18)): -1 else 1"""
adv30 = self._adv(30)
high_n = self._ind_neu(self.high)
c1 = rank(correlation(high_n, self.volume, 8))
c2 = rank(correlation(rank(self.low), rank(adv30), 18))
return where(c1 < c2, -1, 1)
def alpha068(self) -> Panel:
"""比较两个ts_rank后相关性的排名"""
adv20 = self._adv(20)
c1 = rank(correlation(ts_rank(self.high, 3), ts_rank(self.volume, 3), 4))
c2 = rank(correlation(rank(self.low), rank(adv20), 14))
return where(c1 < c2, -1, 1)
def alpha069(self) -> Panel:
"""if rank(corr(rank(vwap),rank(vol),12)) < rank(corr(rank(low),adv40,19)): -1 else 1"""
adv40 = self._adv(40)
c1 = rank(correlation(rank(self.vwap), rank(self.volume), 12))
c2 = rank(correlation(rank(self.low), adv40, 19))
return where(c1 < c2, -1, 1)
def alpha070(self) -> Panel:
"""(-1) * rank(delta(vwap,1))^ts_rank(corr(IndNeutralize(close,ind),adv50,18),18)"""
adv50 = self._adv(50)
close_n = self._ind_neu(self.close)
exp = ts_rank(correlation(close_n, adv50, 18), 18)
base = rank(delta(self.vwap, 1))
return -1 * base ** exp
# ── Alpha 071 ── Alpha 080 ───────────────────────────────────────────────
def alpha071(self) -> Panel:
"""max(rank(decay_linear(corr(ts_rank(close,3),ts_rank(adv50,3),12),7)),
rank(decay_linear((h+l)/2+open-close, 15)))"""
adv50 = self._adv(50)
p1 = rank(decay_linear(correlation(ts_rank(self.close, 3), ts_rank(adv50, 3), 12), 7))
p2 = rank(decay_linear((self.high + self.low) / 2 + self.open - self.close, 15))
return p1.combine(p2, np.maximum)
def alpha072(self) -> Panel:
"""rank(decay_linear(corr(rank(high),rank(adv30),9),4))
- rank(decay_linear(corr(rank(low),rank(adv30),10),8))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(rank(self.high), rank(adv30), 9), 4))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 10), 8))
return p1 - p2
def alpha073(self) -> Panel:
"""rank(decay_linear(delta(vwap,3),2)) + rank(decay_linear((-low+open)/(high-low+1e-8),1))"""
ratio = (-self.low + self.open) / (self.high - self.low + 1e-8)
return rank(decay_linear(delta(self.vwap, 3), 2)) + rank(decay_linear(ratio, 1))
def alpha074(self) -> Panel:
"""(-1)*(rank(corr(low,adv30,17)) + rank(delay(rank(open+close),12))
- rank(decay_linear(corr(rank(vwap),rank(adv50),7),18)))"""
adv30 = self._adv(30)
adv50 = self._adv(50)
p1 = rank(correlation(self.low, adv30, 17))
p2 = rank(delay(rank(self.open + self.close), 12))
p3 = rank(decay_linear(correlation(rank(self.vwap), rank(adv50), 7), 18))
return -1 * (p1 + p2 - p3)
def alpha075(self) -> Panel:
"""(rank(corr(rank(vwap),rank(vol),4)) + rank(corr(rank(low),rank(adv50),19)))
- rank(decay_linear((high+low)/2, 12))"""
adv50 = self._adv(50)
p1 = rank(correlation(rank(self.vwap), rank(self.volume), 4))
p2 = rank(correlation(rank(self.low), rank(adv50), 19))
p3 = rank(decay_linear((self.high + self.low) / 2, 12))
return p1 + p2 - p3
def alpha076(self) -> Panel:
"""(-rank(decay_linear(delta(vwap,2),16))
- rank(decay_linear(-(rank(close+open-low)-rank(vwap)+rank(close-vwap)),17)))
* IndNeutralize(close,ind)"""
inner = -(rank(self.close + self.open - self.low) - rank(self.vwap) + rank(self.close - self.vwap))
p1 = -rank(decay_linear(delta(self.vwap, 2), 16))
p2 = -rank(decay_linear(inner, 17))
return (p1 + p2) * self._ind_neu(self.close)
def alpha077(self) -> Panel:
"""(-1)*rank(decay_linear((h+l)/2+high-vwap-high,2))
+ rank(decay_linear(corr(rank(low),rank(adv50),20),9))"""
adv50 = self._adv(50)
ratio = (self.high + self.low) / 2 + self.high - self.vwap - self.high
p1 = -rank(decay_linear(ratio, 2))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv50), 20), 9))
return p1 + p2
def alpha078(self) -> Panel:
"""(-1)*rank(decay_linear(corr(rank(vwap),rank(adv20),4),5))
- rank(decay_linear(-low,9)) + rank(decay_linear(open,16))"""
adv20 = self._adv(20)
p1 = -rank(decay_linear(correlation(rank(self.vwap), rank(adv20), 4), 5))
p2 = -rank(decay_linear(-self.low, 9))
p3 = rank(decay_linear(self.open, 16))
return p1 + p2 + p3
def alpha079(self) -> Panel:
"""rank(delta(IndNeutralize(close,ind),2)) * rank(corr(IndNeutralize(vwap,ind),adv50,15))
- rank(decay_linear(-low,4))"""
adv50 = self._adv(50)
close_n = self._ind_neu(self.close)
vwap_n = self._ind_neu(self.vwap)
p1 = rank(delta(close_n, 2)) * rank(correlation(vwap_n, adv50, 15))
p2 = rank(decay_linear(-self.low, 4))
return p1 - p2
def alpha080(self) -> Panel:
"""(-1)*(rank(sign(delta(IndNeutralize(open,ind),2))*sign(delta(IndNeutralize(close,ind),3)))
+ rank(ts_rank(volume,5)))"""
open_n = self._ind_neu(self.open)
close_n = self._ind_neu(self.close)
p1 = rank(sign(delta(open_n, 2)) * sign(delta(close_n, 3)))
p2 = rank(ts_rank(self.volume, 5))
return -1 * (p1 + p2)
# ── Alpha 081 ── Alpha 090 ───────────────────────────────────────────────
def alpha081(self) -> Panel:
"""(-1)*(rank(log(product(rank(corr(rank(high),rank(adv10),8)),2))) - 0.5)"""
adv10 = self._adv(10)
return -1 * (rank(log(product(rank(correlation(rank(self.high), rank(adv10), 8)), 2))) - 0.5)
def alpha082(self) -> Panel:
"""(-1)*(rank(log(rank(corr(rank(high),rank(adv10),8))))
+ rank(ts_rank(vol,6)) - rank(corr(open,vol,6)))"""
adv10 = self._adv(10)
p1 = rank(log(rank(correlation(rank(self.high), rank(adv10), 8))))
p2 = rank(ts_rank(self.volume, 6))
p3 = rank(correlation(self.open, self.volume, 6))
return -1 * (p1 + p2 - p3)
def alpha083(self) -> Panel:
"""rank(delay(high,5)*sign(delta(close,5))) + rank(vwap-close)
- rank(corr(open,vol,13))"""
p1 = rank(delay(self.high, 5) * sign(delta(self.close, 5)))
p2 = rank(self.vwap - self.close)
p3 = rank(correlation(self.open, self.volume, 13))
return p1 + p2 - p3
def alpha084(self) -> Panel:
"""rank(corr(vwap,adv20,6)) + rank(corr(open,vol,15))"""
adv20 = self._adv(20)
return rank(correlation(self.vwap, adv20, 6)) + rank(correlation(self.open, self.volume, 15))
def alpha085(self) -> Panel:
"""(rank(corr(close,adv15,9)) + rank(corr(rank(high),rank(vol),11)))
- rank(delta(close,6))"""
adv15 = self._adv(15)
p1 = rank(correlation(self.close, adv15, 9))
p2 = rank(correlation(rank(self.high), rank(self.volume), 11))
p3 = rank(delta(self.close, 6))
return p1 + p2 - p3
def alpha086(self) -> Panel:
"""(rank(corr(close,adv30,7)) + rank(corr(open,vol,13))) - rank(delta(close,6))"""
adv30 = self._adv(30)
p1 = rank(correlation(self.close, adv30, 7))
p2 = rank(correlation(self.open, self.volume, 13))
p3 = rank(delta(self.close, 6))
return p1 + p2 - p3
def alpha087(self) -> Panel:
"""max(rank(delta(close,3)),rank(ts_rank(vol*0.47,5)))
- min(rank(delta(adv50,2)),rank(corr(IndNeutralize(vwap,ind),vol,11)))"""
adv50 = self._adv(50)
vwap_n = self._ind_neu(self.vwap)
hi = (rank(delta(self.close, 3))).combine(rank(ts_rank(self.volume * 0.47, 5)), np.maximum)
lo = (rank(delta(adv50, 2))).combine(rank(correlation(vwap_n, self.volume, 11)), np.minimum)
return hi - lo
def alpha088(self) -> Panel:
"""min(rank(decay_linear(delta(open,2),3)), rank(corr(close,vol,20)))
+ rank(decay_linear(open-low,2))"""
p_min = (rank(decay_linear(delta(self.open, 2), 3))).combine(
rank(correlation(self.close, self.volume, 20)), np.minimum
)
return p_min + rank(decay_linear(self.open - self.low, 2))
def alpha089(self) -> Panel:
"""(-1)*(rank(ts_rank(decay_linear(corr(IndNeutralize(vwap,ind),vol,4),8),7))
- rank(decay_linear(ts_rank(corr(rank(low),rank(adv30),12),19),16))"""
adv30 = self._adv(30)
vwap_n = self._ind_neu(self.vwap)
p1 = rank(ts_rank(decay_linear(correlation(vwap_n, self.volume, 4), 8), 7))
p2 = rank(decay_linear(ts_rank(correlation(rank(self.low), rank(adv30), 12), 19), 16))
return -1 * (p1 - p2)
def alpha090(self) -> Panel:
"""rank(decay_linear(corr(rank(vwap),rank(vol),14),5))
- rank(decay_linear(ts_rank(ts_min(low,5),19),13))
+ rank(corr(IndNeutralize(close,ind),vol,16))"""
close_n = self._ind_neu(self.close)
p1 = rank(decay_linear(correlation(rank(self.vwap), rank(self.volume), 14), 5))
p2 = rank(decay_linear(ts_rank(ts_min(self.low, 5), 19), 13))
p3 = rank(correlation(close_n, self.volume, 16))
return p1 - p2 + p3
# ── Alpha 091 ── Alpha 101 ───────────────────────────────────────────────
def alpha091(self) -> Panel:
"""(-1)*(rank(ts_rank(decay_linear(decay_linear(corr(IndNeutralize(close,ind),vol,3),7),6),4))
- rank(decay_linear(ts_rank(corr(rank(vwap),rank(adv30),19),12),20))"""
adv30 = self._adv(30)
close_n = self._ind_neu(self.close)
inner = decay_linear(decay_linear(correlation(close_n, self.volume, 3), 7), 6)
p1 = rank(ts_rank(inner, 4))
p2 = rank(decay_linear(ts_rank(correlation(rank(self.vwap), rank(adv30), 19), 12), 20))
return -1 * (p1 - p2)
def alpha092(self) -> Panel:
"""min(rank(decay_linear(cond_close_lt_lo_open, 15)),
rank(decay_linear(corr(rank(high),rank(adv30),14),16)))"""
adv30 = self._adv(30)
cond = (self.high + self.low) / 2 + self.close < self.low + self.open
p1 = rank(decay_linear(cond.astype(float), 15))
p2 = rank(decay_linear(correlation(rank(self.high), rank(adv30), 14), 16))
return p1.combine(p2, np.minimum)
def alpha093(self) -> Panel:
"""(5/6)*ts_rank(decay_linear(corr(IndNeutralize(vwap,ind),adv50,19),8),6)
+ (1/6)*rank(decay_linear(corr(rank(vwap),rank(vol),20),4))"""
adv50 = self._adv(50)
vwap_n = self._ind_neu(self.vwap)
p1 = ts_rank(decay_linear(correlation(vwap_n, adv50, 19), 8), 6)
p2 = rank(decay_linear(correlation(rank(self.vwap), rank(self.volume), 20), 4))
return (5 / 6) * p1 + (1 / 6) * p2
def alpha094(self) -> Panel:
"""(rank(decay_linear(corr(vwap,low,18),12))
- rank(decay_linear(corr(rank(low),rank(adv30),14),16)))
+ rank(decay_linear(delta(vwap,3),16))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(self.vwap, self.low, 18), 12))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 14), 16))
p3 = rank(decay_linear(delta(self.vwap, 3), 16))
return p1 - p2 + p3
def alpha095(self) -> Panel:
"""(rank(decay_linear(corr(open,vol,13),18))
- rank(decay_linear(corr(rank(low),rank(adv30),16),12)))
+ rank(decay_linear(delta(vwap,2),17))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(self.open, self.volume, 13), 18))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 16), 12))
p3 = rank(decay_linear(delta(self.vwap, 2), 17))
return p1 - p2 + p3
def alpha096(self) -> Panel:
"""(rank(decay_linear(corr(vwap,vol,17),18))
- rank(decay_linear(corr(rank(low),rank(adv30),18),20)))
+ rank(decay_linear(delta(vwap,3),20))"""
adv30 = self._adv(30)
p1 = rank(decay_linear(correlation(self.vwap, self.volume, 17), 18))
p2 = rank(decay_linear(correlation(rank(self.low), rank(adv30), 18), 20))
p3 = rank(decay_linear(delta(self.vwap, 3), 20))
return p1 - p2 + p3
def alpha097(self) -> Panel:
"""(-1)*(rank(decay_linear(delta(IndNeutralize(close,ind),3),16))
- rank(decay_linear(corr(IndNeutralize(vwap,ind),vol,20),18)))"""
close_n = self._ind_neu(self.close)
vwap_n = self._ind_neu(self.vwap)
p1 = rank(decay_linear(delta(close_n, 3), 16))
p2 = rank(decay_linear(correlation(vwap_n, self.volume, 20), 18))
return -1 * (p1 - p2)
def alpha098(self) -> Panel:
"""rank(decay_linear(corr(vwap,ts_sum(adv5,26),5),8))
- rank(decay_linear(ts_rank(ts_argmin(corr(rank(open),rank(adv15),21),9),7),8))"""
adv5 = self._adv(5)
adv15 = self._adv(15)
p1 = rank(decay_linear(correlation(self.vwap, ts_sum(adv5, 26), 5), 8))
p2 = rank(decay_linear(ts_rank(ts_argmin(correlation(rank(self.open), rank(adv15), 21), 9), 7), 8))
return p1 - p2
def alpha099(self) -> Panel:
"""(rank(decay_linear(corr(high,vol,20),18))
- rank(decay_linear(corr(low,vol,20),20)))
+ rank(decay_linear(corr(low,vol,9),1))"""
p1 = rank(decay_linear(correlation(self.high, self.volume, 20), 18))
p2 = rank(decay_linear(correlation(self.low, self.volume, 20), 20))
p3 = rank(decay_linear(correlation(self.low, self.volume, 9), 1))
return p1 - p2 + p3
def alpha100(self) -> Panel:
"""(rank(decay_linear(delta(vwap,2),20))
+ rank(decay_linear(corr(IndNeutralize(vwap,ind),vol,20),18)))
- rank(decay_linear(delta(close,2),5))"""
vwap_n = self._ind_neu(self.vwap)
p1 = rank(decay_linear(delta(self.vwap, 2), 20))
p2 = rank(decay_linear(correlation(vwap_n, self.volume, 20), 18))
p3 = rank(decay_linear(delta(self.close, 2), 5))
return p1 + p2 - p3
def alpha101(self) -> Panel:
"""(close - open) / (high - low + 0.001)
当日K线实体长度与日内振幅的比值,衡量动量效率。"""
return (self.close - self.open) / (self.high - self.low + 0.001)
# ── 批量计算 ─────────────────────────────────────────────────────────────
def compute_all(self, alphas: list[int] | None = None) -> dict[str, Panel]:
if alphas is None:
alphas = list(range(1, 102))
results: dict[str, Panel] = {}
for n in alphas:
name = f"alpha{n:03d}"
method = getattr(self, name, None)
if method is None:
continue
try:
results[name] = method()
except Exception as exc:
results[name] = pd.DataFrame(
np.nan,
index=self.close.index,
columns=self.close.columns,
)
import warnings
warnings.warn(f"{name} computation failed: {exc}")
return results
# ── 因子说明字典(供外部调用)────────────────────────────────────────────────
ALPHA_DESCRIPTIONS: dict[str, str] = {
"alpha001": "收益率为负时用波动率替代价格,对5日最大argmax排名,捕捉下行风险中的波动偏好。值越高→预期下跌(反向因子),高值表示股票在恐慌时期仍被追捧,短期过热",
"alpha002": "成交量二阶差分排名与收益排名的负相关,捕捉量价背离。值越高→预期下跌(反向因子),高值表示量增但价格未跟上,上涨动能衰竭",
"alpha003": "开盘价横截面排名与成交量排名的负相关。值越高→预期下跌(反向因子),高值表示开盘价在市场中偏高而成交量相对偏小,高价低量超买",
"alpha004": "低价时序排名取反,衡量低价的相对历史强度。值越高→预期下跌(反向因子),高值表示当前低价相对历史偏高,下行空间较大",
"alpha005": "开盘与10日均价偏离 × 收盘与均价偏离的乘积。值越高→预期下跌(反向因子),高值表示开盘和收盘双双偏离均价,价格虚高",
"alpha006": "开盘价与成交量的10日负相关,捕捉放量高开的反转信号。值越高→预期下跌(反向因子),高值表示开盘价与量呈强负相关,放量高开后易回落",
"alpha007": "量超均量时取价差方向×波动排名,否则取反,量能突破动量。值越高→预期上涨(正向因子),高值表示放量上涨,量能支撑价格动量",
"alpha008": "5日开盘×收益积的10日变化取反,捕捉短期量价加速反转。值越高→预期下跌(反向因子),高值表示近期开盘收益积加速上升,短期超买",
"alpha009": "5日价格单调涨跌时顺势,震荡时逆势,趋势与均值回归切换。值越高→预期上涨(正向因子),高值在趋势行情中表示强势,震荡中表示超卖反弹",
"alpha010": "alpha009的4日排名版,短周期趋势识别。值越高→预期上涨(正向因子),高值表示短期趋势或超卖反弹机会",
"alpha011": "vwap与收盘差极值排名之和 × 成交量变化,量价极值动量。值越高→预期上涨(正向因子),高值表示vwap偏离大且成交量放大,多头动能强",
"alpha012": "成交量变化方向 × 价格反向变化,量增价跌反转信号。值越高→预期上涨(反转因子),高值表示量增价跌,短期超卖后反弹概率高",
"alpha013": "收盘价与成交量排名协方差取反,量价协同性逆向因子。值越高→预期下跌(反向因子),高值表示量价协同上涨强烈,短期超买",
"alpha014": "收益3日变化排名取反 × 开盘量相关,动量衰减与量价信号。值越高→预期下跌(反向因子),高值表示近期涨速快且量价背离,动量衰竭",
"alpha015": "高价量3日相关排名的3日累加取反,量价相关持续性反转。值越高→预期下跌(反向因子),高值表示高价伴随高量持续,短期过热后回调",
"alpha016": "高价与成交量排名协方差取反,高位量价协同反转。值越高→预期下跌(反向因子),高值表示高价高量协同强,超买反转信号",
"alpha017": "价格趋势排名 × 价格加速度 × 成交量比排名三重乘积。值越高→预期下跌(反向因子),三重趋势叠加后过热,均值回归",
"alpha018": "振幅标准差+实体方向+量价相关综合取反,波动与动量综合反转。值越高→预期下跌(反向因子),高值表示波动大、阳线多且量价同向,热点股超买",
"alpha019": "7日价格方向 × 长期累计收益排名取反,趋势延续性反转。值越高→预期下跌(反向因子),高值表示近期上涨且长期涨幅大,强势股均值回归",
"alpha020": "开盘与前日高收低之差三乘积取反,跳空反转信号。值越高→预期下跌(反向因子),高值表示向上跳空动能强,缺口易被回补",
"alpha021": "布林带位置判断:突破上轨=−1看空,跌破下轨=+1看多,中间取均量排名。值越高→预期上涨(条件正向因子),高值(+1)表示跌破下轨超卖,反弹概率高",
"alpha022": "高价量5日相关的5日变化 × 收盘波动率,量价相关动量衰减。值越高→预期下跌(反向因子),高值表示高价量相关性近期大幅增加,短期热度过高",
"alpha023": "当日高价高于20日均高时,取2日高价变化的反向排名。值越高→预期下跌(反向因子),高值表示在高价突破后近期高价仍在上升,超买信号",
"alpha024": "斜率平缓时距最低点距离反转,斜率陡时3日变化反转。值越高→预期下跌(反向因子),高值表示价格高于近期低点或近期涨速过快,回调信号",
"alpha025": "量比调整价格排名 × 高低价相关排名 × 7日价格变化。值越高→预期上涨(正向因子),高值表示量比支撑下价格动量强,多因子共振看涨",
"alpha026": "价格时序排名 vs 高价量双重排名之差,价格趋势与量价分歧。值越高→预期上涨(正向因子),高值表示价格趋势强于量价相关性,纯价格动量信号",
"alpha027": "量与vwap 6日相关排名超0.5则返回−1看空,量价中期相关判断。值越高→预期下跌(反向因子),高值(>−1)时量vwap相关弱,但−1时量价同步上涨后反转",
"alpha028": "均量与低价5日相关 + 中间价偏离收盘标准化,量价位置综合。值越高→预期上涨(正向因子),高值表示均量支撑下价格位置偏低,具有上涨空间",
"alpha029": "多层嵌套价格排名 + 延迟收益时序排名,复合价格动量衰减。值越高→预期下跌(反向因子),高值表示多周期价格排名叠加高位,综合超买",
"alpha030": "3日价格方向符号排名 × 短期/长期量比,量价方向与量比结合。值越高→预期上涨(正向因子),高值表示近期上涨且短期放量,量价配合看涨",
"alpha031": "(价格均量相关−价格加速度) × 价格离低点距离排名。值越高→预期上涨(正向因子),高值表示量能支撑价格且当前远离低点,多头格局",
"alpha032": "7日均价偏离标准化 + 2×vwap与延迟价格230日相关排名。值越高→预期上涨(正向因子),高值表示均价偏低且vwap长期趋势向上,价值低估信号",
"alpha033": "开收比反向幂次排名,日内阴线(跌)放大信号。值越高→预期上涨(反转因子),高值表示收盘低于开盘幅度大,超卖后反弹概率高",
"alpha034": "开盘与近期高低价差三重排名之积取反,开盘跳空反转。值越高→预期下跌(反向因子),高值表示开盘价在高低价中位置偏高,跳空高开反转",
"alpha035": "量排名×收盘离低反向 + 开收方向×收益,综合量价反转。值越高→预期下跌(反向因子),高值表示高量且价格偏高,量价双重超买",
"alpha036": "开量相关×2.21 − 收益变化×0.01 + 开低差×1.54 线性组合。值越高→预期上涨(正向因子),高值表示开盘量能配合且开盘接近低价,支撑强",
"alpha037": "前日开收差与今日收盘200日相关 + 开收差排名,隔日价格传导。值越高→预期上涨(正向因子),高值表示前日阳线动量通过量价相关传导,延续性强",
"alpha038": "价格趋势排名 × 收开比排名取反,趋势中相对强度反转。值越高→预期下跌(反向因子),高值表示价格趋势向上但收盘相对开盘偏弱,动能衰减",
"alpha039": "7日价格变化×量比因子排名 × 长期累计收益取反。值越高→预期下跌(反向因子),高值表示短期和长期均强势,长牛股均值回归风险",
"alpha040": "高价10日波动率排名 × 高价量10日相关取反,高价波动量价背离。值越高→预期下跌(反向因子),高值表示高价波动大且量价同向,波动放大后超买",
"alpha041": "高低价几何均值 − vwap,价格重心与均价偏离。值越高→预期上涨(正向因子),高值表示高低价均值高于vwap,买方在均价之上交易,多头强势",
"alpha042": "(vwap − 收盘) / (vwap + 收盘),vwap相对收盘溢价率。值越高→预期上涨(正向因子),高值表示vwap高于收盘价,机构平均成本在收盘之上,支撑反弹",
"alpha043": "成交量比时序排名 × 7日价格逆变化时序排名,量能与价格反转共振。值越高→预期上涨(反转因子),高值表示放量且价格近期回调,量增价跌反弹信号",
"alpha044": "高价与成交量排名负相关取反,高位放量反转。值越高→预期下跌(反向因子),高值表示高价伴随大量,量价同步超买后回调",
"alpha045": "延迟均价排名 × 量价短期相关 × 均价多周期相关排名取反。值越高→预期下跌(反向因子),高值表示均价多周期均强且量价同向,综合超买",
"alpha046": "远近斜率差>0.25返回−1看空,<0返回1看多,否则取价格变化。值越高→预期上涨(条件正向因子),高值(+1)表示短期斜率转正,趋势反转向上",
"alpha047": "价格倒数×超额量×高价位置/均高 − vwap变化排名,量价位置偏离。值越高→预期上涨(正向因子),高值表示低价格高量能,vwap上升,价值洼地放量",
"alpha048": "3日价格方向排名×量比(行业中性化),行业内量价方向信号。值越高→预期上涨(正向因子),高值表示在同行业中近期涨势配合放量,相对强势",
"alpha049": "价格斜率差<−1时返回1看多,否则取价格变化反转。值越高→预期上涨(条件正向因子),高值(+1)表示短期斜率骤降,极度超卖后强力反弹",
"alpha050": "量与vwap相关排名的5日最大值取反,量价相关极值反转。值越高→预期下跌(反向因子),高值取反后实为量vwap相关性弱,量价不同向时反而易跌",
"alpha051": "远近价格斜率差<−0.05返回1看多,否则返回−1看空,二值趋势判断。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示短期急跌后斜率逆转,超卖反弹",
"alpha052": "5日低价变化量 × 长期收益加速排名 × 成交量时序排名,低价突破动量。值越高→预期上涨(正向因子),高值表示低价突破且长期动量加速放量,强势突破",
"alpha053": "价格在高低间位置比的9日变化取反,位置动量反转。值越高→预期下跌(反向因子),高值表示价格在高低区间位置近期快速上升,超买后回调",
"alpha054": "低收差×开盘5次方 / 低高差×收盘5次方 取反,高次方放大价格位置信号。值越高→预期下跌(反向因子),高值表示收盘接近最高且开盘价高,高位阳线超买",
"alpha055": "12日随机指标与成交量排名的负相关,超买区放量反转。值越高→预期下跌(反向因子),高值表示超买区成交量大,高位接盘多后回调概率高",
"alpha056": "短长期收益差排名+收盘离低排名 × 行业中性化收盘,行业内价格位置。值越高→预期上涨(正向因子),高值表示行业内近期超跌且收盘接近低位,相对低估",
"alpha057": "5日随机指标排名取反,价格在近期区间相对位置。值越高→预期上涨(反转因子),高值表示收盘接近5日低点,超卖区间内反弹概率高",
"alpha058": "行业中性化vwap量相关衰减时序排名取反,行业内量价趋势。值越高→预期下跌(反向因子),高值取反表示行业内量价相关趋势弱化,热度消退",
"alpha059": "alpha058加权版,16日衰减更长周期,行业内量价中期趋势。值越高→预期下跌(反向因子),高值取反表示中期量价同向信号衰减,中线热度减退",
"alpha060": "高价量9日相关×14日时序排名 × 低价量7日相关×8日时序排名取反。值越高→预期下跌(反向因子),高值表示高低价均与量相关性强,全面超买",
"alpha061": "vwap量4日相关排名 × 低价量5日相关排名取反,量价双重相关反转。值越高→预期下跌(反向因子),高值表示均价和低价均与量同向,双重超买信号",
"alpha062": "vwap均量相关 vs 低价大均量相关的条件判断,流动性对比信号。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价流动性优于vwap流动性,低位承接强",
"alpha063": "行业中性化vwap量相关 vs 低价大均量相关,行业内流动性判断。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示行业内低位流动性好,相对强势",
"alpha064": "复合价格均量相关 vs 低价大均量相关,价量结构对比。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价区流动性结构更优,低位筹码稳定",
"alpha065": "开盘量11日相关 vs 低价均量15日相关,开盘流动性信号。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示开盘位置流动性强于低价区,开盘承接好",
"alpha066": "开盘量5日相关 vs vwap量排名6日相关,vwap流动性结构判断。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示开盘量能优于vwap区间,短期开盘动力强",
"alpha067": "行业中性化高价量8日相关 vs 低价均量18日相关,行业高低量能对比。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价区量能相对行业内更强,低位有支撑",
"alpha068": "高价量ts_rank×4日相关 vs 低价均量14日相关,趋势量价结构对比。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价成交更活跃,底部筑牢",
"alpha069": "vwap量排名12日相关 vs 低价大均量19日相关,量价趋势与流动性。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示低价区长期量能充沛,底部支撑强",
"alpha070": "vwap日变化排名的幂次(指数=量价相关时序排名)取反,非线性量价动量。值越高→预期下跌(反向因子),高值表示vwap非线性上升且量价相关强,过热后回调",
"alpha071": "收盘趋势衰减相关 vs 中间价开收差衰减的逐元素最大值。值越高→预期上涨(正向因子),高值表示收盘趋势与日内实体均强,多头持续",
"alpha072": "高价量相关衰减 − 低价量相关衰减,高低价量能不对称。值越高→预期上涨(正向因子),高值表示高价区量能强于低价区,上涨时放量下跌时缩量,多头健康",
"alpha073": "vwap 3日变化衰减排名 + 开盘离低比衰减排名,价格趋势与开盘位置。值越高→预期上涨(正向因子),高值表示vwap上升且开盘接近低价,空间大动能足",
"alpha074": "低价量相关排名 + 延迟开收排名 − vwap均量衰减相关排名取反。值越高→预期下跌(反向因子),高值表示低价量能和前期阳线共同过热,综合超买",
"alpha075": "vwap量相关 + 低价大均量相关 − 中间价衰减排名,流动性综合因子。值为+1→预期上涨,为−1→预期下跌,高值(+1)表示量价流动性强,多方主导",
"alpha076": "vwap变化衰减+价格结构衰减 × 行业中性化收盘,行业内价格趋势。值越高→预期上涨(正向因子),高值表示行业内vwap和价格结构均向上,相对强势",
"alpha077": "中间价高价差衰减排名取反 + 低价大均量相关衰减排名,价格位置偏离。值越高→预期上涨(正向因子),高值表示日内价格偏向下沿且低价量能强,逢低做多",
"alpha078": "vwap均量相关衰减取反 + 低价衰减取反 + 开盘衰减,多因子价格偏离。值越高→预期上涨(正向因子),高值表示vwap量价背离且价格低位,反转看涨",
"alpha079": "行业中性化收盘变化×vwap均量相关排名 − 低价衰减排名,行业内趋势。值越高→预期上涨(正向因子),高值表示行业内价格上涨量价配合,低价区相对强势",
"alpha080": "行业中性化开收变化方向 + 成交量时序排名取反,行业内价格动量反转。值越高→预期下跌(反向因子),高值表示行业内阳线多且成交量偏大,超买后均值回归",
"alpha081": "高价均量8日相关乘积log排名−0.5取反,量价相关乘积信号。值越高→预期下跌(反向因子),高值取反表示量价相关乘积低,放量但高价配合差,看空",
"alpha082": "高量相关log排名 + 量时序排名 − 开量相关排名取反,多因子量价。值越高→预期下跌(反向因子),高值取反表示量排名虽高但开盘量价相关强,高开后易跌",
"alpha083": "延迟高价×价格方向 + vwap收盘差 − 开量13日相关排名,综合量价。值越高→预期上涨(正向因子),高值表示前期高价延续且vwap高于收盘,价值支撑",
"alpha084": "vwap均量6日相关 + 开盘量15日相关,双重流动性正向信号。值越高→预期上涨(正向因子),高值表示vwap和开盘均与量同向,量价双重共振看涨",
"alpha085": "收盘均量相关 + 高价量排名相关 − 价格6日变化,量价与动量综合。值越高→预期上涨(正向因子),高值表示量价相关强但近期价格涨幅有限,上涨空间大",
"alpha086": "收盘均量相关 + 开盘量相关 − 价格6日变化,类alpha085版本。值越高→预期上涨(正向因子),高值表示开盘收盘量能充沛但涨价不多,后续补涨潜力",
"alpha087": "max(价格3日变化, 量47%时序) − min(均量变化, 行业vwap量相关),极值差。值越高→预期上涨(正向因子),高值表示价格或量的上行极值强,下行极值弱,多头偏强",
"alpha088": "min(开盘2日变化衰减, 收盘量相关) + 开低差衰减,价格开盘综合。值越高→预期上涨(正向因子),高值表示开盘动能量价配合且开盘接近低价,稳健看涨",
"alpha089": "行业中性化vwap量相关双重衰减时序 − 低价均量相关衰减,量价行业中性。值越高→预期上涨(正向因子),高值表示行业内vwap量价趋势强于低价量能,中高位放量",
"alpha090": "vwap量相关衰减 − 低价ts_min衰减 + 行业中性化收盘量相关。值越高→预期上涨(正向因子),高值表示vwap量价配合且非近期最低,行业内收盘也强",
"alpha091": "行业中性化收盘量相关三重衰减时序 − vwap均量相关衰减取反。值越高→预期上涨(正向因子),高值表示行业内收盘量价持续配合,长期多头结构",
"alpha092": "min(条件衰减, 高价量相关衰减),价格位置与量能最小值信号。值越高→预期上涨(正向因子),高值表示价格位置和高价量能双双强,保守估计看涨",
"alpha093": "行业中性化vwap均量相关衰减时序×5/6 + 量价相关衰减排名×1/6。值越高→预期上涨(正向因子),高值表示行业内量价综合趋势强,主力资金配合",
"alpha094": "vwap低价相关衰减 − 低价均量相关衰减 + vwap变化衰减,三因子vwap偏离。值越高→预期上涨(正向因子),高值表示vwap趋势向上且高于低价水平,多头结构",
"alpha095": "开盘量相关衰减 − 低价均量相关衰减 + vwap变化衰减,类alpha094版本。值越高→预期上涨(正向因子),高值表示开盘量能及vwap趋势强,开盘多头",
"alpha096": "vwap量相关衰减 − 低价均量相关衰减 + vwap 3日变化,vwap量价综合。值越高→预期上涨(正向因子),高值表示vwap量价结构优于低价,且vwap近期上升",
"alpha097": "行业中性化收盘变化衰减 − 行业中性化vwap量相关衰减取反。值越高→预期下跌(反向因子),高值取反表示行业内收盘涨幅强但量价相关弱,价升量缩警惕",
"alpha098": "vwap均量相关衰减 − 开盘均量相关argmin时序衰减,vwap趋势与开盘底部。值越高→预期上涨(正向因子),高值表示vwap量价强且开盘远离近期低量时刻,多头延续",
"alpha099": "高价量相关衰减 − 低价量相关衰减 + 低价量9日短期相关,高低量能不对称。值越高→预期上涨(正向因子),高值表示上涨时放量下跌时缩量,高低量能分化利多",
"alpha100": "vwap变化衰减 + 行业中性化vwap量相关衰减 − 收盘变化衰减,量价趋势综合。值越高→预期上涨(正向因子),高值表示vwap趋势和量价强而收盘涨幅有限,补涨空间",
"alpha101": "当日K线实体长度与日内振幅之比,即(收盘−开盘)/(最高−最低+0.001)。值越高→预期上涨(正向因子),高正值表示收盘远高于开盘、阳线强势,惯性动量延续看涨",
}
FILE:scripts/formulaicAlphas/data_loader.py
"""
data_loader.py — Alpha 101 面板数据加载器
将数据库中的个股日线数据转换为 Alpha 计算所需的面板格式:
- 行(index) = 交易日期(pd.Timestamp)
- 列(columns) = 股票代码
返回字段:open / high / low / close / volume / amount / vwap / returns / ind
vwap 计算:amount / (volume * 100),volume 单位为手(100股/手),amount 单位为元。
若某日某股 vwap 计算结果异常(<=0 或 NaN),回退为 (open+high+low+close)/4。
ind(行业):从 stock_basic.industry 获取,广播到面板同形。
"""
from __future__ import annotations
import os
import sys
import numpy as np
import pandas as pd
# 允许直接运行或被上级包导入
_scripts_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _scripts_dir not in sys.path:
sys.path.insert(0, _scripts_dir)
from data_fetcher import query_daily_kline, query_stock_basic
class AlphaDataLoader:
"""加载 Alpha 因子计算所需的跨截面面板数据。"""
def load(
self,
codes: list[str],
start_date: str,
end_date: str,
fill_method: str = "ffill",
) -> dict[str, pd.DataFrame]:
"""
加载指定股票池和日期范围的面板数据。
Args:
codes: 股票代码列表,如 ['000001.SZ', '600519.SH']
start_date: 起始日期,格式 'YYYY-MM-DD'
end_date: 截止日期,格式 'YYYY-MM-DD'
fill_method: NaN 填充方式('ffill' / 'bfill' / None)
Returns:
字典,key 为字段名,value 为 DataFrame(行=日期,列=股票代码):
open, high, low, close, volume, amount, vwap, returns, ind
"""
klines = query_daily_kline(
codes=codes,
start_date=start_date,
end_date=end_date,
order_by="date ASC",
)
if not klines:
return {}
records = [
{
"date": k.date,
"code": k.code,
"open": k.open,
"high": k.high,
"low": k.low,
"close": k.close,
"volume": k.volume,
"amount": k.amount,
}
for k in klines
]
df = pd.DataFrame(records)
df["date"] = pd.to_datetime(df["date"])
df = df.set_index(["date", "code"])
panels: dict[str, pd.DataFrame] = {}
for field in ("open", "high", "low", "close", "volume", "amount"):
panels[field] = df[field].unstack(level="code").astype(float)
# 填充缺失值
if fill_method:
for key in list(panels.keys()):
panels[key] = getattr(panels[key], fill_method)()
# ── vwap ──────────────────────────────────────────────────────────
vol_safe = panels["volume"].replace(0, np.nan)
vwap = panels["amount"] / (vol_safe * 100) # volume 单位:手
typical = (panels["open"] + panels["high"] + panels["low"] + panels["close"]) / 4
bad = (vwap <= 0) | vwap.isna()
panels["vwap"] = vwap.where(~bad, typical)
# ── returns ───────────────────────────────────────────────────────
panels["returns"] = panels["close"].pct_change()
# ── industry ──────────────────────────────────────────────────────
basics = query_stock_basic()
ind_map = {b.ts_code: (b.industry or "Unknown") for b in basics}
ind_panel = pd.DataFrame(
{
code: ind_map.get(code, "Unknown")
for code in panels["close"].columns
},
index=panels["close"].index,
)
panels["ind"] = ind_panel
return panels
FILE:scripts/formulaicAlphas/operators.py
"""
operators.py — Alpha 101 基础运算符
所有运算符均以 pandas DataFrame 为工作单元:
- 行(index) = 交易日期
- 列(columns) = 股票代码
横截面运算(rank / scale / ind_neutralize)在"股票"轴(axis=1)上操作;
时间序列运算(ts_* / delay / delta / correlation 等)在"时间"轴(axis=0)上操作。
运算符命名与论文保持一致;Python 内建函数冲突时加下划线后缀(如 abs_)。
"""
from __future__ import annotations
import numpy as np
import pandas as pd
from typing import Union
# 类型别名:行=日期,列=股票代码
Panel = pd.DataFrame
# ─────────────────────────────────────────────────────────────────────────────
# 横截面运算
# ─────────────────────────────────────────────────────────────────────────────
def rank(x: Panel) -> Panel:
"""横截面百分位排名:每个交易日内,股票间的相对排名(0~1)。"""
return x.rank(axis=1, pct=True, na_option='keep')
def scale(x: Panel, a: float = 1.0) -> Panel:
"""横截面标准化:使每日各股绝对值之和等于 a。"""
abs_sum = x.abs().sum(axis=1).replace(0, np.nan)
return x.div(abs_sum, axis=0).mul(a)
def ind_neutralize(x: Panel, ind: Panel) -> Panel:
"""行业中性化:减去同日同行业均值。ind 与 x 同形,值为行业代码字符串。"""
result = x.copy().astype(float)
for date in x.index:
row = x.loc[date]
ind_row = ind.loc[date] if date in ind.index else pd.Series(dtype=str)
if ind_row.empty:
continue
means = row.groupby(ind_row).transform('mean')
result.loc[date] = row.values - means.values
return result
# ─────────────────────────────────────────────────────────────────────────────
# 时间序列运算
# ─────────────────────────────────────────────────────────────────────────────
def delay(x: Panel, d: int) -> Panel:
"""d 期前的值。"""
return x.shift(int(d))
def delta(x: Panel, d: int) -> Panel:
"""x 与 d 期前的差:x - delay(x, d)。"""
return x.diff(int(d))
def ts_sum(x: Panel, d: int) -> Panel:
"""过去 d 期滚动求和(含当期)。"""
return x.rolling(int(d), min_periods=int(d)).sum()
def mean(x: Panel, d: int) -> Panel:
"""过去 d 期滚动均值。"""
return x.rolling(int(d), min_periods=int(d)).mean()
def stddev(x: Panel, d: int) -> Panel:
"""过去 d 期滚动标准差(样本标准差,ddof=1)。"""
return x.rolling(int(d), min_periods=int(d)).std(ddof=1)
def ts_max(x: Panel, d: int) -> Panel:
"""过去 d 期滚动最大值。"""
return x.rolling(int(d), min_periods=int(d)).max()
def ts_min(x: Panel, d: int) -> Panel:
"""过去 d 期滚动最小值。"""
return x.rolling(int(d), min_periods=int(d)).min()
def ts_rank(x: Panel, d: int) -> Panel:
"""时间序列百分位排名:当期值在过去 d 期中的排名(0~1)。"""
d = int(d)
def _rank_last(arr: np.ndarray) -> float:
if np.all(np.isnan(arr)):
return np.nan
s = pd.Series(arr)
return float(s.rank(pct=True).iloc[-1])
return x.rolling(d, min_periods=d).apply(_rank_last, raw=False)
def ts_argmax(x: Panel, d: int) -> Panel:
"""过去 d 期内最大值所在位置(1 = 最老,d = 最新)。"""
d = int(d)
return x.rolling(d, min_periods=d).apply(
lambda a: int(np.argmax(a)) + 1, raw=True
)
def ts_argmin(x: Panel, d: int) -> Panel:
"""过去 d 期内最小值所在位置(1 = 最老,d = 最新)。"""
d = int(d)
return x.rolling(d, min_periods=d).apply(
lambda a: int(np.argmin(a)) + 1, raw=True
)
def correlation(x: Panel, y: Panel, d: int) -> Panel:
"""各股票列的滚动 d 期皮尔逊相关系数(x 与 y 对应列)。
当某列方差为 0(常量序列)时,相关系数未定义,填充为 0(中性值)。"""
d = int(d)
# pandas rolling.corr on aligned DataFrames returns element-wise corr
result = x.rolling(d, min_periods=d).corr(y)
# 裁剪异常值,并将未定义的 NaN(零方差)填充为 0
return result.clip(-1, 1).fillna(0)
def covariance(x: Panel, y: Panel, d: int) -> Panel:
"""各股票列的滚动 d 期协方差。"""
d = int(d)
return x.rolling(d, min_periods=d).cov(y)
def decay_linear(x: Panel, d: int) -> Panel:
"""线性衰减加权移动平均:权重为 1,2,...,d(归一化后较新权重更大)。"""
d = int(max(1, round(d)))
weights = np.arange(1, d + 1, dtype=float)
weights /= weights.sum()
def _wavg(arr: np.ndarray) -> float:
mask = ~np.isnan(arr)
if not mask.any():
return np.nan
w = weights[mask]
return float(np.dot(arr[mask], w / w.sum()))
return x.rolling(d, min_periods=d).apply(_wavg, raw=True)
def product(x: Panel, d: int) -> Panel:
"""过去 d 期滚动乘积。"""
return x.rolling(int(d), min_periods=int(d)).apply(np.prod, raw=True)
# ─────────────────────────────────────────────────────────────────────────────
# 逐元素运算
# ─────────────────────────────────────────────────────────────────────────────
def signedpower(x: Panel, t: float) -> Panel:
"""保号指数:sign(x) * |x|^t。"""
return np.sign(x) * (np.abs(x) ** t)
def log(x: Panel) -> Panel:
"""自然对数(对非正值做保护性裁剪)。"""
return np.log(x.clip(lower=1e-10))
def sign(x: Panel) -> Panel:
return np.sign(x)
def abs_(x: Panel) -> Panel:
return x.abs()
def adv(volume: Panel, d: int) -> Panel:
"""过去 d 日平均成交量(Average Daily Volume)。"""
return volume.rolling(int(d), min_periods=int(d)).mean()
# ─────────────────────────────────────────────────────────────────────────────
# 工具函数
# ─────────────────────────────────────────────────────────────────────────────
def where(cond: Panel, a: Union[Panel, float], b: Union[Panel, float]) -> Panel:
"""三元条件:cond 为 True 时取 a,否则取 b(等价于论文中的 ? :)。"""
if isinstance(cond, pd.DataFrame):
result = pd.DataFrame(np.where(cond, a, b),
index=cond.index, columns=cond.columns)
else:
result = pd.DataFrame(np.where(cond, a, b))
return result
FILE:scripts/formulaicAlphas/__init__.py
"""
formulaicAlphas — WorldQuant《101 Formulaic Alphas》实现包
快速入门:
from formulaicAlphas import AlphaDataLoader, Alpha101
# 1. 加载面板数据
loader = AlphaDataLoader()
data = loader.load(
codes=['000001.SZ', '600519.SH', '000858.SZ'],
start_date='2025-01-01',
end_date='2026-03-14',
)
# 2. 计算 alpha 因子
a = Alpha101(data)
alpha1 = a.alpha001() # DataFrame:行=日期,列=股票代码
alpha50 = a.alpha050()
# 3. 取最新一日横截面排名(越大越看多)
latest = alpha1.iloc[-1].dropna().sort_values(ascending=False)
print(latest.head(10))
# 4. 批量计算指定 alpha
results = a.compute_all(alphas=[1, 5, 12, 41, 101])
for name, df in results.items():
print(name, df.iloc[-1].describe())
"""
from .data_loader import AlphaDataLoader
from .alpha101 import Alpha101, ALPHA_DESCRIPTIONS
from . import operators
__all__ = ["AlphaDataLoader", "Alpha101", "ALPHA_DESCRIPTIONS", "operators"]
FILE:references/API_FOR_LLM.md
# StockApi 接口文档
`StockApi` 是项目对外提供的唯一数据与回测接口,封装了股票基础信息查询、K线数据获取、技术指标计算、性能指标计算和回测工具函数。
```python
from stock_api import StockApi
api = StockApi()
```
---
## 目录
1. [初始化](#初始化)
2. [股票基础信息](#股票基础信息)
3. [价格行情](#价格行情)
4. [技术指标(带缓存)](#技术指标带缓存)
5. [性能指标](#性能指标)
6. [回测工具](#回测工具)
7. [回测引擎控制](#回测引擎控制)
8. [策略辅助函数](#策略辅助函数)
9. [数据库维护](#数据库维护)
10. [实时行情 (爬虫)](#实时行情-爬虫)
---
## 初始化
### 初始化 StockApi,自动初始化技术指标缓存数据库
```python
api = StockApi()
# __init__(self)
```
---
### 更新本地数据库,获取最新增量数据
```python
api.update_data()
# update_data() -> None
```
调用此接口会对比服务器上的 patch 列表,下载并导入缺失的数据。
---
## 股票基础信息
### 获取所有股票代码列表
```python
symbols = api.get_all_symbols()
# get_all_symbols() -> List[str]
```
| 返回 | 说明 |
|------|------|
| `List[str]` | 股票代码列表,格式如 `['000001.SZ', '600519.SH', ...]` |
---
### 根据股票代码获取股票基础信息
```python
info = api.get_symbol_basic_infomation('600519.SH')
# get_symbol_basic_infomation(ts_code: str) -> Optional[StockBasic]
```
| 参数 | 类型 | 说明 |
|------|------|------|
| `ts_code` | `str` | 股票代码,如 `000001.SZ` |
| 返回 | 说明 |
|------|------|
| `StockBasic` \| `None` | 股票基础信息,未查询到返回 `None` |
---
## 价格行情
### 查询每日基本面指标列表,支持按股票、日期、分页过滤
```python
# 查询某只股票全部历史基本面数据
basics = api.get_daily_basic(ts_codes=["000001.SZ"])
# 查询某天全市场基本面数据
basics = api.get_daily_basic(trade_date="2024-06-03")
# get_daily_basic(ts_codes=[], trade_date=None, start_date=None,
# end_date=None, limit=None, offset=0,
# order_by="trade_date ASC") -> List[DailyBasic]
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `ts_codes` | `List[str]` | `[]` | 按股票代码列表过滤,空表示不过滤 |
| `trade_date` | `str \| None` | `None` | 精确过滤交易日期,格式 `YYYY-MM-DD` |
| `start_date` | `str \| None` | `None` | 日期范围下限(含),格式 `YYYY-MM-DD` |
| `end_date` | `str \| None` | `None` | 日期范围上限(含),格式 `YYYY-MM-DD` |
| `limit` | `int \| None` | `None` | 返回最大记录数,`None` 表示不限 |
| `offset` | `int` | `0` | 分页偏移量 |
| `order_by` | `str` | `"trade_date ASC"` | 排序表达式 |
---
### 获取股票日线行情,按日期升序
```python
klines = api.get_daily_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# get_daily_kline(symbols: List[str], start_date: str, end_date: str) -> List[DailyKline]
```
| 参数 | 类型 | 说明 |
|------|------|------|
| `symbols` | `List[str]` | 股票代码列表,空表示获取所有股票 |
| `start_date` | `str` | 起始日期,格式 `YYYY-MM-DD` |
| `end_date` | `str` | 结束日期,格式 `YYYY-MM-DD` |
---
### 获取股票周线行情,按日期升序
```python
klines = api.get_weekly_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# get_weekly_kline(symbols: List[str], start_date: str, end_date: str) -> List[WeeklyKline]
```
---
### 获取股票月线行情,按日期升序
```python
klines = api.get_monthly_kline(['600519.SH'], '2026-01-01', '2026-03-01')
# get_monthly_kline(symbols: List[str], start_date: str, end_date: str) -> List[MonthlyKline]
```
---
### 获取指定股票的日线收盘价列表,按日期升序
```python
prices = api.get_daily_close_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_close_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线开盘价列表
```python
prices = api.get_daily_open_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_open_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线最高价列表
```python
prices = api.get_daily_high_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_high_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线最低价列表
```python
prices = api.get_daily_low_prices('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_low_prices(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线成交量列表
```python
volumes = api.get_daily_volumes('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_volumes(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定股票的日线涨跌幅列表(单位:%)
```python
pct = api.get_daily_pct_chg('600519.SH', '2026-01-01', '2026-03-01')
# get_daily_pct_chg(code: str, start_date: str, end_date: str) -> List[float]
```
---
### 获取指定日期的 Tick 级数据(模拟级),包含开高低收量额
```python
tick = api.get_tick_data('600519.SH', '2026-03-01')
# get_tick_data(code: str, date: str) -> Optional[Dict]
```
| 返回字段 | 说明 |
|----------|------|
| `time` | 时间 |
| `open` | 开盘价 |
| `high` | 最高价 |
| `low` | 最低价 |
| `close` | 收盘价 |
| `volume` | 成交量 |
| `amount` | 成交额 |
---
### 获取实时 Bar 数据,与 get_tick_data 等价,用于实盘级接口
```python
bar = api.get_realtime_bar('600519.SH', '2026-03-01')
# get_realtime_bar(code: str, date: str) -> Dict
```
---
## 技术指标(带缓存)
### 获取简单移动平均 SMA
```python
sma = api.get_sma('600519.SH', '2026-03-01', 20)
# get_sma(code: str, date: str, period: int = 20) -> Optional[float]
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `period` | `20` | 计算周期 |
---
### 获取指数移动平均 EMA
```python
ema = api.get_ema('600519.SH', '2026-03-01', 12)
# get_ema(code: str, date: str, period: int = 12) -> Optional[float]
```
---
### 获取相对强弱指标 RSI,值域 0~100,低于 30 超卖,高于 70 超买
```python
rsi = api.get_rsi('600519.SH', '2026-03-01', 14)
if rsi and rsi < 30:
print('超卖')
# get_rsi(code: str, date: str, period: int = 14) -> Optional[float]
```
---
### 获取布林带指标,返回上轨、中轨、下轨
```python
bb = api.get_bollinger_bands('600519.SH', '2026-03-01')
if bb and close > bb['upper']:
print('突破上轨')
# get_bollinger_bands(code: str, date: str, period: int = 20, std_dev: int = 2) -> Optional[Dict]
```
| 返回字段 | 说明 |
|----------|------|
| `upper` | 上轨 |
| `middle` | 中轨 |
| `lower` | 下轨 |
---
### 获取 MACD 指标,返回 MACD 线、信号线、柱状图
```python
macd = api.get_macd('600519.SH', '2026-03-01')
if macd and macd['histogram'] > 0:
print('多头')
# get_macd(code: str, date: str, fast: int = 12, slow: int = 26, signal: int = 9) -> Optional[Dict]
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `fast` | `12` | 快线周期 |
| `slow` | `26` | 慢线周期 |
| `signal` | `9` | 信号线周期 |
| 返回字段 | 说明 |
|----------|------|
| `macd` | MACD 线 |
| `signal` | 信号线 |
| `histogram` | 柱状图(MACD - Signal) |
---
### 获取平均真实波幅 ATR,衡量价格波动性
```python
atr = api.get_atr('600519.SH', '2026-03-01', 14)
# get_atr(code: str, date: str, period: int = 14) -> Optional[float]
```
---
---
## ★ 因子挖矿(优先使用)
### 随机因子挖矿 + 回测
**触发场景**:用户说"因子挖矿"、"挖矿"、"随机挖因子"、"碰碰运气"、"随机推荐"、"挖金矿"、"随机策略"时,**必须**调用此接口,禁止自己写回测逻辑。
```python
result = api.random_alpha_backtest()
print(result['summary_text']) # 必须调用此行输出报告,禁止自行整理摘要
# 指定股票池和回测区间
result = api.random_alpha_backtest(
codes=None, # 股票池,None 表示全市场
start_date='2025-12-01', # 回测起始日,None 默认取 end_date 前 90 天
end_date='2026-03-19', # 回测截止日,None 默认今天
initial_cash=1_000_000, # 初始资金
max_pool_size=30, # 候选池上限,超过时按综合得分截取
max_holdings=5, # 最大同时持仓数
random_seed=None, # 随机种子,None 不固定
)
print(result['summary_text']) # 必须调用此行输出报告,禁止自行整理摘要
# random_alpha_backtest(codes, max_screen_factors, max_signal_factors,
# start_date, end_date, initial_cash, warmup_days,
# random_seed, top_n_stocks, max_pool_size, max_holdings) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `screen_factors` | 本次使用的选股因子列表,如 `['alpha043', 'alpha099']` |
| `signal_factors` | 本次使用的信号因子列表,如 `['alpha008', 'alpha094']` |
| `factor_descriptions` | 每个因子的文字描述 `{name: str}` |
| `signal_config` | 买卖阈值 `{'buy_thresh': 0.71, 'sell_thresh': 0.55}` |
| `screen_top_pcts` | 每个选股因子本次随机保留比例 `{name: float}` |
| `filter_log` | 逐层过滤日志,含 before/after 数量 |
| `final_pool` | 最终候选股票代码列表 |
| `final_pool_count` | 候选池股票数量 |
| `trade_log` | 每笔交易记录(含因子值、排名、阈值) |
| `backtest` | 回测绩效 `{total_return_pct, annualized_return_pct, max_drawdown_pct, sharpe_ratio, equity_curve, ...}` |
| `benchmarks` | 四条基准线对比(上证/沪深300/中证500/创业板指) |
| `ic_stats` | 每个因子的 Rank IC 统计 `{ic_mean, ic_ir, ic_win_rate, ...}` |
| `top_stocks` | Top N 盈利个股详情(含每笔交易的因子值) |
| `summary_text` | 完整格式化报告文本,**直接 `print(result['summary_text'])` 输出给用户,禁止自行整理摘要** |
---
## ★ MoE 买卖时机分析(优先使用)
### 分析单只股票当前买卖信号
**触发场景**:用户询问某只股票"能不能买"、"该不该卖"、"现在适合持有吗"、"当前信号"、"操作建议"时,**必须**调用此接口。
```python
result = api.get_trade_signal('000001.SZ')
result = api.get_trade_signal('600519.SH', date='2026-01-15')
# get_trade_signal(code: str, date: str = None) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `signal` | `"BUY"` 买入 / `"SELL"` 卖出 / `"HOLD"` 持有 |
| `final_score` | 综合评分 0~1,越高越看多 |
| `confidence` | 置信度:`"高"` / `"中"` / `"低"` |
| `reason` | 各专家评分描述,如 `"技术面看多(0.71),Alpha因子看多(0.73)"` |
| `experts` | 四个专家详情:`technical` / `alpha` / `fundamental` / `behavior` |
| `code` | 股票代码 |
| `date` | 分析日期 |
---
## 性能指标
### 计算最大回撤,返回回撤比例及对应的峰值、谷值索引
```python
dd, peak_idx, drawdown_idx = api.get_max_drawdown([1000000, 1100000, 950000])
print(f'最大回撤: {dd:.2%}')
# get_max_drawdown(equity_curve: List[float]) -> tuple
```
---
### 获取最大回撤百分比,如 0.15 表示 15%
```python
pct = api.get_max_drawdown_pct([1000000, 1100000, 950000])
# get_max_drawdown_pct(equity_curve: List[float]) -> float
```
---
### 计算年化收益率
```python
annualized = api.get_annualized_return(0.15, 60)
# get_annualized_return(total_return: float, days: int) -> float
```
| 参数 | 说明 |
|------|------|
| `total_return` | 总收益率,如 `0.15` 表示 15% |
| `days` | 交易天数 |
---
### 计算总收益率
```python
ret = api.get_total_return(1000000, 1150000)
# get_total_return(initial_value: float, final_value: float) -> float
```
---
### 计算夏普比率,衡量单位风险的超额收益
```python
sharpe = api.get_sharpe_ratio([1000000, 1050000, 1020000])
# get_sharpe_ratio(equity_curve: List[float], risk_free_rate: float = 0.03) -> float
```
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `risk_free_rate` | `0.03` | 无风险利率(年化) |
---
### 计算胜率(0~100),盈利交易次数占比
```python
trades = [{'profit': 1000}, {'profit': -500}, {'profit': 800}]
win_rate = api.get_win_rate(trades)
# get_win_rate(trades: List[Dict]) -> float
```
---
### 计算盈亏比,平均盈利 / 平均亏损
```python
ratio = api.get_profit_loss_ratio(trades)
# get_profit_loss_ratio(trades: List[Dict]) -> float
```
---
### 计算卡尔玛比率,年化收益 / 最大回撤
```python
calmar = api.get_calmar_ratio(equity_curve, 252)
# get_calmar_ratio(equity_curve: List[float], days: int) -> float
```
---
### 计算年化波动率,衡量收益稳定性
```python
vol = api.get_volatility(equity_curve)
# get_volatility(equity_curve: List[float]) -> float
```
---
### 获取完整交易统计信息
```python
stats = api.get_trade_stats(trades)
# get_trade_stats(trades: List[Dict]) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `total_trades` | 总交易次数 |
| `wins` | 盈利次数 |
| `losses` | 亏损次数 |
| `win_rate` | 胜率 |
| `profit_loss_ratio` | 盈亏比 |
| `total_profit` | 总盈利 |
| `total_loss` | 总亏损 |
| `avg_profit` | 平均盈利 |
| `avg_loss` | 平均亏损 |
---
### 生成完整回测报告,汇总所有关键绩效指标
```python
equity = [1000000, 1050000, 1020000]
trades = [{'profit': 5000}, {'profit': -3000}]
report = api.calculate_metrics(equity, trades, 1000000, 30)
print(f"收益率: {report['total_return_pct']:.2f}%")
print(f"夏普比率: {report['sharpe_ratio']:.2f}")
# calculate_metrics(equity_curve: List[float], trades: List[Dict],
# initial_cash: float, days: int) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `initial_cash` | 初始资金 |
| `final_value` | 最终资金 |
| `total_return` | 总收益率 |
| `total_return_pct` | 总收益率(%) |
| `annualized_return` | 年化收益率 |
| `annualized_return_pct` | 年化收益率(%) |
| `max_drawdown` | 最大回撤 |
| `max_drawdown_pct` | 最大回撤(%) |
| `sharpe_ratio` | 夏普比率 |
| `calmar_ratio` | 卡尔玛比率 |
| `volatility` | 波动率 |
| `trading_days` | 交易天数 |
| `trade_stats` | 交易统计(同 `get_trade_stats`) |
---
## 回测工具
### 模拟单笔交易,计算成本、手续费和净收款
```python
result = api.simulate_trade('BUY', 100.0, 100)
print(f"成本: {result['cost']}, 手续费: {result['fee']}")
# simulate_trade(action: str, price: float, quantity: int, fee_rate: float = 0.0003) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `cost` | 成本 |
| `fee` | 手续费 |
| `net_proceeds` | 净收款(卖出时) |
---
### 计算交易成本,含手续费和滑点
```python
cost = api.calculate_trade_cost('BUY', 100.0, 100, 0.0003, 0.001)
# calculate_trade_cost(action, price, quantity, fee_rate=0.0003, slippage=0.0) -> float
```
---
### 创建持仓对象,记录股票、股数、买入价和日期
```python
pos = api.create_position('600519.SH', 100, 1800.0, '2026-01-01')
# create_position(code: str, shares: int, price: float, date: str) -> Position
```
---
### 计算持仓市值
```python
value = api.get_position_value(pos, 1900.0)
# get_position_value(position: Position, current_price: float) -> float
```
---
### 计算持仓盈亏,返回盈亏金额和比例
```python
profit, pct = api.get_position_profit(position, 2000.0)
print(f"盈利: {profit}, 比例: {pct:.2%}")
# get_position_profit(position: Position, current_price: float) -> tuple
```
---
### 计算组合总价值(现金 + 所有持仓市值)
```python
value = api.calculate_portfolio_value(500000, positions, current_prices)
# calculate_portfolio_value(cash: float, positions: Dict[str, Position],
# prices: Dict[str, float]) -> float
```
---
### 获取组合持仓详情列表
```python
details = api.get_portfolio_positions(positions)
# get_portfolio_positions(positions: Dict[str, Position]) -> List[Dict]
```
---
### 从每日资产列表构建权益曲线
```python
values = [('2026-01-01', 1000000), ('2026-01-02', 1005000)]
curve = api.build_equity_curve(values)
# build_equity_curve(daily_values: List[tuple]) -> List[float]
```
---
### 计算日收益率序列
```python
returns = api.calculate_daily_returns(equity_curve)
# calculate_daily_returns(equity_curve: List[float]) -> List[float]
```
---
### 买入信号判断:MA 金叉且 RSI 超卖时返回 True
```python
if api.should_buy(close, ma5, ma20, rsi, 30):
print('买入信号')
# should_buy(current_price, ma_short, ma_long, rsi=50, rsi_oversold=30) -> bool
```
---
### 卖出信号判断:MA 死叉或 RSI 超买时返回 True
```python
if api.should_sell(close, ma5, ma20, rsi, 70):
print('卖出信号')
# should_sell(current_price, ma_short, ma_long, rsi=50, rsi_overbought=70) -> bool
```
---
### 计算权益曲线的逐日回撤序列
```python
drawdowns = api.calculate_drawdown([1000000, 1100000, 950000])
# calculate_drawdown(equity_curve: List[float]) -> List[float]
```
---
## 回测引擎控制
### 初始化回测环境,返回含现金、持仓、订单、交易记录的状态字典
```python
env = api.init_backtest(1000000, 0.0003)
# init_backtest(initial_cash: float = 1000000.0, fee_rate: float = 0.0003) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `initial_cash` | 初始资金 |
| `fee_rate` | 手续费率 |
| `cash` | 当前现金 |
| `positions` | 持仓字典 |
| `orders` | 订单列表 |
| `trades` | 交易记录 |
| `equity_curve` | 权益曲线 |
---
### 执行买入操作,自动更新 env 中的现金、持仓和交易记录
```python
result = api.execute_buy(env, '600519.SH', 1800.0, 100, '2026-01-01')
# execute_buy(env, code, price, quantity, date) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `success` | 是否成功 |
| `cost` | 成本 |
| `fee` | 手续费 |
| `reason` | 失败原因(失败时) |
---
### 执行卖出操作,自动更新 env
```python
result = api.execute_sell(env, '600519.SH', 1900.0, 100)
# execute_sell(env, code, price, quantity) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `success` | 是否成功 |
| `net_proceeds` | 净收款 |
| `fee` | 手续费 |
| `reason` | 失败原因(失败时) |
---
### 获取当前总权益(现金 + 持仓市值)
```python
equity = api.get_equity(env, current_prices)
# get_equity(env: Dict, current_prices: Dict[str, float]) -> float
```
---
### 将当日权益追加记录到 env['equity_curve']
```python
api.record_equity(env, '2026-03-01', current_prices)
# record_equity(env, date, current_prices) -> None
```
---
### 平仓,卖出结束多头持仓,返回盈亏和持有天数
```python
result = api.close_position(position, 1900.0, '2026-01-15')
print(f"盈利: {result['profit']}")
# close_position(position, price, date) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `profit` | 盈亏金额 |
| `profit_pct` | 盈亏比例 |
| `hold_days` | 持有天数 |
---
### 更新持仓的当前价格,用于实时市值计算
```python
api.update_position_price(position, 1900.0)
# update_position_price(position, current_price) -> None
```
---
### 创建本地模拟订单(非真实下单)
```python
order = api.create_order('600519.SH', 'BUY', 1800.0, 100)
# create_order(code, action, price, quantity) -> Dict
```
| 返回字段 | 说明 |
|----------|------|
| `order_id` | 订单 ID |
| `status` | 状态(`PENDING`) |
| `create_time` | 创建时间 |
---
### 取消订单,仅 PENDING 状态可取消
```python
success = api.cancel_order(order)
# cancel_order(order: Dict) -> bool
```
---
### 获取订单状态:PENDING / FILLED / CANCELLED / REJECTED
```python
status = api.get_order_status(order)
# get_order_status(order: Dict) -> str
```
---
## 策略辅助函数
### 计算指定股票近 N 日平均涨幅(%)
```python
avg_change = api.get_price_change_rate('600519.SH', '2026-03-01', 3)
# get_price_change_rate(code, date, days=3) -> Optional[float]
```
---
### 从股票列表中筛选出近 N 日涨幅最高的前 N 只,按涨幅降序返回
```python
top_stocks = api.get_top_performers(codes, '2026-03-01', 3, 3)
# [(code, avg_pct), ...]
# get_top_performers(codes, date, days=3, top_n=3) -> List[tuple]
```
---
### 获取指定日期的收盘价,无数据返回 None
```python
price = api.get_price_at_date('600519.SH', '2026-03-01')
# get_price_at_date(code, date) -> Optional[float]
```
---
### 批量获取多个日期的收盘价,按日期升序对齐
```python
prices = api.get_prices_at_dates('600519.SH', ['2026-01-01', '2026-01-02'])
# get_prices_at_dates(code, dates) -> List[Optional[float]]
```
---
### 训练 MoE 权重(遗传算法优化)
**触发场景**:用户说"优化权重"、"重新训练"、"适配最新行情"时调用。
```python
weights = api.train_moe_weights()
# 指定区间和参数
weights = api.train_moe_weights(
start_date='2025-09-01',
end_date='2026-03-01',
population_size=20, # 种群大小,越大越精准但越慢
generations=30, # 迭代代数
train_stock_count=30, # 参与训练的随机采样股票数量
)
# train_moe_weights(start_date, end_date, population_size, generations, train_stock_count) -> Dict
```
训练完成后自动将最优权重写入 `moe_weights.json`,下次调用 `get_trade_signal()` 时自动生效。
---
## 数据库维护
### 初始化所有数据库(指标缓存库等)
```python
api.init_databases()
# init_databases() -> None
```
---
### 清除技术指标缓存,可指定股票或清除全部
```python
api.clear_indicator_cache('600519.SH') # 清除指定股票
api.clear_indicator_cache() # 清除所有
# clear_indicator_cache(code: str = None) -> None
```
---
## 实时行情 (爬虫)
### 初始化爬虫
```python
from stock_crawler import StockCrawler
crawler = StockCrawler()
```
### 获取实时数据
支持从新浪财经、东方财富、同花顺获取数据。
```python
data = crawler.fetch('000001.SZ', source='sina')
# fetch(ts_code: str, source: str = 'sina') -> Dict
```
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `ts_code` | `str` | - | 股票代码,如 `000001.SZ` |
| `source` | `str` | `'sina'` | 数据源:`'sina'` (新浪), `'eastmoney'` (东方财富), `'tonghuashun'` (同花顺) |
| 返回 | 说明 |
|------|------|
| `Dict` | 包含股票实时数据的字典,字段如下 |
**返回字段说明:**
| 字段 | 类型 | 说明 | 数据源支持 |
|------|------|------|------------|
| `source` | `str` | 数据源名称 | All |
| `ts_code` | `str` | 股票代码 | All |
| `status` | `str` | 状态 (`success`/`failed`) | All |
| `name` | `str` | 股票名称 | Sina, EastMoney |
| `price` | `float` | 当前价格 | All |
| `open` | `float` | 开盘价 | All |
| `high` | `float` | 最高价 | All |
| `low` | `float` | 最低价 | All |
| `volume` | `float` | 成交量 (股) | All |
| `amount` | `float` | 成交额 (元) | All |
| `pre_close` | `float` | 昨收价 | Sina, EastMoney |
| `date` | `str` | 日期 | Sina, Tonghuashun |
| `time` | `str` | 时间 | Sina |
| `turnover_rate` | `float` | 换手率 (%) | EastMoney, Tonghuashun |
| `change_pct` | `float` | 涨跌幅 (%) | EastMoney |
| `amplitude` | `float` | 振幅 (%) | Sina, EastMoney |
| `pe_ttm` | `float` | 市盈率(TTM) | EastMoney |
| `pb` | `float` | 市净率 | EastMoney |
| `total_cap` | `float` | 总市值 (元) | EastMoney |
| `circ_cap` | `float` | 流通市值 (元) | EastMoney |
| `total_shares` | `float` | 总股本 (股) | EastMoney |
| `circ_shares` | `float` | 流通股 (股) | EastMoney |
FILE:assets/config.json
{
"version": "1.0.0",
"base_url": "http://info.aicodingyard.com",
"http_timeout": 30
}
FILE:assets/requirements.txt
pandas>=1.3.5
requests>=2.31.0
SQLAlchemy>=2.0.48
numpy>=1.21.0