加载报告中...
- {/* 报告内容 */}
-
+
+
+ {sentiment.map((ind) => (
+
+ {ind.name}
+
+ {ind.value.toFixed(ind.value < 10 ? 2 : 0)}
+
+ {ind.change !== undefined && (
+
+ {formatPercent(ind.change)}
+
+ )}
+ {ind.description}
+
+ ))}
- ) : (
-
-
-
+
+
+
+
+
-
开始分析
-
- 输入股票代码进行分析,或从左侧选择历史报告查看
-
+
+
+ {isLoadingReport ? (
+
+ ) : selectedReport ? (
+
+
+
+ ) : (
+
+
+
Start Analysis
+
+ Enter stock code to analyze, or select from history
+
+
+ )}
+
- )}
-
+
+ )}
);
};
-export default HomePage;
+export default HomePage;
\ No newline at end of file
diff --git a/apps/dsa-web/src/pages/TrendPage.tsx b/apps/dsa-web/src/pages/TrendPage.tsx
new file mode 100644
index 0000000..9fbc511
--- /dev/null
+++ b/apps/dsa-web/src/pages/TrendPage.tsx
@@ -0,0 +1,302 @@
+import type React from 'react';
+import { useState, useEffect, useCallback } from 'react';
+import { Card, Badge } from '../components/common';
+import { marketApi } from '../api/market';
+import type {
+ SectorMomentum,
+ StockMomentum,
+ NewHighLowStock,
+ UpDownStats,
+ KLineChartResponse,
+} from '../types/market';
+import KLineChartModal from '../components/charts/KLineChartModal';
+
+function formatPercent(value: number): string {
+ const sign = value >= 0 ? '+' : '';
+ return `${sign}${value.toFixed(2)}%`;
+}
+
+function formatValue(value: number): string {
+ return value.toFixed(4);
+}
+
+function getChangeColor(value: number): string {
+ if (value > 0) return 'text-emerald-400';
+ if (value < 0) return 'text-red-400';
+ return 'text-secondary';
+}
+
+function getRankChangeColor(change: number): string {
+ if (change > 0) return 'text-emerald-400'; // 排名提升
+ if (change < 0) return 'text-red-400'; // 排名下降
+ return 'text-secondary';
+}
+
+function getRankChangeIcon(change: number): React.ReactNode {
+ if (change > 0) return '↑';
+ if (change < 0) return '↓';
+ return '→';
+}
+
+function recommendationBadge(rec?: string) {
+ if (!rec) return null;
+ switch (rec) {
+ case 'buy':
+ return
BUY;
+ case 'watch':
+ return
WATCH;
+ case 'hold':
+ return
HOLD;
+ default:
+ return
{rec};
+ }
+}
+
+const TrendPage: React.FC = () => {
+ const [sectors, setSectors] = useState
([]);
+ const [stocks, setStocks] = useState([]);
+ const [newHigh, setNewHigh] = useState([]);
+ const [newLow, setNewLow] = useState([]);
+ const [updownStats, setUpdownStats] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const [chartModal, setChartModal] = useState<{
+ open: boolean;
+ type: 'stock' | 'sector';
+ code: string;
+ name: string;
+ } | null>(null);
+
+ const [chartData, setChartData] = useState(null);
+ const [isLoadingChart, setIsLoadingChart] = useState(false);
+
+ const fetchData = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const [sectorsRes, stocksRes, highRes, lowRes, statsRes] = await Promise.all([
+ marketApi.getSectorMomentum('momentumValue', 'desc', 5),
+ marketApi.getStockMomentumRecommendation(15),
+ marketApi.getNewHighStocks(20, 20),
+ marketApi.getNewLowStocks(20, 20),
+ marketApi.getUpDownStats(),
+ ]);
+
+ setSectors(Array.isArray(sectorsRes?.items) ? sectorsRes.items : []);
+ setStocks(Array.isArray(stocksRes?.items) ? stocksRes.items : []);
+ setNewHigh(Array.isArray(highRes) ? highRes : []);
+ setNewLow(Array.isArray(lowRes) ? lowRes : []);
+ setUpdownStats(statsRes || null);
+ } catch (err) {
+ console.error('Failed to fetch trend data:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ const handleItemClick = async (type: 'stock' | 'sector', code: string, name: string) => {
+ setChartModal({ open: true, type, code, name });
+ setIsLoadingChart(true);
+ setChartData(null);
+
+ try {
+ if (type === 'stock') {
+ const data = await marketApi.getStockKline(code, 60);
+ setChartData(data);
+ } else {
+ const data = await marketApi.getSectorKline(code, 60);
+ setChartData(data);
+ }
+ } catch (err) {
+ console.error('Failed to fetch kline data:', err);
+ } finally {
+ setIsLoadingChart(false);
+ }
+ };
+
+ const closeChartModal = () => {
+ setChartModal(null);
+ setChartData(null);
+ };
+
+ return (
+
+
+ 趋势分析
+ 动量板块、个股推荐、新高新低分布
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+ 动量板块 TOP5
+
+
+ {sectors.map((sector, idx) => (
+
handleItemClick('sector', sector.code, sector.name)}
+ >
+
+ {sector.rank || idx + 1}
+ {sector.name}
+ {sector.rank_change !== undefined && (
+
+ {getRankChangeIcon(sector.rank_change)} {Math.abs(sector.rank_change)}
+
+ )}
+
+
+ {formatValue(sector.momentum_value || 0)}
+ {sector.momentum_value_change !== undefined && (
+
+ {formatValue(sector.momentum_value_change)}
+
+ )}
+
+ {formatPercent(sector.change_percent)}
+
+
+
+ ))}
+
+
+
+
+
+ 动量个股推荐
+
+
+ {stocks.map((stock, idx) => (
+
handleItemClick('stock', stock.code, stock.name)}
+ >
+
+
{idx + 1}
+
+ {stock.name}
+ {stock.code}
+
+
+
+ {recommendationBadge(stock.recommendation)}
+
+ {formatPercent(stock.change_percent)}
+
+
+
+ ))}
+
+
+
+
+
+ 新高个股
+ (20日内)
+
+
+ {newHigh.slice(0, 10).map((stock) => (
+
handleItemClick('stock', stock.code, stock.name)}
+ >
+
+ {stock.name}
+ {stock.code}
+
+
+ {stock.price.toFixed(2)}
+ {formatPercent(stock.change_percent)}
+
+
+ ))}
+
+
+
+
+
+ 新低个股
+ (20日内)
+
+
+ {newLow.slice(0, 10).map((stock) => (
+
handleItemClick('stock', stock.code, stock.name)}
+ >
+
+ {stock.name}
+ {stock.code}
+
+
+ {stock.price.toFixed(2)}
+ {formatPercent(stock.change_percent)}
+
+
+ ))}
+
+
+
+ {updownStats && (
+
+
+ 涨跌分布
+
+
+
+
{updownStats.up_count}
+
上涨
+
+
+
{updownStats.down_count}
+
下跌
+
+
+
{updownStats.flat_count}
+
平盘
+
+
+
{updownStats.limit_up_count}
+
涨停
+
+
+
{updownStats.limit_down_count}
+
跌停
+
+
+
+ )}
+
+ )}
+
+
+ {chartModal?.open && (
+
+ )}
+
+ );
+};
+
+export default TrendPage;
\ No newline at end of file
diff --git a/apps/dsa-web/src/types/market.ts b/apps/dsa-web/src/types/market.ts
new file mode 100644
index 0000000..3cb545a
--- /dev/null
+++ b/apps/dsa-web/src/types/market.ts
@@ -0,0 +1,73 @@
+export interface MarketIndex {
+ code: string;
+ name: string;
+ price: number;
+ change?: number;
+ change_percent?: number;
+ volume?: number;
+ amount?: number;
+}
+
+export interface UpDownStats {
+ up_count: number;
+ down_count: number;
+ flat_count: number;
+ limit_up_count: number;
+ limit_down_count: number;
+ total_count: number;
+}
+
+export interface SectorMomentum {
+ code: string;
+ name: string;
+ momentum_score: number;
+ change_percent: number;
+ turnover_rate?: number;
+ leading_stock?: string;
+ leading_stock_name?: string;
+ momentum_value?: number;
+ momentum_value_change?: number;
+ rank?: number;
+ rank_change?: number;
+}
+
+export interface StockMomentum {
+ code: string;
+ name: string;
+ momentum_score: number;
+ change_percent: number;
+ sector?: string;
+ recommendation?: string;
+}
+
+export interface NewHighLowStock {
+ code: string;
+ name: string;
+ price: number;
+ change_percent: number;
+ days_to_high?: number;
+ days_to_low?: number;
+ high_date?: string;
+ low_date?: string;
+}
+
+export interface PriceDistribution {
+ range_label: string;
+ count: number;
+ percent: number;
+}
+
+export interface SentimentIndicator {
+ name: string;
+ value: number;
+ change?: number;
+ level?: string;
+ description?: string;
+}
+
+export interface KLineChartResponse {
+ categoryData: string[];
+ values: number[][];
+ volumes: number[][];
+ stock_name?: string;
+}
\ No newline at end of file
diff --git a/data_provider/amazingdata_fetcher.py b/data_provider/amazingdata_fetcher.py
new file mode 100644
index 0000000..6948e5e
--- /dev/null
+++ b/data_provider/amazingdata_fetcher.py
@@ -0,0 +1,443 @@
+# -*- coding: utf-8 -*-
+"""
+===================================
+AmazingData Custom Data Source Adapter
+===================================
+
+Adapter for AmazingData financial data platform API.
+Provides stock K-line data, realtime quotes, and market statistics.
+
+API Documentation: docs/customApi/API.md
+"""
+
+import logging
+import time
+import requests
+from datetime import datetime, timedelta
+from typing import Optional, List, Dict, Any, Tuple
+
+import pandas as pd
+
+from .base import BaseFetcher, DataFetchError, RateLimitError
+
+logger = logging.getLogger(__name__)
+
+
+class AmazingDataFetcher(BaseFetcher):
+ """
+ AmazingData data source adapter
+
+ Features:
+ - JWT authentication with auto-refresh
+ - Stock K-line data (daily/minute)
+ - Realtime quotes
+ - Market statistics
+ - Sector rankings
+
+ Configuration (via .env):
+ - AMAZINGDATA_BASE_URL: API base URL (default: http://localhost:8000/api/v1)
+ - AMAZINGDATA_USERNAME: SDK username
+ - AMAZINGDATA_PASSWORD: SDK password
+ - AMAZINGDATA_HOST: SDK server host (default: 140.206.44.234)
+ - AMAZINGDATA_PORT: SDK server port (default: 8600)
+ """
+
+ name: str = "AmazingDataFetcher"
+ priority: int = 5
+
+ DEFAULT_BASE_URL = "http://localhost:8000/api/v1"
+ DEFAULT_HOST = "140.206.44.234"
+ DEFAULT_PORT = 8600
+ TOKEN_EXPIRY_BUFFER = 300
+
+ def __init__(self):
+ """Initialize AmazingData fetcher"""
+ self._base_url: Optional[str] = None
+ self._username: Optional[str] = None
+ self._password: Optional[str] = None
+ self._host: Optional[str] = None
+ self._port: Optional[int] = None
+
+ self._token: Optional[str] = None
+ self._token_expires_at: Optional[float] = None
+
+ self._initialized = False
+ self._available = False
+
+ self._init_config()
+
+ def _init_config(self) -> None:
+ """Load configuration from environment"""
+ try:
+ from src.config import get_config
+ config = get_config()
+
+ self._base_url = getattr(config, 'amazingdata_base_url', None) or self.DEFAULT_BASE_URL
+ self._username = getattr(config, 'amazingdata_username', None)
+ self._password = getattr(config, 'amazingdata_password', None)
+ self._host = getattr(config, 'amazingdata_host', None) or self.DEFAULT_HOST
+ self._port = getattr(config, 'amazingdata_port', None) or self.DEFAULT_PORT
+
+ if self._username and self._password:
+ self._initialized = True
+ self._available = True
+ self.priority = 0
+ logger.info(f"[{self.name}] Initialized with base_url={self._base_url}")
+ else:
+ logger.debug(f"[{self.name}] Not configured (missing username/password)")
+ self._available = False
+
+ except Exception as e:
+ logger.warning(f"[{self.name}] Config load failed: {e}")
+ self._available = False
+
+ def _login(self) -> bool:
+ """Authenticate and obtain JWT token"""
+ if not self._initialized:
+ return False
+
+ try:
+ response = requests.post(
+ f"{self._base_url}/auth/login",
+ json={"username": self._username, "password": self._password},
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ data = response.json()
+ if data.get("code") == 200:
+ token_data = data.get("data", {})
+ self._token = token_data.get("access_token")
+ expires_in = token_data.get("expires_in", 86400)
+ self._token_expires_at = time.time() + expires_in - self.TOKEN_EXPIRY_BUFFER
+ logger.info(f"[{self.name}] Login successful, token valid for {expires_in}s")
+ return True
+ else:
+ logger.warning(f"[{self.name}] Login failed: {data.get('message')}")
+ return False
+ else:
+ logger.warning(f"[{self.name}] Login HTTP error: {response.status_code}")
+ return False
+
+ except Exception as e:
+ logger.error(f"[{self.name}] Login exception: {e}")
+ return False
+
+ def _get_headers(self) -> Dict[str, str]:
+ """Get authorization headers"""
+ if not self._token or (self._token_expires_at and time.time() > self._token_expires_at):
+ if not self._login():
+ raise DataFetchError(f"[{self.name}] Authentication failed")
+
+ return {
+ "Authorization": f"Bearer {self._token}",
+ "Content-Type": "application/json"
+ }
+
+ def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]:
+ """Make authenticated API request"""
+ headers = self._get_headers()
+ kwargs["headers"] = headers
+ kwargs["timeout"] = kwargs.get("timeout", 60)
+
+ url = f"{self._base_url}{path}"
+
+ try:
+ response = requests.request(method, url, **kwargs)
+
+ if response.status_code == 401:
+ logger.warning(f"[{self.name}] Token expired, re-authenticating...")
+ self._token = None
+ headers = self._get_headers()
+ kwargs["headers"] = headers
+ response = requests.request(method, url, **kwargs)
+
+ if response.status_code == 200:
+ return response.json()
+ else:
+ raise DataFetchError(f"[{self.name}] API error {response.status_code}: {response.text}")
+
+ except requests.Timeout:
+ raise DataFetchError(f"[{self.name}] Request timeout")
+ except requests.RequestException as e:
+ raise DataFetchError(f"[{self.name}] Request failed: {e}")
+
+ def _fetch_raw_data(self, stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
+ """
+ Fetch raw K-line data from AmazingData
+
+ Args:
+ stock_code: Stock code (e.g., '600519')
+ start_date: Start date (YYYY-MM-DD)
+ end_date: End date (YYYY-MM-DD)
+
+ Returns:
+ Raw DataFrame with K-line data
+ """
+ if not self._available:
+ raise DataFetchError(f"[{self.name}] Not configured")
+
+ code_with_suffix = self._add_exchange_suffix(stock_code)
+ start_dt = start_date.replace("-", "")
+ end_dt = end_date.replace("-", "")
+
+ try:
+ response = self._request("GET", "/stock/kline", params={
+ "codes": code_with_suffix,
+ "start_date": start_dt,
+ "end_date": end_dt,
+ "period": "daily"
+ })
+
+ data = response.get("data", {})
+ kline_list = data.get(code_with_suffix, [])
+
+ if not kline_list:
+ raise DataFetchError(f"[{self.name}] No data for {stock_code}")
+
+ df = pd.DataFrame(kline_list)
+ return df
+
+ except DataFetchError:
+ raise
+ except Exception as e:
+ raise DataFetchError(f"[{self.name}] Fetch failed: {e}")
+
+ def _normalize_data(self, df: pd.DataFrame, stock_code: str) -> pd.DataFrame:
+ """
+ Normalize column names to standard format
+
+ Standard columns: date, open, high, low, close, volume, amount, pct_chg
+ """
+ column_mapping = {
+ "trade_date": "date",
+ "open": "open",
+ "high": "high",
+ "low": "low",
+ "close": "close",
+ "volume": "volume",
+ "amount": "amount",
+ }
+
+ df = df.copy()
+
+ for old_col, new_col in column_mapping.items():
+ if old_col in df.columns and new_col not in df.columns:
+ df[new_col] = df[old_col]
+
+ if "pct_chg" not in df.columns:
+ if "close" in df.columns and len(df) > 1:
+ df["pct_chg"] = df["close"].pct_change() * 100
+ else:
+ df["pct_chg"] = 0.0
+
+ required_cols = ["date", "open", "high", "low", "close", "volume"]
+ missing_cols = [col for col in required_cols if col not in df.columns]
+ if missing_cols:
+ raise DataFetchError(f"[{self.name}] Missing columns: {missing_cols}")
+
+ return df
+
+ def _add_exchange_suffix(self, stock_code: str) -> str:
+ """Add exchange suffix to stock code"""
+ code = stock_code.strip()
+ if "." in code:
+ return code
+
+ if code.startswith("6"):
+ return f"{code}.SH"
+ elif code.startswith(("0", "3")):
+ return f"{code}.SZ"
+ elif code.startswith("68"):
+ return f"{code}.SH"
+ else:
+ return f"{code}.SH"
+
+ def get_realtime_quote(self, stock_code: str):
+ """
+ Get realtime quote for a stock
+
+ Args:
+ stock_code: Stock code
+
+ Returns:
+ UnifiedRealtimeQuote object or None
+ """
+ if not self._available:
+ return None
+
+ try:
+ code_with_suffix = self._add_exchange_suffix(stock_code)
+
+ today = datetime.now().strftime("%Y%m%d")
+ week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y%m%d")
+
+ response = self._request("GET", "/stock/kline", params={
+ "codes": code_with_suffix,
+ "start_date": week_ago,
+ "end_date": today,
+ "period": "daily"
+ })
+
+ data = response.get("data", {})
+ kline_list = data.get(code_with_suffix, [])
+
+ if not kline_list:
+ return None
+
+ latest = kline_list[-1]
+ prev = kline_list[-2] if len(kline_list) > 1 else latest
+
+ from .realtime_types import UnifiedRealtimeQuote
+
+ close = float(latest.get("close", 0))
+ prev_close = float(prev.get("close", close))
+ change = close - prev_close
+ change_pct = (change / prev_close * 100) if prev_close else 0
+
+ quote = UnifiedRealtimeQuote(
+ code=stock_code,
+ name=None,
+ price=close,
+ open=float(latest.get("open", 0)),
+ high=float(latest.get("high", 0)),
+ low=float(latest.get("low", 0)),
+ prev_close=prev_close,
+ volume=float(latest.get("volume", 0) or 0),
+ amount=float(latest.get("amount", 0) or 0),
+ change_amount=change,
+ change_pct=change_pct,
+ )
+
+ return quote
+
+ except Exception as e:
+ logger.warning(f"[{self.name}] Realtime quote failed for {stock_code}: {e}")
+ return None
+
+ def get_main_indices(self) -> Optional[List[Dict[str, Any]]]:
+ """Get major market indices realtime data"""
+ if not self._available:
+ return None
+
+ indices_codes = ["000001.SH", "399001.SZ", "399006.SZ", "000698.SH"]
+
+ try:
+ today = datetime.now().strftime("%Y%m%d")
+ week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y%m%d")
+
+ response = self._request("GET", "/stock/kline", params={
+ "codes": ",".join(indices_codes),
+ "start_date": week_ago,
+ "end_date": today,
+ "period": "daily"
+ })
+
+ data = response.get("data", {})
+ result = []
+
+ index_names = {
+ "000001.SH": "上证指数",
+ "399001.SZ": "深证成指",
+ "399006.SZ": "创业板指",
+ "000698.SH": "科创50",
+ }
+
+ for code in indices_codes:
+ kline_list = data.get(code, [])
+ if kline_list:
+ latest = kline_list[-1]
+ prev = kline_list[-2] if len(kline_list) > 1 else latest
+
+ close = float(latest.get("close", 0))
+ prev_close = float(prev.get("close", close))
+ change = close - prev_close
+ change_pct = (change / prev_close * 100) if prev_close else 0
+
+ result.append({
+ "code": code,
+ "name": index_names.get(code, code),
+ "current": close,
+ "change": change,
+ "change_pct": change_pct,
+ "volume": float(latest.get("volume", 0) or 0),
+ "amount": float(latest.get("amount", 0) or 0),
+ })
+
+ return result if result else None
+
+ except Exception as e:
+ logger.warning(f"[{self.name}] Get indices failed: {e}")
+ return None
+
+ def get_market_stats(self) -> Optional[Dict[str, Any]]:
+ """Get market up/down statistics"""
+ return None
+
+ def get_sector_rankings(self, n: int = 5) -> Optional[Tuple[List[Dict], List[Dict]]]:
+ """Get sector rankings"""
+ return None
+
+ def get_stock_name(self, stock_code: str) -> Optional[str]:
+ """Get stock name from API"""
+ if not self._available:
+ return None
+
+ try:
+ code_with_suffix = self._add_exchange_suffix(stock_code)
+
+ response = self._request("GET", f"/base/codes/{code_with_suffix}/info")
+
+ data = response.get("data", {})
+ name = data.get("name")
+
+ if name:
+ logger.debug(f"[{self.name}] Got name for {stock_code}: {name}")
+ return name
+
+ return None
+
+ except Exception as e:
+ logger.debug(f"[{self.name}] Get stock name failed: {e}")
+ return None
+
+ def get_kline_chart_data(self, stock_code: str, days: int = 60) -> Optional[Dict[str, Any]]:
+ """
+ Get K-line chart data formatted for ECharts
+
+ Args:
+ stock_code: Stock code
+ days: Number of days
+
+ Returns:
+ Dict with categoryData, values, volumes for ECharts
+ """
+ if not self._available:
+ return None
+
+ try:
+ code_with_suffix = self._add_exchange_suffix(stock_code)
+ end_date = datetime.now().strftime("%Y%m%d")
+ start_date = (datetime.now() - timedelta(days=days * 2)).strftime("%Y%m%d")
+
+ response = self._request("GET", f"/stock/kline/{code_with_suffix}/chart", params={
+ "start_date": start_date,
+ "end_date": end_date,
+ "period": "daily"
+ })
+
+ data = response.get("data", {})
+
+ if data:
+ return {
+ "categoryData": data.get("categoryData", []),
+ "values": data.get("values", []),
+ "volumes": data.get("volumes", []),
+ "stock_name": data.get("stock_name"),
+ }
+
+ return None
+
+ except Exception as e:
+ logger.warning(f"[{self.name}] Get kline chart failed: {e}")
+ return None
\ No newline at end of file
diff --git a/data_provider/base.py b/data_provider/base.py
index 905fad8..6b3f7f0 100644
--- a/data_provider/base.py
+++ b/data_provider/base.py
@@ -355,6 +355,7 @@ class DataFetcherManager:
from .pytdx_fetcher import PytdxFetcher
from .baostock_fetcher import BaostockFetcher
from .yfinance_fetcher import YfinanceFetcher
+ from .amazingdata_fetcher import AmazingDataFetcher
from src.config import get_config
config = get_config()
@@ -366,9 +367,11 @@ class DataFetcherManager:
pytdx = PytdxFetcher() # 通达信数据源
baostock = BaostockFetcher()
yfinance = YfinanceFetcher()
+ amazingdata = AmazingDataFetcher() # 自定义数据源
# 初始化数据源列表
self._fetchers = [
+ amazingdata, # 自定义数据源优先级最高(如果配置了)
efinance,
akshare,
tushare,
diff --git a/docs/cache_update_strategy.md b/docs/cache_update_strategy.md
new file mode 100644
index 0000000..a937e74
--- /dev/null
+++ b/docs/cache_update_strategy.md
@@ -0,0 +1,370 @@
+# 缓存更新策略
+
+## 一、概述
+
+缓存更新策略是保证缓存数据与数据源数据一致性的关键。本策略定义了股票分析系统中各类数据的缓存更新机制,包括实时行情、K线数据、板块动量等数据的更新频率、方式和流程。
+
+## 二、数据分类
+
+根据数据的特性和更新频率,将数据分为以下几类:
+
+| 数据类型 | 更新频率 | 数据源 | 缓存策略 |
+|---------|---------|---------|---------|
+| 实时行情 | 高频(30秒/次) | 东方财富/新浪/腾讯 | 主动更新 + 惰性更新 |
+| K线数据 | 低频(每日/次) | 东方财富/新浪/腾讯 | 定时更新 + 按需更新 |
+| 板块动量 | 中频(1小时/次) | 计算生成 | 定时更新 |
+| 市场统计 | 中频(1小时/次) | 计算生成 | 定时更新 |
+| 股票基本信息 | 低频(每周/次) | 数据源 | 定时更新 |
+| 板块信息 | 低频(每周/次) | 数据源 | 定时更新 |
+
+## 三、缓存更新策略
+
+### 1. 实时行情更新策略
+
+#### 更新频率
+- **全市场数据**:每30秒更新一次
+- **单只股票**:按需更新(缓存未命中时)
+
+#### 更新流程
+1. **定时任务**:每30秒执行一次
+2. **数据获取**:调用数据源API获取全市场实时行情
+3. **数据处理**:
+ - 解析API返回数据
+ - 标准化数据格式
+ - 计算涨跌幅、换手率等指标
+4. **缓存更新**:
+ - 更新全市场缓存:`realtime:full`
+ - 批量更新单只股票缓存:`realtime:stock:{code}`
+ - 更新ETF缓存:`realtime:etf:{code}`
+ - 记录更新批次:`realtime:batch`
+5. **数据库更新**:异步批量写入数据库
+
+#### 缓存失效处理
+- **时间过期**:设置30分钟过期时间
+- **主动失效**:数据更新时覆盖旧数据
+- **降级策略**:缓存过期后,请求时触发更新
+
+### 2. K线数据更新策略
+
+#### 更新频率
+- **历史数据**:每日收盘后更新
+- **最近数据**:按需更新(缓存未命中时)
+
+#### 更新流程
+1. **定时任务**:每日收盘后执行
+2. **数据获取**:
+ - 调用数据源API获取当日K线数据
+ - 对于新上市股票,获取完整历史数据
+3. **数据处理**:
+ - 标准化数据格式
+ - 计算技术指标
+4. **缓存更新**:
+ - 更新K线缓存:`kline:{code}:{period}:{days}`
+ - 更新压缩K线缓存:`kline:compressed:{code}:{period}:{days}`
+5. **数据库更新**:批量写入数据库
+
+#### 缓存失效处理
+- **时间过期**:设置1天过期时间
+- **主动失效**:每日更新时覆盖旧数据
+- **按需更新**:缓存未命中时触发更新
+
+### 3. 板块动量更新策略
+
+#### 更新频率
+- **常规更新**:每小时更新一次
+- **特殊情况**:市场剧烈波动时增加更新频率
+
+#### 更新流程
+1. **定时任务**:每小时执行一次
+2. **数据获取**:
+ - 从缓存或数据库获取实时行情
+ - 获取板块成分股信息
+3. **计算过程**:
+ - 按涨跌幅排序,取前16%作为动量个股
+ - 按板块分组,计算每个板块的动量值
+ - 计算动量值变化、排名变化
+4. **缓存更新**:
+ - 更新板块动量缓存:`sector:momentum:{period}:{sort}:{order}:{limit}`
+ - 更新板块成分股缓存:`sector:stocks:{sector_code}`
+5. **数据库更新**:写入板块动量表
+
+#### 缓存失效处理
+- **时间过期**:设置1小时过期时间
+- **主动失效**:更新时覆盖旧数据
+- **降级策略**:缓存过期后,请求时触发更新
+
+### 4. 市场统计更新策略
+
+#### 更新频率
+- **常规更新**:每小时更新一次
+- **特殊情况**:市场开盘和收盘时增加更新频率
+
+#### 更新流程
+1. **定时任务**:每小时执行一次
+2. **数据获取**:
+ - 从缓存或数据库获取实时行情
+ - 统计市场涨跌分布
+3. **计算过程**:
+ - 统计上涨、下跌、平盘家数
+ - 统计涨停、跌停家数
+ - 计算两市成交额
+4. **缓存更新**:
+ - 更新市场统计缓存:`market:stats`
+ - 更新涨跌分布缓存:`market:distribution`
+5. **数据库更新**:写入市场统计表
+
+#### 缓存失效处理
+- **时间过期**:设置1小时过期时间
+- **主动失效**:更新时覆盖旧数据
+- **降级策略**:缓存过期后,请求时触发更新
+
+### 5. 股票基本信息更新策略
+
+#### 更新频率
+- **常规更新**:每周更新一次
+- **特殊情况**:新股上市、股票退市时即时更新
+
+#### 更新流程
+1. **定时任务**:每周执行一次
+2. **数据获取**:
+ - 调用数据源API获取股票基本信息
+ - 处理新股上市、股票退市等情况
+3. **数据处理**:
+ - 标准化数据格式
+ - 关联板块信息
+4. **缓存更新**:
+ - 更新股票基本信息缓存
+ - 更新板块成分股缓存
+5. **数据库更新**:批量写入数据库
+
+#### 缓存失效处理
+- **时间过期**:设置7天过期时间
+- **主动失效**:更新时覆盖旧数据
+- **按需更新**:缓存未命中时触发更新
+
+### 6. 板块信息更新策略
+
+#### 更新频率
+- **常规更新**:每周更新一次
+- **特殊情况**:板块调整时即时更新
+
+#### 更新流程
+1. **定时任务**:每周执行一次
+2. **数据获取**:
+ - 调用数据源API获取板块信息
+ - 处理板块调整情况
+3. **数据处理**:
+ - 标准化数据格式
+ - 计算板块成分股数量
+4. **缓存更新**:
+ - 更新板块基本信息缓存:`sector:info:{sector_code}`
+ - 更新板块成分股缓存:`sector:stocks:{sector_code}`
+5. **数据库更新**:批量写入数据库
+
+#### 缓存失效处理
+- **时间过期**:设置7天过期时间
+- **主动失效**:更新时覆盖旧数据
+- **按需更新**:缓存未命中时触发更新
+
+## 四、缓存预热策略
+
+### 1. 服务启动预热
+
+- **预热数据**:
+ - 热门股票实时行情
+ - 主要指数数据
+ - 最近的板块动量数据
+ - 市场统计数据
+- **预热流程**:
+ 1. 服务启动时触发
+ 2. 并行获取各类数据
+ 3. 写入Redis缓存
+ 4. 记录预热完成时间
+
+### 2. 定时预热
+
+- **预热频率**:每小时执行一次
+- **预热数据**:
+ - 即将到期的缓存
+ - 预测可能会被访问的数据
+ - 计算密集型数据
+- **预热流程**:
+ 1. 定时任务触发
+ 2. 分析缓存使用情况
+ 3. 预测热门数据
+ 4. 提前计算并缓存
+
+### 3. 智能预热
+
+- **基于访问模式**:
+ - 分析历史访问记录
+ - 预测未来访问模式
+ - 提前缓存可能被访问的数据
+- **基于市场事件**:
+ - 市场开盘前预热
+ - 重大新闻发布后预热
+ - 财报季预热相关股票数据
+
+## 五、缓存一致性保障
+
+### 1. 数据一致性策略
+
+- **最终一致性**:
+ - 优先保证系统可用性
+ - 允许短暂的数据不一致
+ - 通过定期同步保证最终一致
+- **强一致性**:
+ - 对关键数据采用强一致性
+ - 使用分布式锁确保数据一致性
+ - 双写模式:同时更新数据库和缓存
+
+### 2. 一致性实现
+
+- **先更新数据库,后更新缓存**:
+ 1. 开始事务
+ 2. 更新数据库
+ 3. 提交事务
+ 4. 更新缓存
+ 5. 释放锁
+- **缓存标记失效**:
+ 1. 更新数据库
+ 2. 标记缓存失效
+ 3. 下次读取时更新缓存
+- **定期全量同步**:
+ - 每日执行全量同步
+ - 验证缓存与数据库一致性
+ - 修复不一致数据
+
+### 3. 并发控制
+
+- **分布式锁**:
+ - 使用Redis实现分布式锁
+ - 避免并发更新导致的数据不一致
+- **乐观锁**:
+ - 使用版本号或时间戳
+ - 检测并发冲突
+- **队列处理**:
+ - 将更新操作放入队列
+ - 串行处理确保顺序
+
+## 六、缓存监控与告警
+
+### 1. 监控指标
+
+| 指标 | 描述 | 阈值 | 告警级别 |
+|------|------|------|---------|
+| 缓存命中率 | 缓存命中次数/总请求次数 | <80% | 警告 |
+| 缓存更新延迟 | 数据更新到缓存的延迟 | >5秒 | 警告 |
+| 缓存过期率 | 过期缓存占比 | >50% | 警告 |
+| 缓存写入失败率 | 缓存写入失败次数/总写入次数 | >1% | 严重 |
+| 缓存读取失败率 | 缓存读取失败次数/总读取次数 | >1% | 严重 |
+
+### 2. 告警策略
+
+- **缓存命中率低**:
+ - 检查缓存策略是否合理
+ - 增加缓存预热
+ - 调整缓存过期时间
+- **缓存更新延迟高**:
+ - 检查数据源API响应时间
+ - 优化数据处理逻辑
+ - 增加更新线程数
+- **缓存写入失败**:
+ - 检查Redis连接状态
+ - 检查Redis内存使用情况
+ - 重启Redis服务
+- **缓存读取失败**:
+ - 检查Redis连接状态
+ - 检查网络连接
+ - 降级到数据库读取
+
+### 3. 监控工具
+
+- **Prometheus**:收集缓存相关指标
+- **Grafana**:可视化缓存监控面板
+- **Redis Exporter**:导出Redis指标
+- **Alertmanager**:处理告警
+
+## 七、异常处理
+
+### 1. 数据源异常
+
+- **处理策略**:
+ - 切换到备用数据源
+ - 使用缓存数据作为降级方案
+ - 记录异常并告警
+- **恢复策略**:
+ - 定期检查数据源状态
+ - 自动恢复到主数据源
+ - 验证数据一致性
+
+### 2. Redis异常
+
+- **处理策略**:
+ - 降级到数据库读取
+ - 记录异常并告警
+ - 尝试重新连接
+- **恢复策略**:
+ - 监控Redis状态
+ - 自动恢复缓存
+ - 验证数据一致性
+
+### 3. 数据库异常
+
+- **处理策略**:
+ - 继续使用缓存数据
+ - 记录异常并告警
+ - 尝试重新连接
+- **恢复策略**:
+ - 监控数据库状态
+ - 自动恢复数据同步
+ - 验证数据一致性
+
+## 八、性能优化
+
+### 1. 批量操作
+
+- **批量读取**:
+ - 使用MGET、HMGET等批量命令
+ - 减少网络往返次数
+- **批量写入**:
+ - 使用Pipeline批量执行命令
+ - 减少网络开销
+
+### 2. 数据压缩
+
+- **K线数据**:
+ - 使用MsgPack或Protocol Buffers压缩
+ - 减少存储空间和网络传输
+- **全量实时数据**:
+ - 使用GZIP压缩
+ - 减少内存使用
+
+### 3. 并发处理
+
+- **多线程更新**:
+ - 使用线程池并行处理数据
+ - 提高更新效率
+- **异步更新**:
+ - 使用消息队列异步处理
+ - 避免阻塞主线程
+
+### 4. 缓存分层
+
+- **热点数据**:
+ - 存储在Redis中
+ - 高频访问
+- **冷数据**:
+ - 存储在数据库中
+ - 低频访问
+- **预热数据**:
+ - 提前计算并缓存
+ - 减少请求时计算开销
+
+## 九、总结
+
+缓存更新策略是保证股票分析系统性能和数据一致性的关键。通过合理的更新频率、流程和监控,可以确保缓存数据的及时性和准确性,同时提高系统的响应速度和可靠性。
+
+本策略采用了多种技术手段,包括定时更新、按需更新、缓存预热、一致性保障等,以应对不同类型数据的更新需求。同时,通过监控和异常处理,确保系统在面对各种异常情况时能够稳定运行。
+
+随着系统的发展和数据量的增长,缓存更新策略也需要不断优化和调整,以适应新的业务需求和技术挑战。
\ No newline at end of file
diff --git a/docs/customApi/API.md b/docs/customApi/API.md
new file mode 100644
index 0000000..398b949
--- /dev/null
+++ b/docs/customApi/API.md
@@ -0,0 +1,913 @@
+# AmazingData 金融数据服务平台 - API 开发文档
+
+## 一、API 概述
+
+### 1.1 基本信息
+
+- **Base URL**: `http://localhost:8000/api/v1`
+- **认证方式**: JWT Bearer Token
+- **数据格式**: JSON
+- **编码**: UTF-8
+
+### 1.2 认证说明
+
+除登录接口外,所有接口都需要在请求头中携带 JWT Token:
+
+```
+Authorization: Bearer {access_token}
+```
+
+---
+
+## 二、认证接口
+
+### 2.1 用户登录
+
+**接口**: `POST /auth/login`
+
+**功能**: 用户登录获取 JWT Token
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| username | string | 是 | 用户名 |
+| password | string | 是 | 密码 |
+
+**请求示例**:
+
+```json
+{
+ "username": "admin",
+ "password": "admin123"
+}
+```
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| code | int | 状态码 (200成功) |
+| message | string | 消息 |
+| data.access_token | string | JWT Token |
+| data.token_type | string | Token类型 (bearer) |
+| data.expires_in | int | 有效期(秒) |
+
+**响应示例**:
+
+```json
+{
+ "code": 200,
+ "message": "登录成功",
+ "data": {
+ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "token_type": "bearer",
+ "expires_in": 86400
+ }
+}
+```
+
+**调用示例**:
+
+```python
+import requests
+
+response = requests.post(
+ 'http://localhost:8000/api/v1/auth/login',
+ json={'username': 'admin', 'password': 'admin123'}
+)
+token = response.json()['data']['access_token']
+```
+
+---
+
+### 2.2 获取当前用户信息
+
+**接口**: `GET /auth/me`
+
+**功能**: 获取当前登录用户信息
+
+**请求参数**: 无
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.id | int | 用户ID |
+| data.username | string | 用户名 |
+| data.email | string | 邮箱 |
+| data.role | string | 角色 |
+| data.is_active | bool | 是否激活 |
+
+**调用示例**:
+
+```python
+headers = {'Authorization': f'Bearer {token}'}
+response = requests.get('http://localhost:8000/api/v1/auth/me', headers=headers)
+user_info = response.json()['data']
+```
+
+---
+
+## 三、SDK配置接口
+
+### 3.1 获取SDK配置列表
+
+**接口**: `GET /configs/sdk`
+
+**功能**: 获取所有SDK配置列表
+
+**请求参数**: 无
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.items | array | 配置列表 |
+| data.total | int | 总数 |
+
+**调用示例**:
+
+```python
+response = requests.get('http://localhost:8000/api/v1/configs/sdk', headers=headers)
+configs = response.json()['data']['items']
+```
+
+---
+
+### 3.2 创建SDK配置
+
+**接口**: `POST /configs/sdk`
+
+**功能**: 创建新的SDK配置
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| name | string | 是 | 配置名称 |
+| username | string | 是 | SDK用户名 |
+| password | string | 是 | SDK密码 |
+| host | string | 是 | 服务器地址 |
+| port | int | 否 | 端口号 (默认8600) |
+| local_path | string | 否 | 本地路径 |
+| is_default | bool | 否 | 是否默认配置 |
+
+**请求示例**:
+
+```json
+{
+ "name": "生产环境SDK",
+ "username": "your_username",
+ "password": "your_password",
+ "host": "140.206.44.234",
+ "port": 8600,
+ "is_default": true
+}
+```
+
+**调用示例**:
+
+```python
+response = requests.post(
+ 'http://localhost:8000/api/v1/configs/sdk',
+ headers=headers,
+ json={
+ 'name': '生产环境SDK',
+ 'username': 'your_username',
+ 'password': 'your_password',
+ 'host': '140.206.44.234',
+ 'port': 8600,
+ 'is_default': True
+ }
+)
+```
+
+---
+
+### 3.3 测试SDK连接
+
+**接口**: `POST /configs/sdk/{id}/test`
+
+**功能**: 测试指定SDK配置的连接
+
+**请求参数**: 无
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.success | bool | 是否成功 |
+| data.message | string | 消息 |
+| data.response_time | float | 响应时间(秒) |
+
+**调用示例**:
+
+```python
+response = requests.post(
+ 'http://localhost:8000/api/v1/configs/sdk/1/test',
+ headers=headers,
+ timeout=60
+)
+result = response.json()['data']
+```
+
+---
+
+## 四、基础数据接口
+
+### 4.1 获取代码列表
+
+**接口**: `GET /base/codes`
+
+**功能**: 获取指定类型的证券代码列表
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| security_type | string | 是 | 证券类型 |
+
+**证券类型**:
+- `EXTRA_STOCK_A`: 沪深A股
+- `EXTRA_FUTURE`: 期货
+- `EXTRA_ETF`: ETF
+- `EXTRA_INDEX_A`: 指数
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data | array | 代码列表 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/base/codes',
+ params={'security_type': 'EXTRA_STOCK_A'},
+ headers=headers
+)
+codes = response.json()['data']
+```
+
+---
+
+### 4.2 获取交易日历
+
+**接口**: `GET /base/calendar`
+
+**功能**: 获取指定市场的交易日历
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| market | string | 是 | 市场代码 (SH, SZ, CFE) |
+| start_date | string | 否 | 开始日期 (YYYYMMDD) |
+| end_date | string | 否 | 结束日期 (YYYYMMDD) |
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data | array | 交易日列表 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/base/calendar',
+ params={
+ 'market': 'SH',
+ 'start_date': '20240101',
+ 'end_date': '20240131'
+ },
+ headers=headers
+)
+trading_days = response.json()['data']
+```
+
+---
+
+### 4.3 获取证券信息
+
+**接口**: `GET /base/codes/{code}/info`
+
+**功能**: 获取指定代码的证券信息
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| code | string | 是 | 证券代码 (URL路径参数) |
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.code | string | 代码 |
+| data.name | string | 名称 |
+| data.market | string | 市场 |
+| data.type | string | 类型 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/base/codes/600000.SH/info',
+ headers=headers
+)
+info = response.json()['data']
+```
+
+---
+
+## 五、股票数据接口
+
+### 5.1 获取股票K线数据
+
+**接口**: `GET /stock/kline`
+
+**功能**: 获取股票K线数据
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| codes | string | 是 | 代码列表 (逗号分隔) |
+| start_date | string | 是 | 开始日期 (YYYYMMDD) |
+| end_date | string | 是 | 结束日期 (YYYYMMDD) |
+| period | string | 否 | 周期 (daily, min1, min5等) |
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.{code} | array | K线数据列表 |
+| data.{code}[].trade_date | string | 交易日期 |
+| data.{code}[].open | float | 开盘价 |
+| data.{code}[].high | float | 最高价 |
+| data.{code}[].low | float | 最低价 |
+| data.{code}[].close | float | 收盘价 |
+| data.{code}[].volume | int | 成交量 |
+| data.{code}[].amount | float | 成交额 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/stock/kline',
+ params={
+ 'codes': '600000.SH,000001.SZ',
+ 'start_date': '20240101',
+ 'end_date': '20240131',
+ 'period': 'daily'
+ },
+ headers=headers
+)
+kline_data = response.json()['data']
+```
+
+---
+
+### 5.2 获取股票K线图表数据
+
+**接口**: `GET /stock/kline/{code}/chart`
+
+**功能**: 获取股票K线图表数据 (ECharts格式)
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| code | string | 是 | 股票代码 (URL路径参数) |
+| start_date | string | 是 | 开始日期 |
+| end_date | string | 是 | 结束日期 |
+| period | string | 否 | 周期 |
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.categoryData | array | 日期列表 |
+| data.values | array | K线值 [open, close, low, high, volume] |
+| data.volumes | array | 成交量数据 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/stock/kline/600000.SH/chart',
+ params={
+ 'start_date': '20240101',
+ 'end_date': '20240131',
+ 'period': 'daily'
+ },
+ headers=headers
+)
+chart_data = response.json()['data']
+```
+
+---
+
+### 5.3 批量获取股票K线
+
+**接口**: `POST /stock/kline/batch`
+
+**功能**: 批量获取多个股票的K线数据
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| codes | array | 是 | 代码列表 |
+| start_date | string | 是 | 开始日期 |
+| end_date | string | 是 | 结束日期 |
+| period | string | 否 | 周期 |
+
+**请求示例**:
+
+```json
+{
+ "codes": ["600000.SH", "000001.SZ", "000002.SZ"],
+ "start_date": "20240101",
+ "end_date": "20240131",
+ "period": "daily"
+}
+```
+
+**调用示例**:
+
+```python
+response = requests.post(
+ 'http://localhost:8000/api/v1/stock/kline/batch',
+ headers=headers,
+ json={
+ 'codes': ['600000.SH', '000001.SZ'],
+ 'start_date': '20240101',
+ 'end_date': '20240131',
+ 'period': 'daily'
+ }
+)
+```
+
+---
+
+## 六、期货数据接口
+
+### 6.1 获取期货K线数据
+
+**接口**: `GET /future/kline`
+
+**功能**: 获取期货K线数据
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| codes | string | 是 | 代码列表 (逗号分隔) |
+| start_date | string | 是 | 开始日期 |
+| end_date | string | 是 | 结束日期 |
+| period | string | 否 | 周期 |
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.{code} | array | K线数据 |
+| data.{code}[].trade_date | string | 交易日期 |
+| data.{code}[].open | float | 开盘价 |
+| data.{code}[].high | float | 最高价 |
+| data.{code}[].low | float | 最低价 |
+| data.{code}[].close | float | 收盘价 |
+| data.{code}[].volume | int | 成交量 |
+| data.{code}[].settle | float | 结算价 |
+| data.{code}[].open_interest | int | 持仓量 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/future/kline',
+ params={
+ 'codes': 'IF2401.CFE',
+ 'start_date': '20240101',
+ 'end_date': '20240131',
+ 'period': 'daily'
+ },
+ headers=headers
+)
+```
+
+---
+
+### 6.2 获取期货品种列表
+
+**接口**: `GET /cache/future-varieties`
+
+**功能**: 获取所有期货品种列表
+
+**请求参数**: 无
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.varieties | array | 品种列表 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/cache/future-varieties',
+ headers=headers
+)
+varieties = response.json()['data']['varieties']
+```
+
+---
+
+### 6.3 获取主力合约
+
+**接口**: `GET /cache/main-contracts`
+
+**功能**: 获取所有品种的主力合约
+
+**请求参数**: 无
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.main_contracts | object | {品种: 主力合约} |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/cache/main-contracts',
+ headers=headers
+)
+main_contracts = response.json()['data']['main_contracts']
+```
+
+---
+
+## 七、缓存管理接口
+
+### 7.1 一键检测所有数据
+
+**接口**: `POST /cache/detect-all-missing`
+
+**功能**: 检测所有数据的缺失情况
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| security_type | string | 是 | 证券类型 (stock, future) |
+| period_type | string | 否 | 周期类型 (daily) |
+| start_date | string | 是 | 开始日期 |
+| end_date | string | 是 | 结束日期 |
+| contract_type | string | 否 | 合约类型 (all, main) |
+
+**请求示例**:
+
+```json
+{
+ "security_type": "stock",
+ "period_type": "daily",
+ "start_date": "20240101",
+ "end_date": "20240131",
+ "contract_type": "all"
+}
+```
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.task_id | int | 任务ID |
+| data.status | string | 任务状态 |
+| data.total_count | int | 检测总数 |
+| data.complete_count | int | 完整数量 |
+| data.missing_count | int | 缺失数量 |
+| data.error_count | int | 错误数量 |
+| data.daily_stats | object | 每日统计 |
+| data.missing_codes | array | 缺失代码列表 |
+
+**调用示例**:
+
+```python
+response = requests.post(
+ 'http://localhost:8000/api/v1/cache/detect-all-missing',
+ headers=headers,
+ json={
+ 'security_type': 'stock',
+ 'period_type': 'daily',
+ 'start_date': '20240101',
+ 'end_date': '20240131',
+ 'contract_type': 'all'
+ }
+)
+result = response.json()['data']
+```
+
+---
+
+### 7.2 一键缓存所有数据
+
+**接口**: `POST /cache/cache-all-missing`
+
+**功能**: 缓存所有数据 (异步执行)
+
+**请求参数**: 同检测接口
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.task_id | int | 任务ID |
+| data.status | string | 任务状态 |
+| data.total_count | int | 总数 |
+| data.progress | float | 进度 |
+
+**调用示例**:
+
+```python
+response = requests.post(
+ 'http://localhost:8000/api/v1/cache/cache-all-missing',
+ headers=headers,
+ json={
+ 'security_type': 'stock',
+ 'period_type': 'daily',
+ 'start_date': '20240101',
+ 'end_date': '20240131'
+ }
+)
+task_id = response.json()['data']['task_id']
+```
+
+---
+
+### 7.3 一键补齐缺失数据
+
+**接口**: `POST /cache/fill-missing`
+
+**功能**: 只补齐检测到的缺失数据 (异步执行)
+
+**请求参数**: 同检测接口
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.task_id | int | 任务ID |
+| data.missing_count | int | 缺失代码数量 |
+| data.status | string | 任务状态 |
+
+**调用示例**:
+
+```python
+response = requests.post(
+ 'http://localhost:8000/api/v1/cache/fill-missing',
+ headers=headers,
+ json={
+ 'security_type': 'stock',
+ 'period_type': 'daily',
+ 'start_date': '20240101',
+ 'end_date': '20240131'
+ }
+)
+```
+
+---
+
+### 7.4 获取任务进度
+
+**接口**: `GET /cache/tasks/{task_id}`
+
+**功能**: 获取缓存任务进度
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| task_id | int | 是 | 任务ID (URL路径参数) |
+
+**响应参数**:
+
+| 参数名 | 类型 | 说明 |
+|--------|------|------|
+| data.task.id | int | 任务ID |
+| data.task.status | string | 状态 |
+| data.task.progress | float | 进度 |
+| data.task.total_count | int | 总数 |
+| data.task.success_count | int | 成功数 |
+| data.task.error_count | int | 错误数 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ f'http://localhost:8000/api/v1/cache/tasks/{task_id}',
+ headers=headers
+)
+task_status = response.json()['data']['task']
+```
+
+---
+
+### 7.5 获取任务列表
+
+**接口**: `GET /cache/tasks`
+
+**功能**: 获取缓存任务列表
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| page | int | 否 | 页码 (默认1) |
+| page_size | int | 否 | 每页数量 (默认20) |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/cache/tasks',
+ params={'page': 1, 'page_size': 10},
+ headers=headers
+)
+tasks = response.json()['data']['items']
+```
+
+---
+
+## 八、财务数据接口
+
+### 8.1 获取资产负债表
+
+**接口**: `GET /finance/balance-sheet`
+
+**功能**: 获取资产负债表数据
+
+**请求参数**:
+
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| codes | string | 是 | 代码列表 |
+| start_date | string | 是 | 开始日期 |
+| end_date | string | 是 | 结束日期 |
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/finance/balance-sheet',
+ params={
+ 'codes': '600000.SH',
+ 'start_date': '20230101',
+ 'end_date': '20231231'
+ },
+ headers=headers
+)
+```
+
+---
+
+### 8.2 获取利润表
+
+**接口**: `GET /finance/income`
+
+**功能**: 获取利润表数据
+
+**请求参数**: 同资产负债表
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/finance/income',
+ params={
+ 'codes': '600000.SH',
+ 'start_date': '20230101',
+ 'end_date': '20231231'
+ },
+ headers=headers
+)
+```
+
+---
+
+### 8.3 获取现金流量表
+
+**接口**: `GET /finance/cash-flow`
+
+**功能**: 获取现金流量表数据
+
+**请求参数**: 同资产负债表
+
+**调用示例**:
+
+```python
+response = requests.get(
+ 'http://localhost:8000/api/v1/finance/cash-flow',
+ params={
+ 'codes': '600000.SH',
+ 'start_date': '20230101',
+ 'end_date': '20231231'
+ },
+ headers=headers
+)
+```
+
+---
+
+## 九、错误码说明
+
+| 错误码 | 说明 |
+|--------|------|
+| 200 | 成功 |
+| 400 | 请求参数错误 |
+| 401 | 未授权 (Token无效或过期) |
+| 403 | 禁止访问 |
+| 404 | 资源不存在 |
+| 500 | 服务器内部错误 |
+
+---
+
+## 十、最佳实践
+
+### 10.1 Token 管理
+
+```python
+class AmazingDataClient:
+ def __init__(self, base_url, username, password):
+ self.base_url = base_url
+ self.token = None
+ self.login(username, password)
+
+ def login(self, username, password):
+ response = requests.post(
+ f'{self.base_url}/auth/login',
+ json={'username': username, 'password': password}
+ )
+ self.token = response.json()['data']['access_token']
+
+ def get_headers(self):
+ return {'Authorization': f'Bearer {self.token}'}
+
+ def request(self, method, path, **kwargs):
+ kwargs['headers'] = self.get_headers()
+ return requests.request(method, f'{self.base_url}{path}', **kwargs)
+```
+
+### 10.2 批量数据处理
+
+```python
+# 分批处理大量代码
+codes = ['600000.SH', '000001.SZ', ...] # 大量代码
+batch_size = 50
+
+for i in range(0, len(codes), batch_size):
+ batch = codes[i:i+batch_size]
+ response = client.request('POST', '/stock/kline/batch', json={
+ 'codes': batch,
+ 'start_date': '20240101',
+ 'end_date': '20240131'
+ })
+```
+
+### 10.3 异步任务轮询
+
+```python
+import time
+
+def wait_for_task(client, task_id, timeout=300):
+ start_time = time.time()
+ while time.time() - start_time < timeout:
+ response = client.request('GET', f'/cache/tasks/{task_id}')
+ task = response.json()['data']['task']
+
+ if task['status'] == 'completed':
+ return task
+ elif task['status'] == 'failed':
+ raise Exception(task['error_message'])
+
+ time.sleep(2)
+
+ raise Exception('任务超时')
+
+# 使用
+response = client.request('POST', '/cache/fill-missing', json={...})
+task_id = response.json()['data']['task_id']
+result = wait_for_task(client, task_id)
+```
+
+---
+
+## 十一、联系与支持
+
+如有问题,请联系技术支持团队。
\ No newline at end of file
diff --git a/docs/database_design.md b/docs/database_design.md
new file mode 100644
index 0000000..7ef19bd
--- /dev/null
+++ b/docs/database_design.md
@@ -0,0 +1,191 @@
+# 数据库设计文档
+
+## 一、设计概述
+
+本数据库设计用于存储股票交易数据、市场统计信息和板块动量数据,支持股票分析系统的运行。设计采用关系型数据库,主要存储以下数据:
+
+- 股票基本信息
+- 股票实时行情
+- K线历史数据
+- 板块信息
+- 板块动量数据
+- 市场统计数据
+
+## 二、数据模型设计
+
+### 1. 股票基本信息表 (`stock_basic`)
+
+| 字段名 | 数据类型 | 约束 | 描述 |
+|-------|---------|------|------|
+| `code` | `VARCHAR(10)` | `PRIMARY KEY` | 股票代码 |
+| `name` | `VARCHAR(50)` | `NOT NULL` | 股票名称 |
+| `sector_code` | `VARCHAR(10)` | `NULL` | 所属板块代码 |
+| `list_date` | `DATE` | `NULL` | 上市日期 |
+| `is_etf` | `BOOLEAN` | `DEFAULT FALSE` | 是否ETF |
+| `market` | `VARCHAR(10)` | `NULL` | 市场(沪市/深市) |
+| `updated_at` | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP` | 更新时间 |
+
+### 2. 股票实时行情表 (`stock_quote`)
+
+| 字段名 | 数据类型 | 约束 | 描述 |
+|-------|---------|------|------|
+| `code` | `VARCHAR(10)` | `PRIMARY KEY` | 股票代码 |
+| `price` | `DECIMAL(10,2)` | `NULL` | 最新价 |
+| `change_pct` | `DECIMAL(6,2)` | `NULL` | 涨跌幅(%) |
+| `change_amount` | `DECIMAL(10,2)` | `NULL` | 涨跌额 |
+| `volume` | `BIGINT` | `NULL` | 成交量 |
+| `amount` | `DECIMAL(18,2)` | `NULL` | 成交额 |
+| `turnover_rate` | `DECIMAL(8,4)` | `NULL` | 换手率(%) |
+| `amplitude` | `DECIMAL(6,2)` | `NULL` | 振幅(%) |
+| `high` | `DECIMAL(10,2)` | `NULL` | 最高价 |
+| `low` | `DECIMAL(10,2)` | `NULL` | 最低价 |
+| `open` | `DECIMAL(10,2)` | `NULL` | 开盘价 |
+| `updated_at` | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP` | 更新时间 |
+
+### 3. 股票K线数据表 (`stock_kline`)
+
+| 字段名 | 数据类型 | 约束 | 描述 |
+|-------|---------|------|------|
+| `id` | `BIGINT` | `PRIMARY KEY AUTO_INCREMENT` | 自增主键 |
+| `code` | `VARCHAR(10)` | `NOT NULL` | 股票代码 |
+| `date` | `DATE` | `NOT NULL` | 日期 |
+| `open` | `DECIMAL(10,2)` | `NULL` | 开盘价 |
+| `high` | `DECIMAL(10,2)` | `NULL` | 最高价 |
+| `low` | `DECIMAL(10,2)` | `NULL` | 最低价 |
+| `close` | `DECIMAL(10,2)` | `NULL` | 收盘价 |
+| `volume` | `BIGINT` | `NULL` | 成交量 |
+| `amount` | `DECIMAL(18,2)` | `NULL` | 成交额 |
+| `pct_chg` | `DECIMAL(6,2)` | `NULL` | 涨跌幅(%) |
+| `INDEX` | | `(code, date)` | 复合索引 |
+
+### 4. 板块信息表 (`sector_info`)
+
+| 字段名 | 数据类型 | 约束 | 描述 |
+|-------|---------|------|------|
+| `code` | `VARCHAR(10)` | `PRIMARY KEY` | 板块代码 |
+| `name` | `VARCHAR(50)` | `NOT NULL` | 板块名称 |
+| `stock_count` | `INT` | `DEFAULT 0` | 成分股数量 |
+| `updated_at` | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP` | 更新时间 |
+
+### 5. 板块动量表 (`sector_momentum`)
+
+| 字段名 | 数据类型 | 约束 | 描述 |
+|-------|---------|------|------|
+| `id` | `BIGINT` | `PRIMARY KEY AUTO_INCREMENT` | 自增主键 |
+| `sector_code` | `VARCHAR(10)` | `NOT NULL` | 板块代码 |
+| `date` | `DATE` | `NOT NULL` | 日期 |
+| `momentum_value` | `DECIMAL(10,4)` | `NULL` | 动量值 |
+| `n` | `INT` | `NULL` | 板块内动量个股数 |
+| `N` | `INT` | `NULL` | 板块总个股数 |
+| `rank` | `INT` | `NULL` | 动量排名 |
+| `rank_change` | `INT` | `NULL` | 排名变化 |
+| `updated_at` | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP` | 更新时间 |
+| `INDEX` | | `(sector_code, date)` | 复合索引 |
+
+### 6. 市场统计表 (`market_stats`)
+
+| 字段名 | 数据类型 | 约束 | 描述 |
+|-------|---------|------|------|
+| `id` | `BIGINT` | `PRIMARY KEY AUTO_INCREMENT` | 自增主键 |
+| `date` | `DATE` | `NOT NULL UNIQUE` | 日期 |
+| `up_count` | `INT` | `DEFAULT 0` | 上涨家数 |
+| `down_count` | `INT` | `DEFAULT 0` | 下跌家数 |
+| `flat_count` | `INT` | `DEFAULT 0` | 平盘家数 |
+| `limit_up_count` | `INT` | `DEFAULT 0` | 涨停家数 |
+| `limit_down_count` | `INT` | `DEFAULT 0` | 跌停家数 |
+| `total_amount` | `DECIMAL(20,2)` | `NULL` | 两市成交额 |
+| `updated_at` | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP` | 更新时间 |
+
+## 三、索引设计
+
+### 1. 主键索引
+- `stock_basic.code`
+- `stock_quote.code`
+- `sector_info.code`
+- `stock_kline.id`
+- `sector_momentum.id`
+- `market_stats.id`
+
+### 2. 复合索引
+- `stock_kline(code, date)`:加速按股票代码和日期范围查询
+- `sector_momentum(sector_code, date)`:加速按板块和日期查询
+
+### 3. 唯一索引
+- `market_stats.date`:确保每日只存储一条市场统计记录
+
+## 四、数据同步策略
+
+### 1. 实时数据
+- **同步频率**:30秒一次
+- **同步方式**:批量更新
+- **存储策略**:只保留最新数据
+
+### 2. K线数据
+- **同步频率**:每日收盘后
+- **同步方式**:增量更新
+- **存储策略**:保留完整历史数据
+
+### 3. 板块数据
+- **同步频率**:每日一次
+- **同步方式**:全量更新
+- **存储策略**:保留完整历史数据
+
+### 4. 市场统计
+- **同步频率**:每日收盘后
+- **同步方式**:增量更新
+- **存储策略**:保留完整历史数据
+
+## 五、数据清理策略
+
+### 1. 实时数据
+- 无需清理,覆盖更新
+
+### 2. K线数据
+- 保留完整历史数据,不清理
+
+### 3. 板块动量
+- 保留最近一年数据
+- 自动清理一年前的数据
+
+### 4. 市场统计
+- 保留完整历史数据,不清理
+
+## 六、性能优化建议
+
+1. **分区表**:对 `stock_kline` 表按日期分区,提高查询性能
+2. **批量操作**:使用批量插入和更新,减少数据库连接次数
+3. **索引优化**:根据查询模式优化索引
+4. **缓存层**:使用Redis缓存热点数据
+5. **读写分离**:主库用于写入,从库用于读取
+6. **连接池**:使用数据库连接池,减少连接建立开销
+
+## 七、扩展性考虑
+
+1. **分库分表**:数据量增长时,可考虑按股票代码分库分表
+2. **时间序列数据库**:K线数据可考虑使用时间序列数据库(如InfluxDB)
+3. **数据仓库**:历史数据可迁移到数据仓库,用于分析
+4. **API接口**:提供标准化的API接口,支持外部系统集成
+
+## 八、安全考虑
+
+1. **访问控制**:设置数据库用户权限,限制访问
+2. **数据加密**:敏感数据加密存储
+3. **备份策略**:定期备份数据库,确保数据安全
+4. **审计日志**:记录数据库操作日志,便于追溯
+
+## 九、技术选型
+
+- **数据库**:PostgreSQL 14+
+- **连接池**:PgBouncer
+- **ORM**:SQLAlchemy
+- **缓存**:Redis 7.0+
+- **监控**:Prometheus + Grafana
+
+## 十、部署建议
+
+1. **开发环境**:单节点数据库
+2. **测试环境**:主从复制
+3. **生产环境**:
+ - 数据库:主从复制 + 自动故障转移
+ - Redis:主从复制 + 哨兵模式
+ - 定期备份:每日全量备份,每小时增量备份
diff --git a/docs/implementation_plan.md b/docs/implementation_plan.md
new file mode 100644
index 0000000..a83b60c
--- /dev/null
+++ b/docs/implementation_plan.md
@@ -0,0 +1,431 @@
+# 实现步骤和迁移计划
+
+## 一、概述
+
+本文档详细说明股票分析系统引入数据库和Redis缓存的实现步骤和迁移计划,包括基础设施搭建、代码修改、数据迁移、测试验证等环节,确保系统平稳过渡到新的架构。
+
+## 二、实施阶段
+
+### 阶段一:基础设施搭建(1-2周)
+
+#### 1. 环境准备
+
+- **硬件准备**:
+ - 数据库服务器:4核8G内存,500G存储空间
+ - Redis服务器:4核8G内存,100G存储空间
+ - 网络配置:确保服务器间网络互通
+
+- **软件安装**:
+ - 数据库:PostgreSQL 14+
+ - Redis:Redis 7.0+
+ - 连接池:PgBouncer
+ - 监控:Prometheus + Grafana
+
+#### 2. 配置管理
+
+- **数据库配置**:
+ - 创建数据库和用户
+ - 配置连接池
+ - 设置备份策略
+
+- **Redis配置**:
+ - 配置主从复制
+ - 设置内存限制
+ - 配置持久化
+
+- **环境变量**:
+ - 数据库连接信息
+ - Redis连接信息
+ - 缓存配置参数
+
+### 阶段二:数据模型实现(2-3周)
+
+#### 1. 数据库表结构
+
+- **执行创建脚本**:
+ - 运行 `scripts/create_tables.sql`
+ - 创建所有数据表
+ - 建立索引
+
+- **验证表结构**:
+ - 检查表结构是否正确
+ - 验证索引是否创建
+ - 测试基本查询
+
+#### 2. 数据访问层
+
+- **实现数据访问类**:
+ - 股票基本信息
+ - 实时行情
+ - K线数据
+ - 板块信息
+ - 市场统计
+
+- **实现ORM映射**:
+ - 使用SQLAlchemy
+ - 定义数据模型
+ - 实现CRUD操作
+
+#### 3. 缓存工具类
+
+- **实现Redis客户端**:
+ - 连接管理
+ - 异常处理
+ - 连接池
+
+- **实现缓存操作**:
+ - 基础操作(get/set/delete)
+ - Hash操作
+ - Sorted Set操作
+ - List操作
+
+- **实现缓存键生成器**:
+ - 统一的缓存键格式
+ - 支持不同数据类型
+
+### 阶段三:业务逻辑集成(3-4周)
+
+#### 1. 数据源适配
+
+- **修改数据获取逻辑**:
+ - 优先从缓存获取
+ - 缓存未命中从数据库获取
+ - 数据库未命中从API获取
+
+- **实现数据同步**:
+ - 实时行情同步
+ - K线数据同步
+ - 板块信息同步
+
+#### 2. 服务层修改
+
+- **修改MarketService**:
+ - 集成Redis缓存
+ - 实现缓存读写逻辑
+ - 优化板块动量计算
+
+- **修改数据服务**:
+ - 集成数据库访问
+ - 实现数据持久化
+ - 优化数据查询
+
+#### 3. API接口修改
+
+- **修改API端点**:
+ - 支持缓存控制参数
+ - 优化响应格式
+ - 增加缓存状态信息
+
+- **实现缓存管理接口**:
+ - 缓存清理
+ - 缓存预热
+ - 缓存状态查询
+
+### 阶段四:数据同步(2-3周)
+
+#### 1. 历史数据导入
+
+- **准备历史数据**:
+ - 从数据源获取历史K线数据
+ - 整理股票基本信息
+ - 准备板块数据
+
+- **批量导入**:
+ - 使用批量插入
+ - 优化导入速度
+ - 验证数据完整性
+
+#### 2. 实时数据同步
+
+- **实现同步任务**:
+ - 实时行情同步
+ - 市场统计同步
+ - 板块动量计算
+
+- **设置定时任务**:
+ - 实时行情:30秒一次
+ - 板块动量:1小时一次
+ - 市场统计:1小时一次
+
+#### 3. 数据验证
+
+- **验证数据一致性**:
+ - 缓存与数据库对比
+ - 数据库与数据源对比
+ - 历史数据与实时数据对比
+
+- **性能测试**:
+ - 数据同步速度
+ - 缓存命中率
+ - 系统响应时间
+
+### 阶段五:测试与优化(2-3周)
+
+#### 1. 功能测试
+
+- **单元测试**:
+ - 数据访问层测试
+ - 缓存操作测试
+ - 服务层测试
+
+- **集成测试**:
+ - API接口测试
+ - 数据同步测试
+ - 缓存一致性测试
+
+- **端到端测试**:
+ - 完整业务流程测试
+ - 系统集成测试
+ - 用户场景测试
+
+#### 2. 性能测试
+
+- **负载测试**:
+ - 模拟并发用户
+ - 测试系统极限
+ - 识别性能瓶颈
+
+- **压力测试**:
+ - 持续高负载
+ - 测试系统稳定性
+ - 验证系统恢复能力
+
+- **基准测试**:
+ - 与旧系统对比
+ - 验证性能提升
+ - 指导进一步优化
+
+#### 3. 优化调整
+
+- **根据测试结果优化**:
+ - 数据库索引
+ - 缓存策略
+ - API性能
+
+- **调整配置参数**:
+ - 连接池大小
+ - 缓存过期时间
+ - 同步频率
+
+### 阶段六:上线部署(1-2周)
+
+#### 1. 部署准备
+
+- **环境准备**:
+ - 生产环境配置
+ - 监控系统部署
+ - 告警策略配置
+
+- **数据准备**:
+ - 生产数据同步
+ - 缓存预热
+ - 系统初始化
+
+#### 2. 灰度发布
+
+- **分阶段部署**:
+ - 先部署非核心服务
+ - 逐步切换流量
+ - 监控系统状态
+
+- **回滚机制**:
+ - 准备回滚方案
+ - 测试回滚流程
+ - 确保快速回滚能力
+
+#### 3. 全量上线
+
+- **切换流量**:
+ - 逐步增加新系统流量
+ - 监控系统性能
+ - 处理异常情况
+
+- **系统监控**:
+ - 实时监控系统状态
+ - 及时处理告警
+ - 优化系统配置
+
+## 三、迁移策略
+
+### 1. 双写模式
+
+- **实现方式**:
+ - 同时写入旧缓存和新缓存
+ - 同时写入数据库
+ - 保持数据一致性
+
+- **优点**:
+ - 确保数据不丢失
+ - 便于回滚
+ - 降低迁移风险
+
+### 2. 灰度切换
+
+- **切换步骤**:
+ - 10%流量:验证基本功能
+ - 30%流量:测试性能
+ - 50%流量:全面测试
+ - 100%流量:完全切换
+
+- **监控指标**:
+ - 响应时间
+ - 错误率
+ - 缓存命中率
+ - 系统负载
+
+### 3. 回滚策略
+
+- **触发条件**:
+ - 系统响应时间显著增加
+ - 错误率超过阈值
+ - 数据不一致
+
+- **回滚步骤**:
+ - 停止新系统写入
+ - 切换流量回旧系统
+ - 验证旧系统运行状态
+ - 分析问题原因
+
+### 4. 数据迁移
+
+- **历史数据**:
+ - 批量导入历史K线数据
+ - 导入股票基本信息
+ - 导入板块信息
+
+- **实时数据**:
+ - 并行同步实时行情
+ - 逐步切换到新系统
+ - 验证数据一致性
+
+## 四、风险评估
+
+### 1. 潜在风险
+
+| 风险 | 影响 | 概率 | 缓解措施 |
+|------|------|------|----------|
+| 数据源API限流 | 数据获取失败 | 高 | 多数据源切换,增加缓存时间 |
+| 数据库性能问题 | 查询缓慢 | 中 | 优化索引,使用连接池 |
+| Redis内存不足 | 缓存失效 | 中 | 合理设置内存限制,使用LRU策略 |
+| 数据一致性问题 | 数据错误 | 中 | 双写模式,定期同步 |
+| 系统负载过高 | 响应缓慢 | 中 | 水平扩展,优化代码 |
+| 部署失败 | 系统不可用 | 低 | 灰度发布,准备回滚方案 |
+
+### 2. 风险应对
+
+- **数据源限流**:
+ - 实现多数据源自动切换
+ - 增加缓存时间
+ - 批量获取数据
+
+- **数据库性能**:
+ - 优化索引
+ - 使用连接池
+ - 实现读写分离
+
+- **Redis内存**:
+ - 合理设置内存限制
+ - 使用LRU策略
+ - 数据压缩
+
+- **数据一致性**:
+ - 双写模式
+ - 定期同步
+ - 数据验证
+
+- **系统负载**:
+ - 水平扩展
+ - 优化代码
+ - 缓存预热
+
+## 五、时间线
+
+| 阶段 | 时间 | 主要任务 |
+|------|------|----------|
+| 阶段一:基础设施搭建 | 第1-2周 | 环境准备,软件安装,配置管理 |
+| 阶段二:数据模型实现 | 第3-5周 | 数据库表结构,数据访问层,缓存工具类 |
+| 阶段三:业务逻辑集成 | 第6-9周 | 数据源适配,服务层修改,API接口修改 |
+| 阶段四:数据同步 | 第10-12周 | 历史数据导入,实时数据同步,数据验证 |
+| 阶段五:测试与优化 | 第13-15周 | 功能测试,性能测试,优化调整 |
+| 阶段六:上线部署 | 第16-17周 | 部署准备,灰度发布,全量上线 |
+
+## 六、团队分工
+
+### 1. 技术角色
+
+| 角色 | 职责 |
+|------|------|
+| 系统架构师 | 整体架构设计,技术选型,方案评审 |
+| 后端开发 | 数据访问层,服务层,API接口 |
+| 前端开发 | 前端适配,用户界面优化 |
+| 数据库工程师 | 数据库设计,性能优化,数据迁移 |
+| 运维工程师 | 基础设施搭建,监控部署,系统维护 |
+| 测试工程师 | 功能测试,性能测试,回归测试 |
+
+### 2. 任务分配
+
+- **系统架构师**:
+ - 设计整体架构
+ - 制定技术方案
+ - 评审代码和设计
+
+- **后端开发**:
+ - 实现数据访问层
+ - 修改服务层逻辑
+ - 优化API接口
+
+- **前端开发**:
+ - 适配新的API接口
+ - 优化前端性能
+ - 测试用户界面
+
+- **数据库工程师**:
+ - 设计数据库表结构
+ - 优化数据库性能
+ - 实现数据迁移
+
+- **运维工程师**:
+ - 搭建基础设施
+ - 配置监控系统
+ - 部署和维护系统
+
+- **测试工程师**:
+ - 编写测试用例
+ - 执行测试计划
+ - 报告和跟踪问题
+
+## 七、成功指标
+
+### 1. 性能指标
+
+| 指标 | 目标值 | 测量方法 |
+|------|--------|----------|
+| API响应时间 | <100ms | 平均响应时间 |
+| 缓存命中率 | >90% | 缓存命中次数/总请求次数 |
+| 数据同步延迟 | <5秒 | 数据更新到缓存的延迟 |
+| 系统并发能力 | >1000用户 | 负载测试 |
+| 数据库查询时间 | <50ms | 平均查询时间 |
+
+### 2. 功能指标
+
+| 指标 | 目标值 | 测量方法 |
+|------|--------|----------|
+| 数据一致性 | 100% | 缓存与数据库对比 |
+| 系统可用性 | >99.9% | 系统运行时间 |
+| 数据完整性 | 100% | 数据验证 |
+| 功能覆盖率 | 100% | 测试用例覆盖 |
+
+### 3. 业务指标
+
+| 指标 | 目标值 | 测量方法 |
+|------|--------|----------|
+| 用户体验 | 提升50% | 用户反馈 |
+| 分析速度 | 提升80% | 分析任务执行时间 |
+| 数据更新频率 | 30秒/次 | 实时行情更新 |
+| 系统稳定性 | 无重大故障 | 故障统计 |
+
+## 八、总结
+
+本实现步骤和迁移计划详细说明了股票分析系统引入数据库和Redis缓存的全过程,包括基础设施搭建、代码修改、数据迁移、测试验证等环节。通过分阶段实施和灰度发布策略,可以确保系统平稳过渡到新的架构,同时降低迁移风险。
+
+实施过程中,需要密切关注系统性能和数据一致性,及时处理出现的问题,确保系统的稳定性和可靠性。通过本计划的实施,可以显著提高系统性能,提升用户体验,为股票分析系统的长期发展奠定基础。
\ No newline at end of file
diff --git a/docs/performance_optimization.md b/docs/performance_optimization.md
new file mode 100644
index 0000000..a301bd3
--- /dev/null
+++ b/docs/performance_optimization.md
@@ -0,0 +1,519 @@
+# 性能优化方案
+
+## 一、概述
+
+性能优化是保证股票分析系统稳定运行和良好用户体验的关键。本方案从系统架构、数据库、缓存、API、前端等多个维度,提出全面的性能优化策略,旨在提高系统响应速度、降低资源消耗、提升用户体验。
+
+## 二、系统架构优化
+
+### 1. 分层架构
+
+- **架构层次**:
+ - 前端层:React应用
+ - API层:FastAPI接口
+ - 服务层:业务逻辑
+ - 数据层:Redis缓存 + 数据库
+ - 数据源层:外部API
+
+- **优化策略**:
+ - 减少层间通信开销
+ - 优化数据流转路径
+ - 实现服务解耦
+
+### 2. 微服务化
+
+- **服务拆分**:
+ - 市场数据服务
+ - 分析计算服务
+ - 缓存服务
+ - 数据同步服务
+
+- **优势**:
+ - 独立部署和扩展
+ - 故障隔离
+ - 按需资源分配
+
+### 3. 容器化部署
+
+- **技术选型**:
+ - Docker容器
+ - Kubernetes编排
+
+- **优势**:
+ - 环境一致性
+ - 快速部署和回滚
+ - 资源利用率高
+
+## 三、数据库优化
+
+### 1. 索引优化
+
+- **优化策略**:
+ - 为频繁查询的字段创建索引
+ - 使用复合索引加速多条件查询
+ - 定期重建索引
+
+- **索引设计**:
+ - `stock_kline(code, date)`:加速按股票和日期查询
+ - `sector_momentum(sector_code, date)`:加速按板块和日期查询
+ - `market_stats(date)`:加速按日期查询
+
+### 2. 查询优化
+
+- **优化策略**:
+ - 使用预处理语句
+ - 避免SELECT *
+ - 合理使用JOIN
+ - 分页查询
+
+- **示例**:
+ ```sql
+ -- 优化前
+ SELECT * FROM stock_kline WHERE code = '600519';
+
+ -- 优化后
+ SELECT date, open, high, low, close FROM stock_kline
+ WHERE code = '600519'
+ ORDER BY date DESC
+ LIMIT 30;
+ ```
+
+### 3. 存储优化
+
+- **分区表**:
+ - 按日期分区`stock_kline`表
+ - 提高查询性能
+ - 便于数据管理
+
+- **数据压缩**:
+ - 启用列压缩
+ - 减少存储空间
+ - 提高I/O性能
+
+### 4. 连接池优化
+
+- **配置优化**:
+ - 合理设置连接池大小
+ - 调整连接超时时间
+ - 监控连接使用情况
+
+- **技术选型**:
+ - PostgreSQL:PgBouncer
+ - MySQL:ProxySQL
+
+## 四、Redis缓存优化
+
+### 1. 内存优化
+
+- **数据结构选择**:
+ - 使用Hash存储对象
+ - 使用Sorted Set存储排序数据
+ - 使用List存储时间序列数据
+
+- **内存配置**:
+ - 设置合理的maxmemory
+ - 选择合适的内存淘汰策略
+ - 监控内存使用情况
+
+### 2. 命令优化
+
+- **批量操作**:
+ - 使用Pipeline批量执行命令
+ - 使用MSET、MGET等批量命令
+ - 减少网络往返次数
+
+- **避免阻塞**:
+ - 避免使用KEYS命令
+ - 使用SCAN替代
+ - 避免大键操作
+
+### 3. 部署优化
+
+- **高可用**:
+ - 主从复制
+ - 哨兵模式
+ - 集群模式
+
+- **网络优化**:
+ - 启用Redis持久化
+ - 优化网络配置
+ - 使用Unix Socket(本地部署)
+
+## 五、API优化
+
+### 1. 接口设计
+
+- **RESTful设计**:
+ - 合理的URL结构
+ - 标准的HTTP方法
+ - 一致的响应格式
+
+- **参数优化**:
+ - 合理的分页参数
+ - 缓存控制参数
+ - 字段过滤参数
+
+### 2. 性能优化
+
+- **异步处理**:
+ - 使用FastAPI的异步特性
+ - 处理耗时操作
+ - 提高并发能力
+
+- **响应优化**:
+ - 压缩响应数据
+ - 合理设置缓存头
+ - 避免重复计算
+
+### 3. 错误处理
+
+- **统一错误处理**:
+ - 标准的错误格式
+ - 详细的错误信息
+ - 合理的错误码
+
+- **降级策略**:
+ - 缓存失效时的降级
+ - 数据源失败时的降级
+ - 系统过载时的降级
+
+## 六、前端优化
+
+### 1. 代码优化
+
+- **组件拆分**:
+ - 合理拆分组件
+ - 减少组件渲染开销
+ - 提高代码可维护性
+
+- **状态管理**:
+ - 合理使用状态管理
+ - 避免不必要的状态更新
+ - 优化状态更新策略
+
+### 2. 资源优化
+
+- **静态资源**:
+ - 压缩JS/CSS
+ - 使用CDN
+ - 资源缓存
+
+- **图片优化**:
+ - 合理的图片格式
+ - 图片压缩
+ - 懒加载
+
+### 3. 渲染优化
+
+- **虚拟滚动**:
+ - 处理大量数据列表
+ - 减少DOM节点
+ - 提高滚动性能
+
+- **防抖和节流**:
+ - 优化用户输入
+ - 减少API调用
+ - 提高响应速度
+
+### 4. 网络优化
+
+- **HTTP/2**:
+ - 启用HTTP/2
+ - 多路复用
+ - 头部压缩
+
+- **API调用优化**:
+ - 批量请求
+ - 缓存请求结果
+ - 合理的请求时机
+
+## 七、数据处理优化
+
+### 1. 数据获取优化
+
+- **批量获取**:
+ - 减少API调用次数
+ - 提高数据获取效率
+ - 降低数据源负载
+
+- **并行处理**:
+ - 并行获取数据
+ - 提高处理速度
+ - 充分利用多核CPU
+
+### 2. 计算优化
+
+- **预计算**:
+ - 提前计算常用指标
+ - 减少实时计算开销
+ - 提高响应速度
+
+- **增量计算**:
+ - 只计算变化的数据
+ - 减少计算量
+ - 提高计算效率
+
+### 3. 数据压缩
+
+- **传输压缩**:
+ - 使用GZIP压缩
+ - 减少网络传输量
+ - 提高传输速度
+
+- **存储压缩**:
+ - 压缩存储数据
+ - 减少存储空间
+ - 提高I/O性能
+
+## 八、监控与调优
+
+### 1. 监控体系
+
+- **系统监控**:
+ - CPU、内存、磁盘、网络
+ - 系统负载
+ - 进程状态
+
+- **应用监控**:
+ - API响应时间
+ - 错误率
+ - 请求量
+
+- **数据库监控**:
+ - 查询性能
+ - 连接数
+ - 缓存命中率
+
+- **Redis监控**:
+ - 内存使用
+ - 命令执行时间
+ - 命中率
+
+### 2. 调优策略
+
+- **性能分析**:
+ - 使用性能分析工具
+ - 识别性能瓶颈
+ - 制定优化方案
+
+- **A/B测试**:
+ - 对比不同优化方案
+ - 选择最优方案
+ - 持续优化
+
+- **自动调优**:
+ - 基于监控数据自动调整
+ - 适应不同负载情况
+ - 提高系统稳定性
+
+## 九、安全优化
+
+### 1. 访问控制
+
+- **API认证**:
+ - 使用JWT认证
+ - 合理的权限控制
+ - 防止未授权访问
+
+- **Rate Limiting**:
+ - 限制API请求频率
+ - 防止恶意请求
+ - 保护系统资源
+
+### 2. 数据安全
+
+- **数据加密**:
+ - 敏感数据加密
+ - 传输加密
+ - 存储加密
+
+- **数据验证**:
+ - 输入验证
+ - 防止SQL注入
+ - 防止XSS攻击
+
+### 3. 系统安全
+
+- **防火墙**:
+ - 配置防火墙规则
+ - 限制访问IP
+ - 保护系统安全
+
+- **漏洞扫描**:
+ - 定期扫描系统漏洞
+ - 及时修复
+ - 提高系统安全性
+
+## 十、扩展性考虑
+
+### 1. 水平扩展
+
+- **服务扩展**:
+ - 无状态服务水平扩展
+ - 负载均衡
+ - 提高系统容量
+
+- **数据扩展**:
+ - 数据库分片
+ - Redis集群
+ - 支持大数据量
+
+### 2. 垂直扩展
+
+- **资源升级**:
+ - 增加服务器资源
+ - 优化配置
+ - 提高单节点性能
+
+- **代码优化**:
+ - 算法优化
+ - 数据结构优化
+ - 提高代码效率
+
+### 3. 技术选型
+
+- **未来兼容**:
+ - 选择成熟的技术栈
+ - 考虑技术发展趋势
+ - 避免技术债务
+
+- **可替代性**:
+ - 模块化设计
+ - 接口标准化
+ - 便于技术升级
+
+## 十一、性能测试
+
+### 1. 测试方法
+
+- **负载测试**:
+ - 模拟并发用户
+ - 测试系统极限
+ - 识别性能瓶颈
+
+- **压力测试**:
+ - 持续高负载
+ - 测试系统稳定性
+ - 验证系统恢复能力
+
+- **性能基准测试**:
+ - 建立性能基准
+ - 对比优化效果
+ - 指导优化方向
+
+### 2. 测试工具
+
+- **API测试**:
+ - JMeter
+ - Locust
+ - k6
+
+- **数据库测试**:
+ - pgbench
+ - sysbench
+
+- **前端测试**:
+ - Lighthouse
+ - WebPageTest
+
+### 3. 测试指标
+
+| 指标 | 目标值 | 测量方法 |
+|------|--------|----------|
+| API响应时间 | <100ms | 平均响应时间 |
+| 页面加载时间 | <2s | 首次内容绘制 |
+| 并发用户数 | >1000 | 系统稳定运行 |
+| 数据库查询时间 | <50ms | 平均查询时间 |
+| 缓存命中率 | >90% | 缓存命中次数/总请求次数 |
+
+## 十二、实施计划
+
+### 1. 短期优化(1-2周)
+
+- **数据库索引优化**:
+ - 创建必要的索引
+ - 优化查询语句
+
+- **Redis缓存优化**:
+ - 调整缓存策略
+ - 优化数据结构
+
+- **API优化**:
+ - 实现异步处理
+ - 优化响应格式
+
+### 2. 中期优化(1-2个月)
+
+- **系统架构优化**:
+ - 服务拆分
+ - 容器化部署
+
+- **数据处理优化**:
+ - 实现批量处理
+ - 优化计算逻辑
+
+- **监控体系**:
+ - 建立监控系统
+ - 实施告警策略
+
+### 3. 长期优化(3-6个月)
+
+- **微服务架构**:
+ - 完成服务拆分
+ - 实现服务治理
+
+- **大数据处理**:
+ - 优化数据存储
+ - 实现数据分片
+
+- **智能优化**:
+ - 基于AI的自动调优
+ - 预测性维护
+
+## 十三、预期效果
+
+### 1. 性能提升
+
+- **响应速度**:
+ - API响应时间减少50%
+ - 页面加载时间减少60%
+ - 数据查询速度提升70%
+
+- **系统容量**:
+ - 并发用户数提升5倍
+ - 数据处理能力提升10倍
+ - 系统稳定性显著提高
+
+### 2. 资源利用
+
+- **CPU利用率**:
+ - 更合理的CPU使用
+ - 减少不必要的计算
+
+- **内存使用**:
+ - 优化内存分配
+ - 减少内存泄漏
+
+- **存储使用**:
+ - 压缩存储数据
+ - 合理的数据清理策略
+
+### 3. 用户体验
+
+- **界面响应**:
+ - 更流畅的操作体验
+ - 减少加载等待时间
+
+- **功能可用性**:
+ - 更高的系统可用性
+ - 更少的错误和异常
+
+- **数据准确性**:
+ - 更准确的市场数据
+ - 更及时的信息更新
+
+## 十四、总结
+
+性能优化是一个持续的过程,需要从系统架构、数据库、缓存、API、前端等多个维度进行全面优化。通过本方案的实施,可以显著提高系统性能,提升用户体验,为股票分析系统的稳定运行和未来发展奠定基础。
+
+同时,性能优化需要结合实际情况,根据系统的特点和业务需求,制定合理的优化策略。通过持续监控和调优,不断提升系统性能,以适应不断变化的业务需求和技术挑战。
\ No newline at end of file
diff --git a/docs/redis_cache_design.md b/docs/redis_cache_design.md
new file mode 100644
index 0000000..5b8f863
--- /dev/null
+++ b/docs/redis_cache_design.md
@@ -0,0 +1,348 @@
+# Redis缓存架构设计
+
+## 一、设计概述
+
+本Redis缓存架构用于存储股票分析系统的热点数据,减少对数据源和数据库的访问,提高系统响应速度。设计采用Redis作为缓存层,主要存储以下数据:
+
+- 实时行情数据
+- 板块动量数据
+- 市场统计数据
+- K线历史数据
+- 计算结果缓存
+
+## 二、缓存键设计
+
+### 1. 实时行情缓存
+
+| 缓存类型 | 缓存键格式 | 数据结构 | 过期时间 | 描述 |
+|---------|-----------|---------|---------|------|
+| 单只股票实时行情 | `realtime:stock:{code}` | Hash | 30分钟 | 存储单只股票的实时行情数据 |
+| 全市场实时行情 | `realtime:full` | String | 30分钟 | 存储全市场股票的实时行情数据(压缩) |
+| ETF实时行情 | `realtime:etf:{code}` | Hash | 30分钟 | 存储ETF的实时行情数据 |
+| 实时行情批次 | `realtime:batch` | Set | 1小时 | 记录最近更新的股票代码 |
+
+### 2. 板块数据缓存
+
+| 缓存类型 | 缓存键格式 | 数据结构 | 过期时间 | 描述 |
+|---------|-----------|---------|---------|------|
+| 板块动量 | `sector:momentum:{period}:{sort}:{order}:{limit}` | Sorted Set | 1小时 | 存储板块动量数据,按动量值排序 |
+| 板块成分股 | `sector:stocks:{sector_code}` | List | 24小时 | 存储板块的成分股列表 |
+| 板块基本信息 | `sector:info:{sector_code}` | Hash | 24小时 | 存储板块的基本信息 |
+
+### 3. 市场数据缓存
+
+| 缓存类型 | 缓存键格式 | 数据结构 | 过期时间 | 描述 |
+|---------|-----------|---------|---------|------|
+| 市场统计 | `market:stats` | Hash | 1小时 | 存储市场统计数据 |
+| 指数数据 | `market:index:{code}` | Hash | 1小时 | 存储指数的实时数据 |
+| 涨跌分布 | `market:distribution` | Hash | 1小时 | 存储市场涨跌分布数据 |
+
+### 4. K线数据缓存
+
+| 缓存类型 | 缓存键格式 | 数据结构 | 过期时间 | 描述 |
+|---------|-----------|---------|---------|------|
+| K线数据 | `kline:{code}:{period}:{days}` | List | 1天 | 存储股票的K线数据 |
+| K线数据(压缩) | `kline:compressed:{code}:{period}:{days}` | String | 1天 | 存储压缩后的K线数据 |
+
+### 5. 计算结果缓存
+
+| 缓存类型 | 缓存键格式 | 数据结构 | 过期时间 | 描述 |
+|---------|-----------|---------|---------|------|
+| 技术指标 | `indicator:{code}:{indicator}:{params}` | Hash | 1天 | 存储计算好的技术指标 |
+| 选股结果 | `stock:selection:{strategy}:{params}` | Set | 1小时 | 存储选股策略的结果 |
+| 回测结果 | `backtest:{strategy}:{params}` | Hash | 24小时 | 存储回测结果 |
+
+## 三、数据结构选择
+
+### 1. Hash
+- **适用场景**:存储结构化数据,如实时行情、板块信息
+- **优点**:
+ - 支持字段级操作,节省内存
+ - 适合存储对象类型数据
+ - 查找速度快
+- **示例**:
+ ```
+ HSET realtime:stock:600519 price 1800.00 change_pct 2.50 volume 1000000
+ HGETALL realtime:stock:600519
+ ```
+
+### 2. Sorted Set
+- **适用场景**:需要排序的数据,如板块动量排名
+- **优点**:
+ - 自动排序
+ - 支持范围查询
+ - 适合Top-N查询
+- **示例**:
+ ```
+ ZADD sector:momentum:5:desc:20 12.5 HY001 10.2 HY002 8.7 HY003
+ ZRANGE sector:momentum:5:desc:20 0 9 WITHSCORES
+ ```
+
+### 3. List
+- **适用场景**:存储有序数据,如K线数据、成分股列表
+- **优点**:
+ - 支持顺序访问
+ - 适合存储时间序列数据
+ - 支持范围查询
+- **示例**:
+ ```
+ LPUSH kline:600519:1d:30 {"date":"2023-01-01","open":1700,"high":1800,"low":1650,"close":1750}
+ LRANGE kline:600519:1d:30 0 -1
+ ```
+
+### 4. Set
+- **适用场景**:存储唯一数据,如股票代码集合
+- **优点**:
+ - 自动去重
+ - 支持集合操作
+ - 适合存储标签类数据
+- **示例**:
+ ```
+ SADD sector:stocks:HY001 600000 601398 601288
+ SMEMBERS sector:stocks:HY001
+ ```
+
+### 5. String
+- **适用场景**:存储任意数据,如全市场实时行情(压缩)
+- **优点**:
+ - 灵活存储任意格式数据
+ - 支持二进制数据
+ - 适合存储大体积数据
+- **示例**:
+ ```
+ SET realtime:full "{compressed json data}" EX 1800
+ GET realtime:full
+ ```
+
+## 四、缓存策略
+
+### 1. 写入策略
+
+#### 实时数据
+- **主动更新**:定时任务每30秒拉取全市场数据,更新Redis
+- **惰性更新**:当缓存过期时,请求触发更新
+- **批量更新**:使用Pipeline批量写入,减少网络往返
+
+#### 计算数据
+- **预计算**:定时任务提前计算板块动量、技术指标等
+- **按需计算**:首次请求时计算并缓存
+- **增量计算**:只更新变化的数据
+
+### 2. 读取策略
+
+- **优先缓存**:先从Redis读取,未命中再从数据库读取
+- **缓存预热**:服务启动时预热热门数据
+- **降级策略**:Redis不可用时,直接从数据库读取
+
+### 3. 过期策略
+
+- **时间过期**:设置合理的过期时间
+- **主动清理**:定期清理过期数据
+- **内存限制**:设置Redis最大内存,使用LRU策略
+
+### 4. 缓存一致性
+
+- **先更新数据库,后更新缓存**:确保数据一致性
+- **缓存标记失效**:更新数据库后,标记缓存失效,下次读取时更新
+- **定期全量同步**:定时全量同步数据,确保最终一致性
+
+## 五、部署方案
+
+### 1. 单节点部署
+- **适用场景**:开发/测试环境
+- **配置**:
+ - 内存:4GB+
+ - 端口:6379
+ - 持久化:RDB + AOF
+
+### 2. 主从复制
+- **适用场景**:生产环境
+- **配置**:
+ - 主节点:负责写入
+ - 从节点:负责读取
+ - 复制方式:异步复制
+
+### 3. 哨兵模式
+- **适用场景**:生产环境,需要高可用性
+- **配置**:
+ - 3个哨兵节点
+ - 自动故障转移
+ - 监控主从状态
+
+### 4. 集群模式
+- **适用场景**:大数据量场景,需要水平扩展
+- **配置**:
+ - 3个主节点,3个从节点
+ - 数据分片
+ - 自动重平衡
+
+## 六、性能优化
+
+### 1. 内存优化
+- **数据压缩**:使用Redis的压缩列表
+- **键空间优化**:使用哈希标签减少键冲突
+- **内存淘汰策略**:设置合适的内存淘汰策略
+
+### 2. 网络优化
+- **Pipeline**:批量执行命令,减少网络往返
+- **连接池**:使用连接池管理Redis连接
+- **批量操作**:使用MSET、MGET等批量命令
+
+### 3. 命令优化
+- **使用Hash存储对象**:减少键数量
+- **使用Sorted Set排序**:利用Redis内置排序
+- **避免使用KEYS命令**:使用SCAN替代
+
+### 4. 监控与调优
+- **监控指标**:
+ - 内存使用情况
+ - 命中率
+ - 命令执行时间
+ - 连接数
+- **调优参数**:
+ - maxmemory
+ - maxmemory-policy
+ - hash-max-ziplist-entries
+ - hash-max-ziplist-value
+
+## 七、缓存工具类设计
+
+### 1. 缓存操作接口
+
+```python
+class RedisCache:
+ def get(self, key):
+ """获取缓存"""
+ pass
+
+ def set(self, key, value, expire=None):
+ """设置缓存"""
+ pass
+
+ def delete(self, key):
+ """删除缓存"""
+ pass
+
+ def hash_get(self, key, field):
+ """获取Hash字段"""
+ pass
+
+ def hash_set(self, key, field, value):
+ """设置Hash字段"""
+ pass
+
+ def sorted_set_add(self, key, score, member):
+ """添加到有序集合"""
+ pass
+
+ def sorted_set_range(self, key, start, end, withscores=False):
+ """获取有序集合范围"""
+ pass
+```
+
+### 2. 缓存键生成器
+
+```python
+class CacheKeyGenerator:
+ @staticmethod
+ def get_stock_realtime_key(code):
+ return f"realtime:stock:{code}"
+
+ @staticmethod
+ def get_sector_momentum_key(period, sort, order, limit):
+ return f"sector:momentum:{period}:{sort}:{order}:{limit}"
+
+ @staticmethod
+ def get_kline_key(code, period, days):
+ return f"kline:{code}:{period}:{days}"
+```
+
+### 3. 缓存管理器
+
+```python
+class CacheManager:
+ def __init__(self, redis_client):
+ self.redis = redis_client
+ self.key_generator = CacheKeyGenerator()
+
+ def get_stock_realtime(self, code):
+ """获取股票实时行情"""
+ key = self.key_generator.get_stock_realtime_key(code)
+ return self.redis.get(key)
+
+ def set_stock_realtime(self, code, data, expire=1800):
+ """设置股票实时行情"""
+ key = self.key_generator.get_stock_realtime_key(code)
+ return self.redis.set(key, data, expire)
+
+ def get_sector_momentum(self, period, sort, order, limit):
+ """获取板块动量"""
+ key = self.key_generator.get_sector_momentum_key(period, sort, order, limit)
+ return self.redis.sorted_set_range(key, 0, limit-1, withscores=True)
+```
+
+## 八、监控与告警
+
+### 1. 监控指标
+
+| 指标 | 描述 | 阈值 |
+|------|------|------|
+| 内存使用率 | Redis内存使用百分比 | >80% |
+| 命中率 | 缓存命中次数/总请求次数 | <80% |
+| 连接数 | 当前Redis连接数 | >1000 |
+| 命令执行时间 | 平均命令执行时间 | >1ms |
+| 过期键数量 | 过期键占比 | >50% |
+
+### 2. 告警策略
+
+- **内存告警**:内存使用率超过80%时告警
+- **命中率告警**:命中率低于80%时告警
+- **连接数告警**:连接数超过1000时告警
+- **错误率告警**:命令错误率超过1%时告警
+
+### 3. 监控工具
+
+- **Prometheus**:收集Redis指标
+- **Grafana**:可视化监控面板
+- **Redis Exporter**:导出Redis指标
+- **Alertmanager**:处理告警
+
+## 九、安全考虑
+
+### 1. 访问控制
+- **密码认证**:设置Redis密码
+- **IP白名单**:限制访问IP
+- **ACL**:使用Redis 6.0+的ACL功能
+
+### 2. 数据安全
+- **数据加密**:对敏感数据进行加密
+- **定期备份**:定期备份Redis数据
+- **灾难恢复**:制定灾难恢复计划
+
+### 3. 网络安全
+- **网络隔离**:Redis服务放在内网
+- **SSL加密**:使用SSL加密传输
+- **防火墙**:配置防火墙规则
+
+## 十、扩展性考虑
+
+### 1. 数据分片
+- **水平分片**:按股票代码范围分片
+- **垂直分片**:按数据类型分片
+- **一致性哈希**:使用一致性哈希算法
+
+### 2. 读写分离
+- **主节点**:处理写入请求
+- **从节点**:处理读取请求
+- **负载均衡**:使用负载均衡器分发请求
+
+### 3. 集群扩展
+- **自动扩缩容**:根据负载自动扩缩容
+- **滚动升级**:支持无停机升级
+- **跨区域部署**:支持多区域部署
+
+## 十一、总结
+
+Redis缓存架构设计采用多种数据结构和缓存策略,为股票分析系统提供高性能、高可靠性的数据访问层。通过合理的缓存键设计、数据结构选择和缓存策略,可以显著减少对数据源和数据库的访问,提高系统响应速度,同时保证数据的一致性和可靠性。
+
+该设计不仅满足当前系统的需求,还为未来的扩展和优化预留了空间,能够应对数据量增长和业务需求变化的挑战。
\ No newline at end of file
diff --git a/scripts/create_tables.sql b/scripts/create_tables.sql
new file mode 100644
index 0000000..d8467ac
--- /dev/null
+++ b/scripts/create_tables.sql
@@ -0,0 +1,118 @@
+-- 创建数据库(如果不存在)
+CREATE DATABASE IF NOT EXISTS stock_analysis;
+
+-- 使用数据库
+USE stock_analysis;
+
+-- 股票基本信息表
+CREATE TABLE IF NOT EXISTS stock_basic (
+ code VARCHAR(10) PRIMARY KEY,
+ name VARCHAR(50) NOT NULL,
+ sector_code VARCHAR(10),
+ list_date DATE,
+ is_etf BOOLEAN DEFAULT FALSE,
+ market VARCHAR(10),
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 股票实时行情表
+CREATE TABLE IF NOT EXISTS stock_quote (
+ code VARCHAR(10) PRIMARY KEY,
+ price DECIMAL(10,2),
+ change_pct DECIMAL(6,2),
+ change_amount DECIMAL(10,2),
+ volume BIGINT,
+ amount DECIMAL(18,2),
+ turnover_rate DECIMAL(8,4),
+ amplitude DECIMAL(6,2),
+ high DECIMAL(10,2),
+ low DECIMAL(10,2),
+ open DECIMAL(10,2),
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 股票K线数据表
+CREATE TABLE IF NOT EXISTS stock_kline (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ code VARCHAR(10) NOT NULL,
+ date DATE NOT NULL,
+ open DECIMAL(10,2),
+ high DECIMAL(10,2),
+ low DECIMAL(10,2),
+ close DECIMAL(10,2),
+ volume BIGINT,
+ amount DECIMAL(18,2),
+ pct_chg DECIMAL(6,2),
+ INDEX idx_code_date (code, date)
+);
+
+-- 板块信息表
+CREATE TABLE IF NOT EXISTS sector_info (
+ code VARCHAR(10) PRIMARY KEY,
+ name VARCHAR(50) NOT NULL,
+ stock_count INT DEFAULT 0,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 板块动量表
+CREATE TABLE IF NOT EXISTS sector_momentum (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ sector_code VARCHAR(10) NOT NULL,
+ date DATE NOT NULL,
+ momentum_value DECIMAL(10,4),
+ n INT,
+ N INT,
+ rank INT,
+ rank_change INT,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ INDEX idx_sector_date (sector_code, date)
+);
+
+-- 市场统计表
+CREATE TABLE IF NOT EXISTS market_stats (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT,
+ date DATE NOT NULL UNIQUE,
+ up_count INT DEFAULT 0,
+ down_count INT DEFAULT 0,
+ flat_count INT DEFAULT 0,
+ limit_up_count INT DEFAULT 0,
+ limit_down_count INT DEFAULT 0,
+ total_amount DECIMAL(20,2),
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 创建外键约束(可选)
+-- ALTER TABLE stock_basic ADD FOREIGN KEY (sector_code) REFERENCES sector_info(code);
+-- ALTER TABLE sector_momentum ADD FOREIGN KEY (sector_code) REFERENCES sector_info(code);
+
+-- 创建索引
+CREATE INDEX IF NOT EXISTS idx_stock_basic_sector ON stock_basic(sector_code);
+CREATE INDEX IF NOT EXISTS idx_sector_momentum_rank ON sector_momentum(rank);
+CREATE INDEX IF NOT EXISTS idx_market_stats_date ON market_stats(date);
+
+-- 插入示例数据
+-- 板块信息
+INSERT IGNORE INTO sector_info (code, name, stock_count) VALUES
+('HY001', '银行', 42),
+('HY002', '证券', 48),
+('HY003', '保险', 10),
+('HY004', '房地产', 130),
+('HY005', '医药', 300),
+('HY006', '科技', 500),
+('HY007', '消费', 200),
+('HY008', '能源', 80),
+('HY009', '材料', 150),
+('HY010', '工业', 250);
+
+-- 股票基本信息(示例)
+INSERT IGNORE INTO stock_basic (code, name, sector_code, is_etf, market) VALUES
+('600000', '浦发银行', 'HY001', FALSE, '沪市'),
+('600519', '贵州茅台', 'HY007', FALSE, '沪市'),
+('000001', '平安银行', 'HY001', FALSE, '深市'),
+('000858', '五粮液', 'HY007', FALSE, '深市'),
+('510300', '沪深300ETF', 'HY006', TRUE, '沪市'),
+('510500', '500ETF', 'HY006', TRUE, '沪市');
+
+-- 市场统计(示例)
+INSERT IGNORE INTO market_stats (date, up_count, down_count, flat_count, limit_up_count, limit_down_count, total_amount) VALUES
+(CURDATE(), 2500, 2000, 500, 100, 20, 1500000000000);
diff --git a/src/config.py b/src/config.py
index 7d1506a..f23637c 100644
--- a/src/config.py
+++ b/src/config.py
@@ -59,6 +59,13 @@ class Config:
# === 数据源 API Token ===
tushare_token: Optional[str] = None
+ # === AmazingData 自定义数据源配置 ===
+ amazingdata_base_url: Optional[str] = None
+ amazingdata_username: Optional[str] = None
+ amazingdata_password: Optional[str] = None
+ amazingdata_host: Optional[str] = None
+ amazingdata_port: Optional[int] = None
+
# === AI 分析配置 ===
gemini_api_key: Optional[str] = None
gemini_model: str = "gemini-3-flash-preview" # 主模型
@@ -355,6 +362,11 @@ class Config:
feishu_app_secret=os.getenv('FEISHU_APP_SECRET'),
feishu_folder_token=os.getenv('FEISHU_FOLDER_TOKEN'),
tushare_token=os.getenv('TUSHARE_TOKEN'),
+ amazingdata_base_url=os.getenv('AMAZINGDATA_BASE_URL'),
+ amazingdata_username=os.getenv('AMAZINGDATA_USERNAME'),
+ amazingdata_password=os.getenv('AMAZINGDATA_PASSWORD'),
+ amazingdata_host=os.getenv('AMAZINGDATA_HOST'),
+ amazingdata_port=int(os.getenv('AMAZINGDATA_PORT', '8600')) if os.getenv('AMAZINGDATA_PORT') else None,
gemini_api_key=os.getenv('GEMINI_API_KEY'),
gemini_model=os.getenv('GEMINI_MODEL', 'gemini-3-flash-preview'),
gemini_model_fallback=os.getenv('GEMINI_MODEL_FALLBACK', 'gemini-2.5-flash'),
diff --git a/src/services/market_service.py b/src/services/market_service.py
new file mode 100644
index 0000000..a1a9a8f
--- /dev/null
+++ b/src/services/market_service.py
@@ -0,0 +1,912 @@
+# -*- coding: utf-8 -*-
+"""
+===================================
+Market Service Layer
+===================================
+
+Responsibilities:
+1. Provide market indices data
+2. Provide sector momentum data
+3. Provide stock momentum recommendations
+4. Provide new high/low stocks
+5. Provide price distribution
+6. Provide K-line chart data
+"""
+
+import logging
+import random
+import json
+from datetime import datetime, timedelta
+from typing import List, Optional, Dict, Any
+
+from api.v1.schemas.market import (
+ MarketIndex,
+ UpDownStats,
+ SectorMomentum,
+ StockMomentum,
+ NewHighLowStock,
+ PriceDistribution,
+ PriceDistributionResponse,
+ SentimentIndicator,
+ KLineChartResponse,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class MarketService:
+ """
+ Market data service
+
+ Provides market overview, sector momentum, stock recommendations, etc.
+ """
+
+ MAJOR_INDICES = [
+ {"code": "000001.SH", "name": "上证指数"},
+ {"code": "399001.SZ", "name": "深证成指"},
+ {"code": "399006.SZ", "name": "创业板指"},
+ {"code": "000698.SH", "name": "科创50"},
+ ]
+
+ SECTOR_NAMES = {
+ "BK0001": "计算机",
+ "BK0002": "通信",
+ "BK0003": "电子",
+ "BK0004": "医药生物",
+ "BK0005": "食品饮料",
+ "BK0006": "家电",
+ "BK0007": "汽车",
+ "BK0008": "机械设备",
+ "BK0009": "化工",
+ "BK0010": "有色金属",
+ "BK0011": "钢铁",
+ "BK0012": "建筑材料",
+ "BK0013": "房地产",
+ "BK0014": "银行",
+ "BK0015": "非银金融",
+ "BK0016": "传媒",
+ "BK0017": "互联网",
+ "BK0018": "商贸零售",
+ "BK0019": "公用事业",
+ "BK0020": "纺织服饰",
+ }
+
+ # 板块股票数量映射(模拟数据)
+ SECTOR_STOCK_COUNT = {
+ "BK0001": 250, # 计算机
+ "BK0002": 80, # 通信
+ "BK0003": 300, # 电子
+ "BK0004": 350, # 医药生物
+ "BK0005": 120, # 食品饮料
+ "BK0006": 60, # 家电
+ "BK0007": 150, # 汽车
+ "BK0008": 200, # 机械设备
+ "BK0009": 180, # 化工
+ "BK0010": 100, # 有色金属
+ "BK0011": 50, # 钢铁
+ "BK0012": 70, # 建筑材料
+ "BK0013": 120, # 房地产
+ "BK0014": 40, # 银行
+ "BK0015": 80, # 非银金融
+ "BK0016": 100, # 传媒
+ "BK0017": 50, # 互联网
+ "BK0018": 90, # 商贸零售
+ "BK0019": 60, # 公用事业
+ "BK0020": 80, # 纺织服饰
+ }
+
+ CACHE_KEYS = {
+ "sector_momentum": "sector_momentum",
+ "sector_ranking": "sector_ranking",
+ "stock_momentum": "stock_momentum",
+ }
+
+ CACHE_EXPIRY = 3600 # 1小时
+
+ def __init__(self):
+ """Initialize market service"""
+ self._cache: Dict[str, Any] = {}
+ self._cache_time: Optional[Dict[str, float]] = {}
+ self._previous_sector_data: Dict[str, Any] = {}
+
+ def get_indices(self) -> List[MarketIndex]:
+ """
+ Get major market indices
+
+ Returns list of major indices with current prices
+ """
+ try:
+ indices = []
+ for idx in self.MAJOR_INDICES:
+ quote = self._get_index_quote(idx["code"])
+ indices.append(MarketIndex(
+ code=idx["code"],
+ name=idx["name"],
+ price=quote.get("price", 0.0),
+ change=quote.get("change"),
+ change_percent=quote.get("change_percent"),
+ volume=quote.get("volume"),
+ amount=quote.get("amount")
+ ))
+ return indices
+ except Exception as e:
+ logger.error(f"Failed to get indices: {e}", exc_info=True)
+ return []
+
+ def get_updown_stats(self) -> UpDownStats:
+ """
+ Get up/down statistics
+
+ Returns market breadth statistics
+ """
+ try:
+ stats = self._fetch_updown_stats()
+ return UpDownStats(
+ up_count=stats.get("up_count", 0),
+ down_count=stats.get("down_count", 0),
+ flat_count=stats.get("flat_count", 0),
+ limit_up_count=stats.get("limit_up_count", 0),
+ limit_down_count=stats.get("limit_down_count", 0),
+ total_count=stats.get("total_count", 0)
+ )
+ except Exception as e:
+ logger.error(f"Failed to get updown stats: {e}", exc_info=True)
+ return UpDownStats(
+ up_count=0, down_count=0, flat_count=0,
+ limit_up_count=0, limit_down_count=0, total_count=0
+ )
+
+ def get_price_distribution(self) -> PriceDistributionResponse:
+ """
+ Get price distribution
+
+ Returns distribution of stocks across price ranges
+ """
+ try:
+ distribution = self._fetch_price_distribution()
+ items = [
+ PriceDistribution(
+ range_label=item.get("range_label", ""),
+ count=item.get("count", 0),
+ percent=item.get("percent", 0.0)
+ )
+ for item in distribution
+ ]
+ total_count = sum(item.count for item in items)
+ return PriceDistributionResponse(items=items, total_count=total_count)
+ except Exception as e:
+ logger.error(f"Failed to get price distribution: {e}", exc_info=True)
+ return PriceDistributionResponse(items=[], total_count=0)
+
+ def get_sentiment_indicators(self) -> List[SentimentIndicator]:
+ """
+ Get sentiment indicators
+
+ Returns market sentiment data
+ """
+ try:
+ indicators = self._fetch_sentiment()
+ return [
+ SentimentIndicator(
+ name=ind.get("name", ""),
+ value=ind.get("value", 0.0),
+ change=ind.get("change"),
+ level=ind.get("level"),
+ description=ind.get("description")
+ )
+ for ind in indicators
+ ]
+ except Exception as e:
+ logger.error(f"Failed to get sentiment: {e}", exc_info=True)
+ return []
+
+ def get_sector_momentum(
+ self,
+ sort: str = "momentumScore",
+ order: str = "desc",
+ limit: int = 20,
+ period: int = 5 # 5日涨跌幅
+ ) -> List[SectorMomentum]:
+ """
+ Get sector momentum ranking
+
+ Args:
+ sort: Sort field
+ order: Sort order
+ limit: Limit count
+ period: Period for calculating momentum (days)
+
+ Returns:
+ List of sector momentum data
+ """
+ try:
+ # 尝试从缓存获取
+ cache_key = f"{self.CACHE_KEYS['sector_momentum']}_{period}"
+ cached = self._get_from_cache(cache_key)
+ if cached:
+ return cached
+
+ # 计算板块动量
+ sectors = self._calculate_sector_momentum(period)
+
+ if sort == "momentumScore":
+ sectors.sort(key=lambda x: x.get("momentum_score", 0), reverse=(order == "desc"))
+ elif sort == "momentumValue":
+ sectors.sort(key=lambda x: x.get("momentum_value", 0), reverse=(order == "desc"))
+ elif sort == "changePercent":
+ sectors.sort(key=lambda x: x.get("change_percent", 0), reverse=(order == "desc"))
+ elif sort == "rankChange":
+ sectors.sort(key=lambda x: x.get("rank_change", 0), reverse=(order == "desc"))
+
+ sectors = sectors[:limit]
+
+ # 转换为模型
+ result = [
+ SectorMomentum(
+ code=s.get("code", ""),
+ name=s.get("name", ""),
+ momentum_score=s.get("momentum_score", 0.0),
+ change_percent=s.get("change_percent", 0.0),
+ turnover_rate=s.get("turnover_rate"),
+ leading_stock=s.get("leading_stock"),
+ leading_stock_name=s.get("leading_stock_name"),
+ momentum_value=s.get("momentum_value"),
+ momentum_value_change=s.get("momentum_value_change"),
+ rank=s.get("rank"),
+ rank_change=s.get("rank_change")
+ )
+ for s in sectors
+ ]
+
+ # 缓存结果
+ self._set_to_cache(cache_key, result)
+
+ return result
+ except Exception as e:
+ logger.error(f"Failed to get sector momentum: {e}", exc_info=True)
+ return []
+
+ def _calculate_sector_momentum(self, period: int = 5) -> List[Dict[str, Any]]:
+ """
+ Calculate sector momentum based on the specified algorithm
+
+ Algorithm:
+ 1. Get all stocks with their price change percentage for the period
+ 2. Sort by change percentage
+ 3. Take top 16% as momentum stocks
+ 4. Group by sector
+ 5. Calculate momentum value = (n²) / N, where:
+ - n: number of momentum stocks in sector
+ - N: total stocks in sector
+ 6. Calculate momentum value change and rank change
+ """
+ # Step 1: Get all stocks with their change percentages
+ all_stocks = self._fetch_all_stocks_with_change(period)
+
+ if not all_stocks:
+ return self._generate_mock_sector_momentum()
+
+ # Step 2: Sort by change percentage (descending)
+ all_stocks.sort(key=lambda x: x.get("change_percent", 0), reverse=True)
+
+ # Step 3: Take top 16%
+ top_16_percent_count = max(1, int(len(all_stocks) * 0.16))
+ momentum_stocks = all_stocks[:top_16_percent_count]
+
+ # Step 4: Group by sector
+ sector_momentum_map = {}
+ for stock in momentum_stocks:
+ sector = stock.get("sector")
+ if sector:
+ if sector not in sector_momentum_map:
+ sector_momentum_map[sector] = []
+ sector_momentum_map[sector].append(stock)
+
+ # Step 5: Calculate momentum value for each sector
+ sector_data = []
+ for sector_code, stocks in sector_momentum_map.items():
+ n = len(stocks)
+ N = self.SECTOR_STOCK_COUNT.get(sector_code, 1) # Default to 1 to avoid division by zero
+ momentum_value = (n ** 2) / N if N > 0 else 0
+
+ sector_info = {
+ "code": sector_code,
+ "name": self.SECTOR_NAMES.get(sector_code, sector_code),
+ "momentum_value": round(momentum_value, 4),
+ "n": n, # Number of momentum stocks in sector
+ "N": N, # Total stocks in sector
+ "stocks": stocks
+ }
+ sector_data.append(sector_info)
+
+ # Add sectors with no momentum stocks
+ for sector_code, sector_name in self.SECTOR_NAMES.items():
+ if sector_code not in sector_momentum_map:
+ N = self.SECTOR_STOCK_COUNT.get(sector_code, 1)
+ sector_data.append({
+ "code": sector_code,
+ "name": sector_name,
+ "momentum_value": 0.0,
+ "n": 0,
+ "N": N,
+ "stocks": []
+ })
+
+ # Step 6: Sort by momentum value (descending)
+ sector_data.sort(key=lambda x: x.get("momentum_value", 0), reverse=True)
+
+ # Step 7: Calculate ranks and changes
+ current_ranking = {sector["code"]: i + 1 for i, sector in enumerate(sector_data)}
+
+ # Get previous data
+ previous_data = self._previous_sector_data.get(period, {})
+ previous_ranking = previous_data.get("ranking", {})
+ previous_momentum = previous_data.get("momentum", {})
+
+ # Calculate changes and add additional fields
+ for i, sector in enumerate(sector_data):
+ sector_code = sector["code"]
+ current_rank = i + 1
+ previous_rank = previous_ranking.get(sector_code, current_rank)
+ rank_change = previous_rank - current_rank # Positive means rank improved
+
+ current_momentum = sector["momentum_value"]
+ previous_momentum_value = previous_momentum.get(sector_code, current_momentum)
+ momentum_value_change = current_momentum - previous_momentum_value
+
+ # Calculate additional metrics
+ if sector["stocks"]:
+ avg_change = sum(s.get("change_percent", 0) for s in sector["stocks"]) / len(sector["stocks"])
+ leading_stock = max(sector["stocks"], key=lambda s: s.get("change_percent", 0))
+ turnover_rate = random.uniform(1, 10)
+ else:
+ avg_change = 0
+ leading_stock = None
+ turnover_rate = random.uniform(0.5, 3)
+
+ sector.update({
+ "rank": current_rank,
+ "rank_change": rank_change,
+ "momentum_value_change": round(momentum_value_change, 4),
+ "momentum_score": round(sector["momentum_value"] * 100, 2),
+ "change_percent": round(avg_change, 2),
+ "turnover_rate": round(turnover_rate, 2),
+ "leading_stock": leading_stock.get("code") if leading_stock else None,
+ "leading_stock_name": leading_stock.get("name") if leading_stock else None
+ })
+
+ # Update previous data for next calculation
+ self._previous_sector_data[period] = {
+ "ranking": current_ranking,
+ "momentum": {sector["code"]: sector["momentum_value"] for sector in sector_data},
+ "timestamp": datetime.now().isoformat()
+ }
+
+ return sector_data
+
+ def _fetch_all_stocks_with_change(self, period: int = 5) -> List[Dict[str, Any]]:
+ """
+ Fetch all stocks with their price change percentage for the specified period
+
+ Args:
+ period: Number of days for calculating change
+
+ Returns:
+ List of stocks with change percentage and sector
+ """
+ # In a real implementation, this would fetch data from a data provider
+ # For now, we'll generate mock data
+
+ stocks = []
+ sector_codes = list(self.SECTOR_NAMES.keys())
+
+ # Generate mock stocks for each sector
+ for sector_code in sector_codes:
+ stock_count = self.SECTOR_STOCK_COUNT.get(sector_code, 50)
+
+ for i in range(stock_count):
+ stock = {
+ "code": f"{random.randint(600000, 605000) if random.random() > 0.5 else random.randint(0, 300000):06d}",
+ "name": f"{self.SECTOR_NAMES.get(sector_code, sector_code)}股票{i+1}",
+ "change_percent": random.uniform(-10, 15), # Random change percentage
+ "sector": sector_code
+ }
+ stocks.append(stock)
+
+ return stocks
+
+ def get_stock_momentum_recommendation(self, limit: int = 15) -> List[StockMomentum]:
+ """
+ Get stock momentum recommendations
+
+ Args:
+ limit: Limit count
+
+ Returns:
+ List of recommended stocks
+ """
+ try:
+ cache_key = self.CACHE_KEYS['stock_momentum']
+ cached = self._get_from_cache(cache_key)
+ if cached:
+ return cached[:limit]
+
+ stocks = self._fetch_stock_momentum()
+ stocks = stocks[:limit]
+
+ result = [
+ StockMomentum(
+ code=s.get("code", ""),
+ name=s.get("name", ""),
+ momentum_score=s.get("momentum_score", 0.0),
+ change_percent=s.get("change_percent", 0.0),
+ sector=s.get("sector"),
+ recommendation=s.get("recommendation")
+ )
+ for s in stocks
+ ]
+
+ self._set_to_cache(cache_key, result)
+ return result
+ except Exception as e:
+ logger.error(f"Failed to get stock momentum: {e}", exc_info=True)
+ return []
+
+ def get_new_high_stocks(self, days: int = 20, limit: int = 20) -> List[NewHighLowStock]:
+ """
+ Get stocks that reached new high
+
+ Args:
+ days: Days to look back
+ limit: Limit count
+
+ Returns:
+ List of new high stocks
+ """
+ try:
+ stocks = self._fetch_new_high_low("high", days)
+ stocks = stocks[:limit]
+
+ return [
+ NewHighLowStock(
+ code=s.get("code", ""),
+ name=s.get("name", ""),
+ price=s.get("price", 0.0),
+ change_percent=s.get("change_percent", 0.0),
+ days_to_high=s.get("days_to_high"),
+ high_date=s.get("high_date")
+ )
+ for s in stocks
+ ]
+ except Exception as e:
+ logger.error(f"Failed to get new high stocks: {e}", exc_info=True)
+ return []
+
+ def get_new_low_stocks(self, days: int = 20, limit: int = 20) -> List[NewHighLowStock]:
+ """
+ Get stocks that reached new low
+
+ Args:
+ days: Days to look back
+ limit: Limit count
+
+ Returns:
+ List of new low stocks
+ """
+ try:
+ stocks = self._fetch_new_high_low("low", days)
+ stocks = stocks[:limit]
+
+ return [
+ NewHighLowStock(
+ code=s.get("code", ""),
+ name=s.get("name", ""),
+ price=s.get("price", 0.0),
+ change_percent=s.get("change_percent", 0.0),
+ days_to_low=s.get("days_to_low"),
+ low_date=s.get("low_date")
+ )
+ for s in stocks
+ ]
+ except Exception as e:
+ logger.error(f"Failed to get new low stocks: {e}", exc_info=True)
+ return []
+
+ def get_stock_kline_chart(self, stock_code: str, days: int = 60) -> KLineChartResponse:
+ """
+ Get stock K-line chart data for ECharts
+
+ Args:
+ stock_code: Stock code
+ days: Number of days
+
+ Returns:
+ K-line chart data formatted for ECharts
+ """
+ try:
+ kline_data = self._fetch_kline_data(stock_code, days)
+
+ if not kline_data:
+ return KLineChartResponse(
+ categoryData=[], values=[], volumes=[], stock_name=None
+ )
+
+ category_data = []
+ values = []
+ volumes = []
+
+ for i, item in enumerate(kline_data):
+ category_data.append(item.get("date", ""))
+ values.append([
+ item.get("open", 0.0),
+ item.get("close", 0.0),
+ item.get("low", 0.0),
+ item.get("high", 0.0),
+ item.get("volume", 0.0)
+ ])
+
+ change_sign = 1 if item.get("close", 0) >= item.get("open", 0) else -1
+ volumes.append([i, item.get("volume", 0.0), change_sign])
+
+ stock_name = self._get_stock_name(stock_code)
+
+ return KLineChartResponse(
+ categoryData=category_data,
+ values=values,
+ volumes=volumes,
+ stock_name=stock_name
+ )
+ except Exception as e:
+ logger.error(f"Failed to get stock kline: {e}", exc_info=True)
+ return KLineChartResponse(
+ categoryData=[], values=[], volumes=[], stock_name=None
+ )
+
+ def get_sector_kline(self, sector_code: str, days: int = 60) -> KLineChartResponse:
+ """
+ Get sector K-line chart
+
+ Args:
+ sector_code: Sector code
+ days: Number of days
+
+ Returns:
+ K-line chart data
+ """
+ try:
+ kline_data = self._fetch_sector_kline_data(sector_code, days)
+
+ if not kline_data:
+ return KLineChartResponse(
+ categoryData=[], values=[], volumes=[], stock_name=None
+ )
+
+ category_data = []
+ values = []
+ volumes = []
+
+ for i, item in enumerate(kline_data):
+ category_data.append(item.get("date", ""))
+ values.append([
+ item.get("open", 0.0),
+ item.get("close", 0.0),
+ item.get("low", 0.0),
+ item.get("high", 0.0),
+ item.get("volume", 0.0)
+ ])
+
+ change_sign = 1 if item.get("close", 0) >= item.get("open", 0) else -1
+ volumes.append([i, item.get("volume", 0.0), change_sign])
+
+ sector_name = self.SECTOR_NAMES.get(sector_code, sector_code)
+
+ return KLineChartResponse(
+ categoryData=category_data,
+ values=values,
+ volumes=volumes,
+ stock_name=sector_name
+ )
+ except Exception as e:
+ logger.error(f"Failed to get sector kline: {e}", exc_info=True)
+ return KLineChartResponse(
+ categoryData=[], values=[], volumes=[], stock_name=None
+ )
+
+ def _get_index_quote(self, code: str) -> Dict[str, Any]:
+ """Get index quote from data provider"""
+ try:
+ from data_provider.base import DataFetcherManager
+ manager = DataFetcherManager()
+ quote = manager.get_realtime_quote(code)
+ if quote:
+ return {
+ "price": getattr(quote, "price", 0.0) or 0.0,
+ "change": getattr(quote, "change_amount", None),
+ "change_percent": getattr(quote, "change_pct", None),
+ "volume": getattr(quote, "volume", None),
+ "amount": getattr(quote, "amount", None),
+ }
+ except Exception as e:
+ logger.warning(f"Failed to get index quote for {code}: {e}")
+
+ return self._generate_mock_index_quote(code)
+
+ def _generate_mock_index_quote(self, code: str) -> Dict[str, Any]:
+ """Generate mock index quote for testing"""
+ base_prices = {
+ "000001.SH": 3200.0,
+ "399001.SZ": 10500.0,
+ "399006.SZ": 2100.0,
+ "000698.SH": 950.0,
+ }
+ base_price = base_prices.get(code, 1000.0)
+ change_pct = random.uniform(-1.5, 2.0)
+ change = base_price * change_pct / 100
+
+ return {
+ "price": base_price + change,
+ "change": change,
+ "change_percent": change_pct,
+ "volume": random.randint(100000000, 500000000),
+ "amount": random.randint(10000000000, 50000000000),
+ }
+
+ def _fetch_updown_stats(self) -> Dict[str, Any]:
+ """Fetch up/down statistics"""
+ try:
+ from data_provider.akshare_fetcher import AkshareFetcher
+ fetcher = AkshareFetcher()
+ stats = fetcher.get_market_updown_stats()
+ if stats:
+ return stats
+ except Exception as e:
+ logger.warning(f"Failed to fetch updown stats: {e}")
+
+ return self._generate_mock_updown_stats()
+
+ def _generate_mock_updown_stats(self) -> Dict[str, Any]:
+ """Generate mock up/down stats"""
+ total = 5000
+ up = random.randint(1500, 3500)
+ down = total - up - random.randint(100, 300)
+ flat = total - up - down
+
+ return {
+ "up_count": up,
+ "down_count": down,
+ "flat_count": flat,
+ "limit_up_count": random.randint(10, 50),
+ "limit_down_count": random.randint(1, 9),
+ "total_count": total,
+ }
+
+ def _fetch_price_distribution(self) -> List[Dict[str, Any]]:
+ """Fetch price distribution"""
+ ranges = [
+ ("<10元", 0, 10),
+ ("10-20元", 10, 20),
+ ("20-50元", 20, 50),
+ ("50-100元", 50, 100),
+ (">100元", 100, 10000),
+ ]
+
+ total = 5000
+ distribution = []
+ remaining = total
+
+ for i, (label, low, high) in enumerate(ranges):
+ if i == len(ranges) - 1:
+ count = remaining
+ else:
+ count = random.randint(500, 1500)
+ remaining -= count
+ percent = round(count / total * 100, 1)
+ distribution.append({
+ "range_label": label,
+ "count": count,
+ "percent": percent
+ })
+
+ return distribution
+
+ def _fetch_sentiment(self) -> List[Dict[str, Any]]:
+ """Fetch sentiment indicators"""
+ return [
+ {
+ "name": "市场宽度",
+ "value": random.uniform(40, 60),
+ "change": random.uniform(-5, 5),
+ "level": "medium",
+ "description": "上涨股票占比"
+ },
+ {
+ "name": "量比指标",
+ "value": random.uniform(0.8, 1.5),
+ "change": random.uniform(-0.2, 0.2),
+ "level": "medium",
+ "description": "今日成交量/5日平均成交量"
+ },
+ {
+ "name": "涨停数",
+ "value": random.randint(20, 80),
+ "change": random.randint(-10, 10),
+ "level": "medium",
+ "description": "涨停股票数量"
+ },
+ ]
+
+ def _generate_mock_sector_momentum(self) -> List[Dict[str, Any]]:
+ """Generate mock sector momentum data"""
+ sectors = []
+ for code, name in self.SECTOR_NAMES.items():
+ n = random.randint(0, 20)
+ N = self.SECTOR_STOCK_COUNT.get(code, 100)
+ momentum_value = (n ** 2) / N if N > 0 else 0
+
+ # Calculate rank change (mock)
+ rank = random.randint(1, len(self.SECTOR_NAMES))
+ rank_change = random.randint(-5, 5)
+
+ sectors.append({
+ "code": code,
+ "name": name,
+ "momentum_value": round(momentum_value, 4),
+ "momentum_value_change": round(random.uniform(-0.5, 1.0), 4),
+ "momentum_score": round(momentum_value * 100, 2),
+ "change_percent": round(random.uniform(-3, 8), 2),
+ "turnover_rate": round(random.uniform(1, 10), 2),
+ "leading_stock": f"{random.randint(30, 605000):06d}",
+ "leading_stock_name": f"龙头股{random.randint(1, 100)}",
+ "rank": rank,
+ "rank_change": rank_change,
+ "n": n,
+ "N": N,
+ "stocks": []
+ })
+
+ # Sort by momentum value
+ sectors.sort(key=lambda x: x.get("momentum_value", 0), reverse=True)
+
+ # Update ranks based on sorted order
+ for i, sector in enumerate(sectors):
+ sector["rank"] = i + 1
+
+ return sectors
+
+ def _fetch_sector_momentum(self) -> List[Dict[str, Any]]:
+ """Fetch sector momentum data"""
+ return self._generate_mock_sector_momentum()
+
+ def _fetch_stock_momentum(self) -> List[Dict[str, Any]]:
+ """Fetch stock momentum recommendations"""
+ stocks = []
+ for i in range(30):
+ code = f"{random.randint(30, 605000):06d}"
+ momentum_score = random.uniform(50, 95)
+ change_pct = random.uniform(-3, 8)
+ recommendation = "buy" if momentum_score > 80 else "watch" if momentum_score > 70 else "hold"
+
+ stocks.append({
+ "code": code,
+ "name": f"股票{code}",
+ "momentum_score": round(momentum_score, 2),
+ "change_percent": round(change_pct, 2),
+ "sector": random.choice(list(self.SECTOR_NAMES.values())),
+ "recommendation": recommendation,
+ })
+
+ stocks.sort(key=lambda x: x.get("momentum_score", 0), reverse=True)
+ return stocks
+
+ def _fetch_new_high_low(self, type: str, days: int) -> List[Dict[str, Any]]:
+ """Fetch new high/low stocks"""
+ stocks = []
+ for i in range(30):
+ code = f"{random.randint(30, 605000):06d}"
+ price = random.uniform(10, 100)
+ change_pct = random.uniform(1, 10) if type == "high" else random.uniform(-1, -10)
+
+ stocks.append({
+ "code": code,
+ "name": f"股票{code}",
+ "price": round(price, 2),
+ "change_percent": round(change_pct, 2),
+ "days_to_high": random.randint(1, days) if type == "high" else None,
+ "days_to_low": random.randint(1, days) if type == "low" else None,
+ "high_date": datetime.now().strftime("%Y-%m-%d") if type == "high" else None,
+ "low_date": datetime.now().strftime("%Y-%m-%d") if type == "low" else None,
+ })
+
+ stocks.sort(key=lambda x: abs(x.get("change_percent", 0)), reverse=True)
+ return stocks
+
+ def _fetch_kline_data(self, stock_code: str, days: int) -> List[Dict[str, Any]]:
+ """Fetch K-line data for stock"""
+ try:
+ from data_provider.base import DataFetcherManager
+ manager = DataFetcherManager()
+ df, source = manager.get_daily_data(stock_code, days=days)
+
+ if df is not None and not df.empty:
+ kline = []
+ for _, row in df.iterrows():
+ date_val = row.get("date")
+ if hasattr(date_val, "strftime"):
+ date_str = date_val.strftime("%Y-%m-%d")
+ else:
+ date_str = str(date_val)
+
+ kline.append({
+ "date": date_str,
+ "open": float(row.get("open", 0)),
+ "close": float(row.get("close", 0)),
+ "high": float(row.get("high", 0)),
+ "low": float(row.get("low", 0)),
+ "volume": float(row.get("volume", 0) or 0),
+ })
+ return kline
+ except Exception as e:
+ logger.warning(f"Failed to fetch kline for {stock_code}: {e}")
+
+ return self._generate_mock_kline(days)
+
+ def _fetch_sector_kline_data(self, sector_code: str, days: int) -> List[Dict[str, Any]]:
+ """Fetch K-line data for sector"""
+ return self._generate_mock_kline(days)
+
+ def _generate_mock_kline(self, days: int) -> List[Dict[str, Any]]:
+ """Generate mock K-line data"""
+ kline = []
+ base_price = random.uniform(10, 100)
+
+ for i in range(days):
+ date = datetime.now() - timedelta(days=days - i - 1)
+ change = random.uniform(-0.05, 0.05)
+ open_price = base_price
+ close_price = base_price * (1 + change)
+ high_price = max(open_price, close_price) * (1 + random.uniform(0, 0.02))
+ low_price = min(open_price, close_price) * (1 - random.uniform(0, 0.02))
+
+ kline.append({
+ "date": date.strftime("%Y-%m-%d"),
+ "open": round(open_price, 2),
+ "close": round(close_price, 2),
+ "high": round(high_price, 2),
+ "low": round(low_price, 2),
+ "volume": random.randint(100000, 10000000),
+ })
+
+ base_price = close_price
+
+ return kline
+
+ def _get_stock_name(self, stock_code: str) -> Optional[str]:
+ """Get stock name"""
+ try:
+ from data_provider.base import DataFetcherManager
+ manager = DataFetcherManager()
+ return manager.get_stock_name(stock_code)
+ except Exception as e:
+ logger.warning(f"Failed to get stock name for {stock_code}: {e}")
+ return f"股票{stock_code}"
+
+ def _get_from_cache(self, key: str) -> Optional[Any]:
+ """Get data from cache"""
+ if key in self._cache:
+ timestamp = self._cache_time.get(key, 0)
+ if time.time() - timestamp < self.CACHE_EXPIRY:
+ return self._cache[key]
+ else:
+ # Cache expired
+ del self._cache[key]
+ del self._cache_time[key]
+ return None
+
+ def _set_to_cache(self, key: str, value: Any) -> None:
+ """Set data to cache"""
+ self._cache[key] = value
+ if self._cache_time is None:
+ self._cache_time = {}
+ self._cache_time[key] = time.time()
+
+import time
\ No newline at end of file