Skill: wu-xiaolin/a-share-short-decision-bak (version 1.0.0)

--- name: A股短线交易决策 A-Share Short-Term Decision slug: a-share-short-decision description: A-share short-term trading decision skill for 1-5 day horizon. Use when you need real-data market sentiment, sector rotation, strong stock scanning, capital flow confirmation, date-based short-term signal scoring, prediction logging, and next-day market comparison for CN A-share momentum trading. --- # A-Share Short-Term Decision Skill Implement in sequence: 1. Run `short_term_signal_engine(analysis_date)` for target date. 2. If needed, persist prediction with `run_prediction_for_date(analysis_date)`. 3. Compare prediction vs actual market with `compare_prediction_with_market(prediction_date, actual_date)`. 4. Output report with `generate_daily_report(analysis_date)`. ## Tool Contracts ### `short_term_signal_engine(analysis_date=None)` - `analysis_date`: `YYYY-MM-DD` or `YYYYMMDD` - Returns weighted short-term score and recommendation status. - Always returns friendly `no_recommendation_message` when no tradable candidate exists. ### `run_prediction_for_date(analysis_date)` - Runs signal engine for the specified date. - Appends decision snapshot into `data/decision_log.jsonl`. ### `compare_prediction_with_market(prediction_date, actual_date=None)` - Loads prediction from log (or auto-generates if missing). - Compares predicted candidates against real market closes on `actual_date`. - Returns per-stock return and summary statistics. ## No-Recommendation Behavior Required behavior: - Never return empty output. - If `candidates` is empty or signal is `NO_TRADE`, explicitly say: `当前暂无可执行短线买入标的`. - Include reason and next action. ## Runtime ```bash python3 main.py short_term_signal_engine --date 2026-02-12 python3 main.py run_prediction_for_date --date 2026-02-12 python3 main.py compare_prediction_with_market --prediction-date 2026-02-12 --actual-date 2026-02-13 python3 main.py generate_daily_report --date 2026-02-12 ``` ## Subskills Workflow For recurring optimize-then-recommend flow, run: ```bash python3 subskills/config-optimization/optimize_from_aggressive.py --analysis-period "2026-02-01 to 2026-02-12" python3 subskills/daily-recommendation/generate_daily_recommendation.py --date 2026-02-14 ``` All generated artifacts are stored under `data/`.

path: README.md
content: # a-share-short-decision

A股短线（1-5日）决策 Skill，基于 AkShare 实时/历史数据进行情绪、板块、个股与资金流分析。

## 目录结构

```text
a-share-short-decision/
├── tools/
│   ├── __init__.py
│   ├── settings.py
│   ├── market_data.py
│   ├── indicators.py
│   ├── sentiment.py
│   ├── money_flow.py
│   ├── fusion_engine.py
│   ├── risk_control.py
│   ├── reporting.py
│   └── decision_eval.py
├── prompts/
│   └── analysis_prompt.txt
├── data/
│   └── decision_log.jsonl
├── scheduler.yaml
├── config.json
├── main.py
├── SKILL.md
└── README.md
```

## 安装

```bash
pip install akshare pandas
```

## 常用命令

```bash
# 当日短线信号
python main.py short_term_signal_engine

# 指定日期短线信号
python main.py short_term_signal_engine --date 2026-02-12

# 记录指定日期预测
python main.py run_prediction_for_date --date 2026-02-12

# 对比预测与实际表现
python main.py compare_prediction_with_market --prediction-date 2026-02-12 --actual-date 2026-02-13

# 生成日报
python main.py generate_daily_report --date 2026-02-12

# 调试模式
python main.py short_term_signal_engine --date 2026-02-12 --debug
```

## 数据策略

- 默认严格真实数据模式：`config.json` 中 `data_source.fallback_enabled=false`
- 当关键数据不可用时，返回 `data_source: unavailable` 和友好提示，不返回模拟股票

## 筛股参数配置

筛股参数位于 `config.json` 的 `strategy.screener`。

主要字段：
- `prefilter_change_pct`：初筛涨幅阈值（实时池）
- `min_change_pct`：最终候选最低涨幅
- `min_volume_ratio`：最低量比阈值
- `trend_lookback`：趋势判断回看天数
- `min_history_days`：最小历史K线天数
- `volume_baseline_days`：量比基准窗口
- `high_volume_bearish_drop_pct`：高位巨量阴线跌幅阈值
- `high_volume_bearish_vol_ratio`：高位巨量阴线量比阈值
- `historical_mode.*`：历史日期模式专用阈值

---

## 三档模板（可直接复制）

使用方式：将 `config.json` 里的 `strategy.screener` 整段替换为以下任意一档。

### 1) 激进（更多标的，容忍噪音）

```json
"screener": {
  "prefilter_change_pct": 3.0,
  "min_change_pct": 3.5,
  "min_volume_ratio": 1.1,
  "trend_lookback": 2,
  "min_history_days": 5,
  "volume_baseline_days": 4,
  "high_volume_bearish_drop_pct": -4.0,
  "high_volume_bearish_vol_ratio": 3.0,
  "historical_mode": {
    "min_change_pct": 2.0,
    "trend_lookback": 2,
    "min_volume_ratio": 1.0,
    "high_volume_bearish_drop_pct": -4.5,
    "high_volume_bearish_vol_ratio": 3.2,
    "relaxed_pick_from_pool_when_empty": true
  }
}
```

适用：情绪强周期、想要更多候选、可接受更高回撤。

### 2) 平衡（默认推荐）

```json
"screener": {
  "prefilter_change_pct": 4.5,
  "min_change_pct": 5.0,
  "min_volume_ratio": 1.5,
  "trend_lookback": 3,
  "min_history_days": 6,
  "volume_baseline_days": 5,
  "high_volume_bearish_drop_pct": -2.0,
  "high_volume_bearish_vol_ratio": 2.2,
  "historical_mode": {
    "min_change_pct": 3.0,
    "trend_lookback": 2,
    "min_volume_ratio": 1.2,
    "high_volume_bearish_drop_pct": -3.0,
    "high_volume_bearish_vol_ratio": 2.8,
    "relaxed_pick_from_pool_when_empty": true
  }
}
```

