YutoAIYutoAIPrompts
PromptsSkillsWorkflowsCategoriesTagsPromptmasters
Developers
Login
Prompts Awesome by YutoAI © 2021-2026
w

wangzhi43

@clawhub-wangzhi43-f2e391be5c

4prompts
0upvotes received
0contributions
Joined 4 months ago
4 contributions in the last year
Sep
Oct
Nov
Dec
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
M
W
F
Less
More
BitSoul AI Face Beauty 人像AI美颜
Skill

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()
ClawHubCodingBackend+2
W@clawhub-wangzhi43-f2e391be5c
0
bitsoul ppt 制作器
Skill

将用户的内容一键生成罗振宇风格的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文件,可直接打开运行

## 严禁行为

- 堆字 / 密集排版
- 花哨配色
- 复杂图表
- 横屏比例
- 偏离极简科技风

ClawHubAI Agents
W@clawhub-wangzhi43-f2e391be5c
0
BitSoulStockSkill
Skill

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
ClawHubCodingBackend+2
W@clawhub-wangzhi43-f2e391be5c
0
BitSoulStockSkill
Skill

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
ClawHubCodingBackend+2
W@clawhub-wangzhi43-f2e391be5c
0