Initial commit: cc-cursor 全链路量化研究平台

7 Sprints 全部完成:
  Sprint 0: 基础设施 (DataManager + MariaDB)
  Sprint 1: 因子引擎 (34因子/12分类)
  Sprint 2: VectorBT 回测 (5策略+截面)
  Sprint 3: Optuna 优化 (+Walk-Forward)
  Sprint 4: ML 模型 (LightGBM+CatBoost)
  Sprint 5: Qwen 情绪因子 (三源新闻+日期对齐)
  Sprint 6: Agent 系统 (4Agent+日报.md/.html)

生产加固 (15项): Tushare双源fallback, SSH自动恢复, pool_pre_ping,
  save_daily先删后插, load_dotenv绝对路径, 日报5d/20d修复,
  RiskAgent改上证指数, 昨日对比+数据截止, mac_report utf8mb4,
  CLAUDE-*.md 9条已知Bug, demo全参数化, djapi数据源归一化,
  indexDatas API修正

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 15:59:05 +08:00
co-authored by Claude Opus 4.7
commit 271a9343a5
293 changed files with 59598 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
"""
Sprint 5 验证脚本 — 情绪因子快速验证。
用法:
python cli/demo_sentiment.py
python cli/demo_sentiment.py --ts_code 600519.SH
python cli/demo_sentiment.py --ts_code 000001.SZ --no-qwen
"""
import sys, os, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pandas as pd
from data.data_manager import DataManager
from factors.sentiment.news_source import NewsSource, align_news_to_trading_days
from factors.sentiment.qwen_client import QwenClient
from factors.sentiment.sentiment_engine import SentimentEngine
def main():
p = argparse.ArgumentParser(description="Sprint 5 — 情绪因子快速验证")
p.add_argument("--ts_code", default="000001.SZ", help="测试股票代码")
p.add_argument("--no-qwen", action="store_true", help="跳过 Qwen 情绪分析")
args = p.parse_args()
print("=" * 60)
print("Sprint 5 — Qwen 情绪因子验证")
print("=" * 60)
dm = DataManager(); dm.init_db()
print("\n[1/5] 新闻数据源测试...")
news_src = NewsSource(use_mcp=False)
news_df = news_src.fetch(args.ts_code, max_news=10)
if not news_df.empty:
print(" 获取 {} 条新闻".format(len(news_df)))
for _, row in news_df.head(3).iterrows():
print(" [{}] {}... (source={})".format(row["date"], str(row["title"])[:80], row["source"]))
else:
print(" (无新闻数据)")
print("[OK]")
print("\n[2/5] 日期对齐测试...")
from data.data_manager import DataManager as DM
price = dm.get_daily(args.ts_code) if dm.get_daily(args.ts_code) is not None else dm.get_daily("000001.SZ")
if price is not None and not price.empty:
price = price.set_index("trade_date").sort_index()
daily_idx = pd.to_datetime(price.index, format="%Y%m%d", errors="coerce")
test_news = pd.DataFrame({"date": ["20240601", "20240602", "20240603"],
"title": ["周六新闻", "周日新闻", "周一新闻"],
"content": [""] * 3, "source": ["test"] * 3, "url": [""] * 3})
aligned = align_news_to_trading_days(test_news, daily_idx)
for _, row in aligned.iterrows():
print(" {}: {}".format(row["title"], row["date"]))
print("[OK]")
print("\n[3/5] Qwen 客户端状态...")
client = QwenClient()
has_api = bool(client.api_key) or bool(client.local_base_url)
if has_api and not args.no_qwen:
mode = "本地Ollama/{}".format(client.local_model) if client.local_base_url else "DashScope/{}".format(client.model)
print(" 模式: {}".format(mode))
else:
print(" 模式: 未配置或跳过")
print("[OK]")
print("\n[4/5] SentimentEngine 全链路 (max_news=10)...")
try:
sent_engine = SentimentEngine(dm, qwen_client=client, news_source=news_src)
sent_df = sent_engine.compute(args.ts_code, max_news=10)
if sent_df is not None and not sent_df.empty:
valid = sent_df.dropna(how="all")
print(" 情绪因子: {}".format(list(sent_df.columns)))
print(" 有效行: {}/{}".format(len(valid), len(sent_df)))
if not valid.empty:
print(valid.tail(5).round(4).to_string())
except Exception as e:
print(" [WARN] {}".format(e))
print("[OK]")
print("\n[5/5] 分析范围解析...")
scope = sent_engine.get_scope_stocks()
print(" 成分股数量: {}".format(len(scope)))
if scope:
print(" 示例: {}".format(", ".join(scope[:5])))
print("[OK]")
try:
from reports.storage import save_report
save_report("## 情绪因子验证 — {}\n\n- 新闻源: OK\n- 日期对齐: OK\n- Qwen: {}".format(
args.ts_code, "就绪" if has_api else "未配置"),
"情绪因子验证", subject_type="stock", subject_code=args.ts_code)
print(" 报告已存入 DB")
except Exception:
pass
print("\n" + "=" * 60)
print("Sprint 5 验证完成")
print("=" * 60)
if __name__ == "__main__":
main()