适用：大多数常规短线场景。

### 3) 保守（数量更少，质量优先）

```json
"screener": {
  "prefilter_change_pct": 6.0,
  "min_change_pct": 7.0,
  "min_volume_ratio": 1.8,
  "trend_lookback": 4,
  "min_history_days": 8,
  "volume_baseline_days": 6,
  "high_volume_bearish_drop_pct": -1.5,
  "high_volume_bearish_vol_ratio": 1.8,
  "historical_mode": {
    "min_change_pct": 4.5,
    "trend_lookback": 3,
    "min_volume_ratio": 1.4,
    "high_volume_bearish_drop_pct": -2.2,
    "high_volume_bearish_vol_ratio": 2.2,
    "relaxed_pick_from_pool_when_empty": false
  }
}
```

适用：震荡期/弱市，优先控制误判与回撤。

---

## 风险说明

本工具仅用于策略研究与辅助决策，不构成投资建议。股市有风险，入市需谨慎。


path: _meta.json
content: {
  "owner": "kenera",
  "slug": "a-share-short-decision",
  "displayName": "A股短线交易决策 A Share Short Term Decision",
  "latest": {
    "version": "1.2.0",
    "publishedAt": 1771041578079,
    "commit": "https://github.com/openclaw/skills/commit/9ee1538b1e3779cb28ddec8811e73628b36ddc01"
  },
  "history": [
    {
      "version": "1.0.0",
      "publishedAt": 1770979998259,
      "commit": "https://github.com/openclaw/skills/commit/73a5f6d0898deb315f5ad12dc69621868fc75aa6"
    }
  ]
}


path: config.json
content: {
  "skill_name": "a-share-short-decision",
  "version": "1.2.0",
  "market": "CN-A",
  "data_source": {
    "provider": "akshare",
    "fallback_enabled": false
  },
  "strategy": {
    "holding_days_default": "1-3",
    "weights": {
      "market_sentiment": 0.25,
      "sector_strength": 0.25,
      "stock_volume_strength": 0.2,
      "capital_inflow": 0.2,
      "technical_structure": 0.1
    },
    "thresholds": {
      "buy": 75,
      "watch": 60,
      "market_block": 40
    },
    "screener": {
      "prefilter_change_pct": 2.8,
      "min_change_pct": 3.2,
      "min_volume_ratio": 1.15,
      "trend_lookback": 2,
      "min_history_days": 5,
      "volume_baseline_days": 4,
      "high_volume_bearish_drop_pct": -3.8,
      "high_volume_bearish_vol_ratio": 2.8,
      "historical_mode": {
        "min_change_pct": 1.8,
        "trend_lookback": 2,
        "min_volume_ratio": 1.05,
        "high_volume_bearish_drop_pct": -4.2,
        "high_volume_bearish_vol_ratio": 3.0,
        "relaxed_pick_from_pool_when_empty": true
      }
    }
  },
  "risk_control": {
    "max_single_position": 0.15,
    "stop_loss_pct": -6,
    "take_profit_pct": 12,
    "ma5_break_stop": true
  },
  "optimization_info": {
    "base_date": "2026-02-14",
    "analysis_period": "2026-02-01 to 2026-02-12",
    "config_type": "aggressive_optimized",
    "expected_improvements": {
      "candidate_increase": "15-25%",
      "success_rate_improvement": "3-5 percentage points",
      "avg_return_improvement": "0.5-1.0 percentage points"
    }
  }
}

path: main.py
content: """Simple CLI for the A-share short-term decision skill."""

from __future__ import annotations

import argparse
import json
import os

from tools.decision_eval import compare_prediction_with_market, run_prediction_for_date
from tools.fusion_engine import short_term_signal_engine
from tools.market_data import get_market_sentiment, get_sector_rotation, scan_strong_stocks
from tools.money_flow import analyze_capital_flow
from tools.reporting import generate_daily_report
from tools.risk_control import short_term_risk_control


def _print(data: object) -> None:
    print(json.dumps(data, ensure_ascii=False, indent=2))


def main() -> None:
    parser = argparse.ArgumentParser(description="A-share short-term decision tools")
    parser.add_argument(
        "tool",
        choices=[
            "get_market_sentiment",
            "get_sector_rotation",
            "scan_strong_stocks",
            "analyze_capital_flow",
            "short_term_signal_engine",
            "short_term_risk_control",
            "generate_daily_report",
            "run_prediction_for_date",
            "compare_prediction_with_market",
        ],
    )
    parser.add_argument("--symbol", default=None, help="stock symbol for capital flow tool")
    parser.add_argument("--score", type=float, default=50, help="market sentiment score for risk control tool")
    parser.add_argument("--date", default=None, help="analysis date, format YYYY-MM-DD or YYYYMMDD")
    parser.add_argument("--prediction-date", default=None, help="prediction date for comparison, format YYYY-MM-DD")
    parser.add_argument("--actual-date", default=None, help="actual comparison date, format YYYY-MM-DD")
    parser.add_argument("--debug", action="store_true", help="enable debug_info in outputs")
    args = parser.parse_args()

    debug = bool(args.debug)
    if debug:
        os.environ["SHORT_DECISION_DEBUG"] = "1"

    if args.tool == "get_market_sentiment":
        _print(get_market_sentiment(analysis_date=args.date, debug=debug))
    elif args.tool == "get_sector_rotation":
        _print(get_sector_rotation(analysis_date=args.date, debug=debug))
    elif args.tool == "scan_strong_stocks":
        _print(scan_strong_stocks(analysis_date=args.date, debug=debug))
    elif args.tool == "analyze_capital_flow":
        _print(analyze_capital_flow(symbol=args.symbol, analysis_date=args.date, debug=debug))
    elif args.tool == "short_term_signal_engine":
        _print(short_term_signal_engine(analysis_date=args.date, debug=debug))
    elif args.tool == "short_term_risk_control":
        _print(short_term_risk_control(args.score))
    elif args.tool == "generate_daily_report":
        _print(generate_daily_report(analysis_date=args.date, debug=debug))
    elif args.tool == "run_prediction_for_date":
        if not args.date:
            raise SystemExit("--date is required for run_prediction_for_date")
        _print(run_prediction_for_date(analysis_date=args.date, debug=debug))
    else:
        pred_date = args.prediction_date or args.date
        if not pred_date:
            raise SystemExit("--prediction-date (or --date) is required for compare_prediction_with_market")
        _print(
            compare_prediction_with_market(
                prediction_date=pred_date,
                actual_date=args.actual_date,
                auto_generate_if_missing=True,
                debug=debug,
            )
        )


if __name__ == "__main__":
    main()


path: prompts/__init__.py
content: 404: Not Found

path: requirements.md
content: 12 files changed
+539
-218



README.md
这是一套专门为 A股短线（1~5日） 设计的 OpenClaw Skill 方案,请实现.
🎯 一、短线模型核心逻辑
A股短线最重要的 4 件事：
情绪强度（涨停家数、连板高度）
板块强度（行业轮动）
成交量放大
资金流向（主力/北向）
技术指标在短线里权重较低。
🧠 二、短线模型结构（重构版）
市场情绪监测
      ↓
强势板块识别
      ↓
个股强弱筛选
      ↓
量能确认
      ↓
资金确认
      ↓
评分引擎
      ↓
风控过滤
      ↓
OpenClaw解释 + 推送

🏗 三、Tool 设计
短线专用工具,数据来源AkShare
1️⃣ Tool: get_market_sentiment ⭐⭐⭐
短线最重要模块。
数据：
今日涨停家数
连板高度
跌停家数
炸板率
成交额
输出：
{
  "limit_up": 65,
  "limit_down": 4,
  "max_height": 4,
  "break_rate": 0.18,
  "market_sentiment_score": 72
}

评分逻辑：
涨停 > 50 → 情绪强
跌停 < 10 → 风险低
连板 ≥ 3 → 投机活跃
2️⃣ Tool: get_sector_rotation ⭐⭐⭐
找出当天强势板块。
数据：
板块涨幅
板块成交额
板块涨停数量
输出：
{
  "top_sectors": [
    {"name": "算力", "strength": 85},
    {"name": "AI", "strength": 78}
  ]
}

3️⃣ Tool: scan_strong_stocks ⭐⭐⭐
在强势板块中筛选个股：
筛选条件：
今日涨幅 > 5%
成交量放大 > 1.5
近3日趋势向上
未处于高位巨量阴线
输出：
[
  {
    "code": "300XXX",
    "volume_ratio": 2.3,
    "strength_rank": 1
  }
]

4️⃣ Tool: analyze_capital_flow
短线非常关键。
关注：
主力净流入
北向连续流入天数
资金强度排名
输出：
{
  "main_flow": 180000000,
  "flow_trend": "3-day-inflow"
}

5️⃣ Tool: short_term_signal_engine（核心重构）
短线评分模型（0~100）：
因子权重
市场情绪
25%
板块强度
25%
个股量能
20%
资金流入
20%
技术结构
10%
规则：
score ≥ 75 → 强势短线标的
60~75 → 观察
<60 → 放弃
输出：
{
  "score": 82,
  "signal": "SHORT_BUY",
  "holding_days": "1-3",
  "confidence": 0.68
}

6️⃣ Tool: short_term_risk_control ⭐⭐⭐
短线必须风控严格。
规则：
单股仓位 ≤ 15%
跌破5日线止损
-6% 自动止损
市场情绪 < 40 禁止开仓
输出：
{
  "max_position": 0.15,
  "stop_loss": -6,
  "take_profit": 12,
  "market_filter": true
}

📜 四、短线 Agent Prompt（重写）
System Prompt：
你是A股短线交易策略分析师。

目标：
1-3日交易周期
优先考虑情绪与量能
严格风险控制

请输出：
- 当前市场情绪判断
- 强势板块
- 推荐个股（如有）
- 入场逻辑
- 止损建议
- 风险提示

避免长期投资逻辑。

� 五、完整 Skill 目录结构建议
a-share-short-decision/
│
├── tools/
│   ├── market_data.py
│   ├── indicators.py
│   ├── sentiment.py
│   ├── money_flow.py
│   ├── fusion_engine.py
│   └── risk_control.py
│
├── prompts/
│   └── analysis_prompt.txt
│
├── scheduler.yaml
├── config.json
└── README.md

�📅 六、短线每日流程设计
14:30 盘中扫描
情绪判断
强板块识别
强势个股初筛
15:10 收盘确认
量能是否确认
是否封板
是否放量回落
15:20 推送报告
📨 七、每日推送格式
【A股短线日报】

市场情绪：偏强（涨停62家）
最高连板：4板
炸板率：18%

强势板块：
1. AI算力
2. 存储芯片

短线关注：
300XXX
- 量能放大2.3倍
- 主力净流入1.8亿
- 板块龙头

建议：
轻仓试错（≤15%）
止损 -6%
目标 +10~15%

风险：
指数接近压力位

🔥 八、短线模型关键升级点
你可以继续升级：
1️⃣ 打板模型
2️⃣ 低吸模型
3️⃣ 反包模型
4️⃣ 龙头战法
5️⃣ 连板博弈模型
⚠️ 九、短线必须知道的现实
A股短线：
极端波动
情绪切换快
黑天鹅多
必须：
小仓位
严格止损
只做情绪上升期

path: scheduler.yaml
content: version: 1
timezone: Asia/Shanghai
jobs:
  - id: intraday_scan
    cron: "30 14 * * 1-5"
    task: short_term_signal_engine
    description: "14:30 intraday scan for sentiment, sectors and strong stock candidates"

  - id: close_confirm
    cron: "10 15 * * 1-5"
    task: short_term_signal_engine
    description: "15:10 close confirmation for volume and board lock validation"

  - id: daily_push
    cron: "20 15 * * 1-5"
    task: generate_daily_report
    description: "15:20 publish short-term A-share daily report"


path: tools/__init__.py
content: """A-share short-term decision tools package."""

from .decision_eval import compare_prediction_with_market, run_prediction_for_date
from .fusion_engine import short_term_signal_engine
from .market_data import get_market_sentiment, get_sector_rotation, scan_strong_stocks
from .money_flow import analyze_capital_flow
from .reporting import generate_daily_report
from .risk_control import short_term_risk_control

__all__ = [
    "get_market_sentiment",
    "get_sector_rotation",
    "scan_strong_stocks",
    "analyze_capital_flow",
    "short_term_signal_engine",
    "short_term_risk_control",
    "generate_daily_report",
    "run_prediction_for_date",
    "compare_prediction_with_market",
]


path: tools/debug_utils.py
content: """Debug helpers for conditional verbose diagnostics."""

from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any


def resolve_debug(debug: bool = False) -> bool:
    if debug:
        return True
    raw = os.getenv("SHORT_DECISION_DEBUG", "").strip().lower()
    return raw in ("1", "true", "yes", "on")


def with_debug(payload: dict[str, Any], debug: bool, debug_info: dict[str, Any]) -> dict[str, Any]:
    if not debug:
        return payload
    enriched = dict(payload)
    enriched["debug_info"] = debug_info
    return enriched


def is_fallback_enabled(default: bool = False) -> bool:
    """
    Resolve fallback behavior.

    Priority:
    1) env SHORT_DECISION_FALLBACK_ENABLED
    2) config.json data_source.fallback_enabled
    3) default
    """
    raw = os.getenv("SHORT_DECISION_FALLBACK_ENABLED", "").strip().lower()
    if raw in ("1", "true", "yes", "on"):
        return True
    if raw in ("0", "false", "no", "off"):
        return False

    try:
        cfg_path = Path(__file__).resolve().parents[1] / "config.json"
        cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
        return bool(cfg.get("data_source", {}).get("fallback_enabled", default))
    except Exception:
        return default


path: tools/decision_eval.py
content: """Prediction logging and next-day comparison utilities."""

from __future__ import annotations

import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict

try:
    import pandas as pd
except Exception:  # pragma: no cover
    pd = None  # type: ignore

try:
    import akshare as ak  # type: ignore
except Exception:  # pragma: no cover
    ak = None  # type: ignore

from .fusion_engine import short_term_signal_engine

DATA_DIR = Path(__file__).resolve().parents[1] / "data"
LOG_PATH = DATA_DIR / "decision_log.jsonl"


def _ensure_data_dir() -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)


def _append_jsonl(path: Path, item: dict[str, Any]) -> None:
    _ensure_data_dir()
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(item, ensure_ascii=False) + "\n")


def _load_entries(path: Path) -> list[dict[str, Any]]:
    if not path.exists():
        return []
    entries: list[dict[str, Any]] = []
    with path.open("r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                entries.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return entries


def run_prediction_for_date(analysis_date: str, debug: bool = False) -> dict[str, Any]:
    signal = short_term_signal_engine(analysis_date=analysis_date, debug=debug)
    entry = {
        "logged_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        "analysis_date": signal.get("analysis_date", analysis_date),
        "signal": signal,
    }
    _append_jsonl(LOG_PATH, entry)
    return {
        "saved": True,
        "log_path": str(LOG_PATH),
        "analysis_date": entry["analysis_date"],
        "signal": signal,
    }


def _pick_entry(entries: list[dict[str, Any]], analysis_date: str) -> dict[str, Any] | None:
    for item in reversed(entries):
        if str(item.get("analysis_date", "")) == analysis_date:
            return item
    return None


def _to_records(frame: Any) -> list[dict[str, Any]]:
    if frame is None or pd is None:
        return []
    if not isinstance(frame, pd.DataFrame) or frame.empty:
        return []
    return frame.to_dict(orient="records")


def _num(row: dict[str, Any], keys: list[str], default: float = 0.0) -> float:
    for key in keys:
        if key in row and row[key] not in (None, ""):
            try:
                return float(row[key])
            except Exception:
                continue
    return default



def compare_prediction_with_market(
    prediction_date: str,
    actual_date: str | None = None,
    auto_generate_if_missing: bool = True,
    debug: bool = False,
) -> dict[str, Any]:
    entries = _load_entries(LOG_PATH)
    entry = _pick_entry(entries, prediction_date)

    if entry is None and auto_generate_if_missing:
        generated = run_prediction_for_date(prediction_date, debug=debug)
        entry = {
            "analysis_date": generated.get("analysis_date", prediction_date),
            "signal": generated.get("signal", {}),
            "logged_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        }

    if entry is None:
        return {
            "ok": False,
            "message": "未找到该日期预测记录，请先运行 run_prediction_for_date。",
            "prediction_date": prediction_date,
        }

    signal = entry.get("signal", {})
    candidates = signal.get("candidates", []) if isinstance(signal, dict) else []
    if not candidates:
        return {
            "ok": True,
            "prediction_date": prediction_date,
            "actual_date": actual_date or datetime.now().strftime("%Y-%m-%d"),
            "comparison": [],
            "summary": {
                "picked": 0,
                "hit_ratio": 0.0,
                "avg_return_pct": 0.0,
            },
            "message": "该日无可执行推荐标的，无法进行收益对比。",
            "signal": signal,
        }

    if ak is None or pd is None:
        return {
            "ok": False,
            "message": "akshare 或 pandas 不可用，无法做真实市场对比。",
            "prediction_date": prediction_date,
            "signal": signal,
        }

    pred_dt = datetime.strptime(prediction_date, "%Y-%m-%d")
    actual_dt = datetime.strptime(actual_date, "%Y-%m-%d") if actual_date else datetime.now()

    pred_ymd = pred_dt.strftime("%Y%m%d")
    actual_ymd = actual_dt.strftime("%Y%m%d")

    comparison: list[dict[str, Any]] = []
    for item in candidates:
        code = str(item.get("code", "")).strip()
        name = str(item.get("name", "")).strip()
        if not code:
            continue
        try:
            hist = ak.stock_zh_a_hist(symbol=code, period="daily", start_date=pred_ymd, end_date=actual_ymd, adjust="qfq")
            rows = _to_records(hist)
        except Exception:
            rows = []

        if len(rows) < 2:
            comparison.append(
                {
                    "code": code,
                    "name": name,
                    "status": "no_data",
                    "return_pct": None,
                }
            )
            continue

        first = rows[0]
        last = rows[-1]
        buy_px = _num(first, ["收盘"], 0.0)
        sell_px = _num(last, ["收盘"], 0.0)
        ret = ((sell_px - buy_px) / buy_px * 100.0) if buy_px else 0.0

        comparison.append(
            {
                "code": code,
                "name": name,
                "buy_date": str(first.get("日期", prediction_date)),
                "buy_close": round(buy_px, 4),
                "eval_date": str(last.get("日期", actual_date or datetime.now().strftime("%Y-%m-%d"))),
                "eval_close": round(sell_px, 4),
                "return_pct": round(ret, 2),
                "hit": ret > 0,
            }
        )

    valid_returns = [x["return_pct"] for x in comparison if isinstance(x.get("return_pct"), (int, float))]
    hits = [x for x in comparison if x.get("hit") is True]

    avg_ret = sum(valid_returns) / len(valid_returns) if valid_returns else 0.0
    hit_ratio = len(hits) / len(valid_returns) if valid_returns else 0.0

    return {
        "ok": True,
        "prediction_date": prediction_date,
        "actual_date": actual_date or datetime.now().strftime("%Y-%m-%d"),
        "comparison": comparison,
        "summary": {
            "picked": len(candidates),
            "evaluated": len(valid_returns),
            "hit_ratio": round(hit_ratio, 4),
            "avg_return_pct": round(avg_ret, 4),
        },
        "signal": signal,
    }



path: tools/fusion_engine.py
content: """Signal fusion engine for short-term stock decisions."""

from __future__ import annotations

from typing import Any, Dict, List

from .debug_utils import resolve_debug, with_debug
from .indicators import clamp
from .market_data import get_market_sentiment, get_sector_rotation, scan_strong_stocks
from .money_flow import analyze_capital_flow
from .risk_control import short_term_risk_control


def _calc_sector_score(top_sectors: List[dict[str, Any]]) -> float:
    if not top_sectors:
        return 0.0
    top3 = top_sectors[:3]
    return sum(float(x.get("strength", 0.0)) for x in top3) / len(top3)


def _calc_stock_volume_score(stocks: List[dict[str, Any]]) -> float:
    if not stocks:
        return 0.0
    top = stocks[0]
    ratio = float(top.get("volume_ratio", 0.0))
    change = float(top.get("change_pct", 0.0))
    return clamp(ratio / 3.0 * 60 + change / 10.0 * 40, 0, 100)


def _calc_capital_score(capital: dict[str, Any]) -> float:
    main_flow = float(capital.get("main_flow", 0.0))
    inflow_days = float(capital.get("northbound_inflow_days", 0.0))
    score = clamp(main_flow / 300_000_000 * 70, 0, 70) + clamp(inflow_days / 5 * 30, 0, 30)
    return clamp(score, 0, 100)


def _calc_technical_score(stocks: List[dict[str, Any]]) -> float:
    if not stocks:
        return 0.0
    top = stocks[0]
    ratio = float(top.get("volume_ratio", 0.0))
    return clamp(55 + ratio * 10, 0, 100)


def _build_no_recommendation_message(
    signal: str,
    stocks: List[dict[str, Any]],
    top_sectors: List[dict[str, Any]],
    sentiment_score: float,
) -> str:
    if signal == "SHORT_BUY" and stocks:
        return "已筛选到可跟踪标的，请结合盘中量能确认后执行。"
    if sentiment_score < 40:
        return "当前市场情绪偏弱，建议禁止开新仓，等待情绪修复。"
    if not top_sectors:
        return "未获取到可靠板块轮动数据，建议今日观望，等待数据恢复。"
    if not stocks:
        return "当前未筛选到满足短线条件的真实标的，建议继续观望。"
    return "当前不满足短线开仓条件，建议观望。"


def short_term_signal_engine(analysis_date: str | None = None, debug: bool = False) -> Dict[str, Any]:
    """Build weighted short-term signal."""
    debug = resolve_debug(debug)
    sentiment = get_market_sentiment(analysis_date=analysis_date, debug=debug)
    sector_info = get_sector_rotation(top_n=5, analysis_date=analysis_date, debug=debug)
    sector_names = [x["name"] for x in sector_info.get("top_sectors", [])]
    stocks = scan_strong_stocks(sectors=sector_names, top_n=5, analysis_date=analysis_date, debug=debug)
    target_symbol = stocks[0]["code"] if stocks else None
    capital = analyze_capital_flow(symbol=target_symbol, analysis_date=analysis_date, debug=debug)
    risk = short_term_risk_control(float(sentiment.get("market_sentiment_score", 0.0)))

    sentiment_score = float(sentiment.get("market_sentiment_score", 0.0))
    sector_score = _calc_sector_score(sector_info.get("top_sectors", []))
    stock_score = _calc_stock_volume_score(stocks)
    capital_score = _calc_capital_score(capital)
    technical_score = _calc_technical_score(stocks)

    score = (
        sentiment_score * 0.25
        + sector_score * 0.25
        + stock_score * 0.20
        + capital_score * 0.20
        + technical_score * 0.10
    )
    score = round(clamp(score, 0, 100), 2)

    if score >= 75 and risk["market_filter"] and len(stocks) > 0:
        signal = "SHORT_BUY"
        confidence = clamp(score / 100.0 * 0.9, 0.0, 0.95)
        holding_days = "1-3"
    elif score >= 60 and risk["market_filter"]:
        signal = "WATCHLIST"
        confidence = clamp(score / 100.0 * 0.75, 0.0, 0.85)
        holding_days = "1-2"
    else:
        signal = "NO_TRADE"
        confidence = clamp(score / 100.0 * 0.6, 0.0, 0.7)
        holding_days = "0"

    no_msg = _build_no_recommendation_message(signal, stocks, sector_info.get("top_sectors", []), sentiment_score)

    payload = {
        "analysis_date": sentiment.get("analysis_date", analysis_date),
        "score": score,
        "signal": signal,
        "holding_days": holding_days,
        "confidence": round(confidence, 2),
        "risk_control": risk,
        "market_sentiment": sentiment,
        "top_sectors": sector_info.get("top_sectors", []),
        "candidates": stocks,
        "capital_flow": capital,
        "has_recommendation": len(stocks) > 0 and signal == "SHORT_BUY",
        "no_recommendation_message": no_msg,
        "factor_breakdown": {
            "market_sentiment": round(sentiment_score, 2),
            "sector_strength": round(sector_score, 2),
            "stock_volume_strength": round(stock_score, 2),
            "capital_inflow": round(capital_score, 2),
            "technical_structure": round(technical_score, 2),
        },
    }
    if not debug:
        return payload
    debug_info = {
        "module": "short_term_signal_engine",
        "selected_symbol": target_symbol,
        "sources": {
            "market_sentiment": sentiment.get("data_source", "unknown"),
            "sector_rotation": sector_info.get("data_source", "unknown"),
            "capital_flow": capital.get("data_source", "unknown"),
        },
    }
    return with_debug(payload, debug, debug_info)


path: tools/indicators.py
content: """Reusable indicators for short-term A-share analysis."""

from __future__ import annotations

from typing import Iterable, List


def clamp(value: float, low: float, high: float) -> float:
    """Clamp value into [low, high]."""
    return max(low, min(high, value))


def safe_pct_change(current: float, previous: float) -> float:
    """Return percentage change. Return 0 when denominator is invalid."""
    if previous == 0:
        return 0.0
    return (current - previous) / abs(previous)


def volume_ratio(current_volume: float, baseline_volume: float) -> float:
    """Return current volume divided by baseline volume."""
    if baseline_volume <= 0:
        return 0.0
    return current_volume / baseline_volume


def moving_average(values: Iterable[float], window: int) -> float:
    """Calculate simple moving average on the latest window."""
    seq: List[float] = list(values)
    if not seq:
        return 0.0
    if window <= 0:
        return seq[-1]
    sliced = seq[-window:] if len(seq) >= window else seq
    return sum(sliced) / len(sliced)


def trend_up(values: Iterable[float], lookback: int = 3) -> bool:
    """Check whether latest lookback values are strictly improving."""
    seq: List[float] = list(values)
    if len(seq) < lookback:
        return False
    recent = seq[-lookback:]
    return all(recent[idx] > recent[idx - 1] for idx in range(1, len(recent)))


def score_bucket(value: float, bands: list[tuple[float, float]]) -> float:
    """
    Convert a raw value into score.

    bands format: [(threshold, score), ...], sorted by threshold descending.
    """
    for threshold, score in bands:
        if value >= threshold:
            return score
    return 0.0


path: tools/money_flow.py
content: """Capital flow analysis tools."""

from __future__ import annotations

from datetime import datetime, timedelta
import math
from typing import Any, Dict

from .debug_utils import is_fallback_enabled, resolve_debug, with_debug

try:
    import pandas as pd
except Exception:  # pragma: no cover
    pd = None  # type: ignore

try:
    import akshare as ak  # type: ignore
except Exception:  # pragma: no cover
    ak = None  # type: ignore


def _to_records(frame: Any) -> list[dict[str, Any]]:
    if frame is None or pd is None:
        return []
    if not isinstance(frame, pd.DataFrame) or frame.empty:
        return []
    return frame.to_dict(orient="records")


def _normalize_number(value: Any) -> float:
    if value in ("", None):
        return 0.0
    if isinstance(value, (int, float)):
        out = float(value)
        return out if math.isfinite(out) else 0.0
    text = str(value).strip().replace(",", "")
    if text in ("-", "--"):
        return 0.0
    unit = 1.0
    if text.endswith("亿"):
        unit = 100_000_000.0
        text = text[:-1]
    elif text.endswith("万"):
        unit = 10_000.0
        text = text[:-1]
    try:
        out = float(text) * unit
        return out if math.isfinite(out) else 0.0
    except Exception:
        return 0.0


def _num_unit(row: dict[str, Any], keys: list[str], default: float = 0.0) -> float:
    for key in keys:
        if key in row:
            parsed = _normalize_number(row[key])
            if parsed != 0.0 or row[key] in (0, "0", "0.0"):
                return parsed
    return default


def _parse_analysis_date(analysis_date: str | None) -> tuple[str, datetime]:
    if not analysis_date:
        now = datetime.now()
        return now.strftime("%Y%m%d"), now
    cleaned = analysis_date.strip()
    for fmt in ("%Y-%m-%d", "%Y%m%d"):
        try:
            d = datetime.strptime(cleaned, fmt)
            return d.strftime("%Y%m%d"), d
        except ValueError:
            pass
    now = datetime.now()
    return now.strftime("%Y%m%d"), now


def _fallback_capital_flow(
    symbol: str | None = None, debug: bool = False, debug_info: dict[str, Any] | None = None
) -> Dict[str, Any]:
    payload = {
        "symbol": symbol or "market",
        "main_flow": 0,
        "northbound_net": 0,
        "northbound_inflow_days": 0,
        "flow_trend": "unknown",
        "strength_rank": 100,
        "data_source": "fallback",
        "error": "real_data_required",
    }
    return with_debug(payload, debug, debug_info or {"reason": "fallback_empty_capital_flow"})


def _count_consecutive_inflow(values: list[float]) -> int:
    count = 0
    for item in reversed(values):
        if item > 0:
            count += 1
        else:
            break
    return count


def _load_northbound_series(debug_info: dict[str, Any] | None = None) -> list[float]:
    if ak is None or pd is None:
        return []

    try:
        flow_df = ak.stock_hsgt_north_net_flow_in_em(symbol="北上")
        flow_records = _to_records(flow_df)
        if debug_info is not None:
            debug_info.setdefault("api_calls", []).append(
                {"api": "stock_hsgt_north_net_flow_in_em", "ok": True, "rows": len(flow_records)}
            )
        vals = [_num_unit(x, ["value", "净流入", "当日净流入", "净买额"], 0.0) for x in flow_records]
        vals = [x for x in vals if x != 0.0]
        if vals:
            return vals[-60:]
    except Exception:
        if debug_info is not None:
            debug_info.setdefault("api_calls", []).append({"api": "stock_hsgt_north_net_flow_in_em", "ok": False})

    if hasattr(ak, "stock_hsgt_hist_em"):
        for symbol in ("北向资金", "沪股通", "深股通"):
            try:
                hist_df = ak.stock_hsgt_hist_em(symbol=symbol)
                hist_records = _to_records(hist_df)
                if debug_info is not None:
                    debug_info.setdefault("api_calls", []).append(
                        {"api": "stock_hsgt_hist_em", "symbol": symbol, "ok": True, "rows": len(hist_records)}
                    )
                vals = [_num_unit(x, ["当日成交净买额", "当日净流入", "净买额", "净流入"], 0.0) for x in hist_records]
                vals = [x for x in vals if x != 0.0]
                if vals:
                    return vals[-60:]
            except Exception:
                if debug_info is not None:
                    debug_info.setdefault("api_calls", []).append({"api": "stock_hsgt_hist_em", "symbol": symbol, "ok": False})
    return []


def _load_symbol_main_flow(symbol: str, end_dt: datetime, debug_info: dict[str, Any] | None = None) -> float:
    if ak is None or pd is None:
        return 0.0

    try:
        start = (end_dt - timedelta(days=15)).strftime("%Y%m%d")
        end = end_dt.strftime("%Y%m%d")
        hist = ak.stock_individual_fund_flow(symbol=symbol, start_date=start, end_date=end)
        records = _to_records(hist)
        if debug_info is not None:
            debug_info.setdefault("api_calls", []).append(
                {"api": "stock_individual_fund_flow", "symbol": symbol, "ok": True, "rows": len(records)}
            )
        if records:
            latest = records[-1]
            return _num_unit(latest, ["主力净流入-净额", "主力净额", "主力净流入", "主力净流入净额"], 0.0)
    except Exception:
        if debug_info is not None:
            debug_info.setdefault("api_calls", []).append({"api": "stock_individual_fund_flow", "symbol": symbol, "ok": False})

    if hasattr(ak, "stock_individual_fund_flow_rank"):
        for indicator in ("今日", "5日", "10日"):
            try:
                rank_df = ak.stock_individual_fund_flow_rank(indicator=indicator)
                rows = _to_records(rank_df)
                if debug_info is not None:
                    debug_info.setdefault("api_calls", []).append(
                        {"api": "stock_individual_fund_flow_rank", "indicator": indicator, "ok": True, "rows": len(rows)}
                    )
                for row in rows:
                    code = str(row.get("股票代码", row.get("代码", ""))).strip()
                    if code == symbol:
                        return _num_unit(row, ["今日主力净流入-净额", "主力净流入-净额", "主力净额"], 0.0)
            except Exception:
                if debug_info is not None:
                    debug_info.setdefault("api_calls", []).append(
                        {"api": "stock_individual_fund_flow_rank", "indicator": indicator, "ok": False}
                    )
    return 0.0


def analyze_capital_flow(symbol: str | None = None, analysis_date: str | None = None, debug: bool = False) -> Dict[str, Any]:
    debug = resolve_debug(debug)
    fallback_ok = is_fallback_enabled(default=False)
    analysis_ymd, analysis_dt = _parse_analysis_date(analysis_date)
    dbg: dict[str, Any] = {
        "module": "analyze_capital_flow",
        "analysis_date": analysis_ymd,
        "symbol": symbol or "market",
        "api_calls": [],
        "fallback_enabled": fallback_ok,
    }

    if ak is None or pd is None:
        dbg["fallback_reason"] = "akshare_or_pandas_missing"
        return _fallback_capital_flow(symbol, debug=debug, debug_info=dbg) if fallback_ok else with_debug(
            {
                "symbol": symbol or "market",
                "main_flow": 0,
                "northbound_net": 0,
                "northbound_inflow_days": 0,
                "flow_trend": "unknown",
                "strength_rank": 100,
                "data_source": "unavailable",
                "error": "real_data_required",
            },
            debug,
            dbg,
        )

    north_vals = _load_northbound_series(debug_info=dbg)
    if not north_vals:
        dbg["fallback_reason"] = "northbound_series_empty"
        return _fallback_capital_flow(symbol, debug=debug, debug_info=dbg) if fallback_ok else with_debug(
            {
                "symbol": symbol or "market",
                "main_flow": 0,
                "northbound_net": 0,
                "northbound_inflow_days": 0,
                "flow_trend": "unknown",
                "strength_rank": 100,
                "data_source": "unavailable",
                "error": "real_data_required",
            },
            debug,
            dbg,
        )

    # Series endpoints generally return latest values; for historical date we approximate by taking
    # trailing sequence and selecting value by relative position.
    northbound_net = north_vals[-1]
    if analysis_date and analysis_dt.date() < datetime.now().date() and len(north_vals) >= 2:
        days_back = max(0, (datetime.now().date() - analysis_dt.date()).days)
        idx = max(0, len(north_vals) - 1 - days_back)
        northbound_net = north_vals[idx]

    northbound_inflow_days = _count_consecutive_inflow(north_vals)
    main_flow = _load_symbol_main_flow(symbol, end_dt=analysis_dt, debug_info=dbg) if symbol else northbound_net * 0.55

    if main_flow >= 300_000_000:
        strength_rank = 5
    elif main_flow >= 150_000_000:
        strength_rank = 12
    elif main_flow >= 0:
        strength_rank = 30
    else:
        strength_rank = 70

    trend = f"{northbound_inflow_days}-day-inflow" if northbound_inflow_days > 0 else "outflow"
    payload = {
        "date": analysis_dt.strftime("%Y-%m-%d"),
        "symbol": symbol or "market",
        "main_flow": int(main_flow),
        "northbound_net": int(northbound_net),
        "northbound_inflow_days": northbound_inflow_days,
        "flow_trend": trend,
        "strength_rank": strength_rank,
        "data_source": "akshare-live",
    }
    dbg["derived"] = {"north_series_points": len(north_vals)}
    return with_debug(payload, debug, dbg)


path: tools/reporting.py
content: """Daily report generation for A-share short-term strategy."""

from __future__ import annotations

from typing import Any, Dict, List

from .debug_utils import resolve_debug
from .fusion_engine import short_term_signal_engine


def _fmt_sectors(sectors: List[dict[str, Any]]) -> str:
    if not sectors:
        return "无"
    lines = []
    for idx, item in enumerate(sectors[:3], start=1):
        lines.append(f"{idx}. {item.get('name', 'UNKNOWN')} (strength {item.get('strength', 0)})")
    return "\n".join(lines)


def _fmt_candidates(cands: List[dict[str, Any]]) -> str:
    if not cands:
        return "暂无"
    lines = []
    for item in cands[:3]:
        lines.append(
            f"{item.get('code', '')} {item.get('name', '')} | "
            f"chg {item.get('change_pct', 0)}% | vol {item.get('volume_ratio', 0)}x"
        )
    return "\n".join(lines)


def generate_daily_report(analysis_date: str | None = None, debug: bool = False) -> Dict[str, Any]:
    debug = resolve_debug(debug)
    signal = short_term_signal_engine(analysis_date=analysis_date, debug=debug)
    m = signal["market_sentiment"]
    sectors = signal["top_sectors"]
    cands = signal["candidates"]
    risk = signal["risk_control"]
    friendly = signal.get("no_recommendation_message", "当前暂无推荐标的，建议观望。")

    report = (
        "【A股短线日报】\n\n"
        f"分析日期：{signal.get('analysis_date', m.get('date', ''))}\n"
        f"市场情绪：score={m.get('market_sentiment_score', 0)} "
        f"(涨停{m.get('limit_up', 0)} 跌停{m.get('limit_down', 0)} 炸板率{m.get('break_rate', 0)})\n"
        f"最高连板：{m.get('max_height', 0)}\n\n"
        f"强势板块：\n{_fmt_sectors(sectors)}\n\n"
        f"短线关注：\n{_fmt_candidates(cands)}\n\n"
        f"结论：\n{friendly}\n\n"
        "建议：\n"
        f"轻仓试错(<= {int(risk.get('max_position', 0) * 100)}%)\n"
        f"止损 {risk.get('stop_loss', -6)}%\n"
        f"止盈 {risk.get('take_profit', 12)}%\n\n"
        "风险：\n"
        f"{risk.get('risk_note', '')}"
    )

    return {
        "report": report,
        "signal": signal,
    }


path: tools/risk_control.py
content: """Risk control rules for short-term strategy."""

from __future__ import annotations

from typing import Any, Dict


def short_term_risk_control(market_sentiment_score: float) -> Dict[str, Any]:
    """
    Return strict short-term risk controls.

    Rules:
    - max position <= 15%
    - stop loss -6%
    - if market sentiment < 40, block opening new positions
    """
    market_filter = market_sentiment_score >= 40
    return {
        "max_position": 0.15,
        "stop_loss": -6,
        "take_profit": 12,
        "break_ma5_stop": True,
        "market_filter": market_filter,
        "risk_note": "no new position when sentiment score < 40",
    }


def validate_trade_plan(plan: Dict[str, Any], market_sentiment_score: float) -> Dict[str, Any]:
    """Validate a candidate trade plan against risk control rules."""
    rc = short_term_risk_control(market_sentiment_score)
    position = float(plan.get("position", 0))
    stop_loss = float(plan.get("stop_loss", 0))

    violations: list[str] = []
    if position > rc["max_position"]:
        violations.append("position_over_limit")
    if stop_loss < rc["stop_loss"]:
        violations.append("stop_loss_too_wide")
    if not rc["market_filter"]:
        violations.append("market_sentiment_blocked")

    return {
        "pass": len(violations) == 0,
        "violations": violations,
        "risk_control": rc,
    }


path: tools/settings.py
content: """Configuration loader for strategy thresholds."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any, Dict

DEFAULT_SCREENER_CONFIG: Dict[str, Any] = {
    "prefilter_change_pct": 4.5,
    "min_change_pct": 5.0,
    "min_volume_ratio": 1.5,
    "trend_lookback": 3,
    "min_history_days": 6,
    "volume_baseline_days": 5,
    "high_volume_bearish_drop_pct": -2.0,
    "high_volume_bearish_vol_ratio": 2.2,
    "historical_mode": {
        "min_change_pct": 3.0,
        "trend_lookback": 2,
        "min_volume_ratio": 1.2,
        "high_volume_bearish_drop_pct": -3.0,
        "high_volume_bearish_vol_ratio": 2.8,
        "relaxed_pick_from_pool_when_empty": True,
    },
}


def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
    out = dict(base)
    for k, v in override.items():
        if isinstance(v, dict) and isinstance(out.get(k), dict):
            out[k] = _deep_merge(out[k], v)
        else:
            out[k] = v
    return out


def load_config() -> Dict[str, Any]:
    cfg_path = Path(__file__).resolve().parents[1] / "config.json"
    if not cfg_path.exists():
        return {}
    try:
        return json.loads(cfg_path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def get_screener_config() -> Dict[str, Any]:
    cfg = load_config()
    user_cfg = cfg.get("strategy", {}).get("screener", {})
    if not isinstance(user_cfg, dict):
        user_cfg = {}
    return _deep_merge(DEFAULT_SCREENER_CONFIG, user_cfg)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)