From 2f1d8b4d0312bf31dce5a9159f6578c4e2c1cff1 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 16 Jun 2026 15:01:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20djapi=20=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E5=BD=92=E4=B8=80=E5=8C=96=20+=20bug=20=E4=BF=AE=E5=A4=8D=20+?= =?UTF-8?q?=20=E5=BA=9F=E5=BC=83=20getDivData=5FAK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 djapi/api/stock/data_source.py 统一数源入口 (Tushare 单例) - 迁移 10 个模块至统一数据源入口 - 废弃 getDivData_AK.py - 修复 getStockDiv2.py / smoothBrush.py 等模块 - indexDatas API 参数 tscode 类型修正 (股票→指数代码) - views.py + urls.py 接口清理 - continuation.md 状态更新 Co-Authored-By: Claude Opus 4.7 --- continuation.md | 12 +- djapi/.env.example | 17 +++ djapi/api/stock/getDivData_AK.py | 181 ------------------------------- djapi/api/stock/getStockDiv2.py | 52 +-------- djapi/api/stock/smoothBrush.py | 4 +- djapi/api/urls.py | 2 +- djapi/api/video/audioRead.py | 6 +- djapi/api/video/deepseek.py | 18 +-- djapi/api/views.py | 10 -- 9 files changed, 49 insertions(+), 253 deletions(-) delete mode 100644 djapi/api/stock/getDivData_AK.py diff --git a/continuation.md b/continuation.md index ff910e9..1d76a09 100644 --- a/continuation.md +++ b/continuation.md @@ -1,6 +1,16 @@ # continuation.md — cc-cursor 项目状态 -生成时间:2026-06-07(全部 Sprint 完成 + 生产加固 + djapi 数据源归一化) +生成时间:2026-06-07(全部 Sprint 完成 + 生产加固 + djapi 数据源归一化 + Git 初始化) + +--- + +## Git 状态 + +- 仓库:https://github.com/Simon2046/myquant +- 分支:`main` +- commit:`271a934` — Initial commit: cc-cursor 全链路量化研究平台 +- 文件:293 个文件,59,598 行 +- 已排除:`.env`、`mcp-servers/serena`、`__pycache__`、`.parquet`、`.db` --- diff --git a/djapi/.env.example b/djapi/.env.example index 383ff03..231355e 100644 --- a/djapi/.env.example +++ b/djapi/.env.example @@ -17,3 +17,20 @@ DEEPSEEK_API_KEY=your-deepseek-api-key # 阿里 DashScope (ASR + Qwen) DASHSCOPE_API_KEY=your-dashscope-api-key + +# ======================== +# 以下为 video 模块 AI 模型配置 +# 修改后重启 uWSGI 生效 +# ======================== + +# DeepSeek 模型名(视频分割 + 标题提取) +# 可用: deepseek-chat(推荐,支持 response_format), deepseek-reasoner +DEEPSEEK_MODEL=deepseek-chat + +# DashScope ASR 语音识别模型 +# 可用: paraformer-realtime-v2, paraformer-v2 +DASHSCOPE_ASR_MODEL=paraformer-realtime-v2 + +# DashScope 大语言模型(文本纠错 + 文本分析) +# 可用: qwen-plus, qwen-max, qwen-turbo +DASHSCOPE_LLM_MODEL=qwen-plus diff --git a/djapi/api/stock/getDivData_AK.py b/djapi/api/stock/getDivData_AK.py deleted file mode 100644 index 1c6289d..0000000 --- a/djapi/api/stock/getDivData_AK.py +++ /dev/null @@ -1,181 +0,0 @@ -""" -⚠️ 已废弃 — 2026-06-05 - -AkShare 股息率数据获取功能已归一化到 data_source.py。 -如需 AkShare 日线数据,请使用: - - from .data_source import get_daily - df = get_daily(ts_code, start, end, source='akshare') - -本文件保留仅用于向后兼容,所有公开函数委托给统一数据源。 -""" - -import pandas as pd -import numpy as np -from datetime import datetime, timedelta - -from .data_source import get_daily - -from .stock_utils import tscodeCheck, date_format_correction -from .smoothBrush import smooth_dataframe_brush - -GAP_DAYS = 360 # TTM 计算窗口 - - -def _parse_tscode(tscode: str) -> tuple: - """将 tscode(如 000001.SZ)转为 akshare 格式的 symbol 和 market""" - code = tscode.split('.')[0] - suffix = tscode.split('.')[1].lower() - market_map = {'sz': 'sz', 'sh': 'sh', 'bj': 'bj'} - return code, market_map.get(suffix, suffix) - - -def _fetch_daily_price(symbol: str, start_date: str, end_date: str) -> pd.DataFrame: - """通过 akshare 获取前复权日线行情""" - df = ak.stock_zh_a_hist( - symbol=symbol, - period='daily', - start_date=start_date, - end_date=end_date, - adjust='qfq' - ) - if df.empty: - return pd.DataFrame() - - df = df.rename(columns={ - '日期': 'trade_date', - '开盘': 'open', - '收盘': 'close', - '最高': 'high', - '最低': 'low', - '成交量': 'vol', - '成交额': 'amount', - '换手率': 'turnover_rate', - }) - df['trade_date'] = df['trade_date'].astype(str).str.replace('-', '') - return df - - -def _fetch_dividends(symbol: str, market: str) -> pd.DataFrame: - """通过 akshare 获取历史分红记录""" - try: - df = ak.stock_dividend_cninfo(stock=symbol, symbol=market + symbol) - except Exception: - return pd.DataFrame() - - if df.empty: - return pd.DataFrame() - - # 列名映射(akshare 返回中文列名) - col_map = { - '除权除息日': 'ex_date', - '每股派息': 'cash_div_tax', - } - df = df.rename(columns={k: v for k, v in col_map.items() if k in df.columns}) - - if 'ex_date' not in df.columns or 'cash_div_tax' not in df.columns: - return pd.DataFrame() - - df['ex_date'] = df['ex_date'].astype(str).str.replace('-', '').str[:8] - df['cash_div_tax'] = pd.to_numeric(df['cash_div_tax'], errors='coerce').fillna(0) - return df[['ex_date', 'cash_div_tax']] - - -def get_akshare_dividend_yield(tscode: str, start_date: str = None, end_date: str = None) -> pd.DataFrame: - """ - 通过 akshare 获取股价和分红数据,计算股息率。 - - Args: - tscode: 股票代码,如 '000001.SZ' - start_date: 起始日期 yyyyMMdd - end_date: 结束日期 yyyyMMdd - - Returns: - DataFrame: ts_code, trade_date, close, cash_div_tax, cash_div_year, div_yield - """ - tscode = tscodeCheck(tscode) - today = datetime.now().strftime('%Y%m%d') - - if start_date: - start_date = date_format_correction(start_date) - else: - start_date = '20200101' - if end_date: - end_date = date_format_correction(end_date) - else: - end_date = today - - if end_date > today: - end_date = today - - symbol, market = _parse_tscode(tscode) - - # 1. 获取日线行情(运算时起始日期往前推 GAP_DAYS) - calc_start = datetime.strptime(start_date, '%Y%m%d') - timedelta(days=GAP_DAYS) - calc_start_str = calc_start.strftime('%Y%m%d') - - df_price = _fetch_daily_price(symbol, calc_start_str, end_date) - if df_price.empty: - return pd.DataFrame() - df_price = df_price.sort_values('trade_date').reset_index(drop=True) - - # 2. 获取分红数据 - df_div = _fetch_dividends(symbol, market) - - # 3. 计算 TTM 分红序列 - if df_div.empty: - df_price['cash_div_tax'] = 0.0 - df_price['cash_div_year'] = 0.0 - else: - # 将分红按 ex_date 合并到交易日历 - df_div['ex_date'] = pd.to_datetime(df_div['ex_date'], format='%Y%m%d') - df_price['trade_date_dt'] = pd.to_datetime(df_price['trade_date'], format='%Y%m%d') - - # 按日期合并 - div_dict = df_div.set_index('ex_date')['cash_div_tax'].to_dict() - - def calc_ttm_div(trade_dt): - window_start = trade_dt - timedelta(days=GAP_DAYS) - total = 0.0 - for ex_dt, cash in div_dict.items(): - if window_start < ex_dt <= trade_dt: - total += cash - return total - - cash_div_tax_list = [] - cash_div_year_list = [] - - for _, row in df_price.iterrows(): - td = row['trade_date_dt'] - cash = div_dict.get(td, 0.0) - cash_div_tax_list.append(cash) - cash_div_year_list.append(calc_ttm_div(td)) - - df_price['cash_div_tax'] = cash_div_tax_list - df_price['cash_div_year'] = cash_div_year_list - df_price = df_price.drop(columns=['trade_date_dt']) - - # 4. 毛刺平滑 - df_price = smooth_dataframe_brush( - df_price, - target_columns=['cash_div_year'], - window_size=31, - threshold_factor=0.5, - max_brush_length=15 - ) - - # 5. 截取请求的时间范围 - df_result = df_price[ - (df_price['trade_date'] >= start_date) & (df_price['trade_date'] <= end_date) - ].copy() - - # 6. 计算股息率 - df_result['div_yield'] = 0.0 - mask = (df_result['close'] > 0) & (df_result['cash_div_year'] > 0) - df_result.loc[mask, 'div_yield'] = ( - (df_result.loc[mask, 'cash_div_year'] / df_result.loc[mask, 'close']) * 100 - ).round(4) - - df_result['ts_code'] = tscode - final_cols = ['ts_code', 'trade_date', 'close', 'cash_div_tax', 'cash_div_year', 'div_yield'] - return df_result[final_cols].reset_index(drop=True) diff --git a/djapi/api/stock/getStockDiv2.py b/djapi/api/stock/getStockDiv2.py index 9dcb8f7..d01ae63 100644 --- a/djapi/api/stock/getStockDiv2.py +++ b/djapi/api/stock/getStockDiv2.py @@ -52,7 +52,6 @@ def analyze_stock_dividend_and_price(ts_code, start_date=START_DATE, end_date=EN if start_date > today: start_date = today GAP_DAYS = 360 # 定义TTM计算窗口期为360天 - n = 15 # 定义向前填充的最大非零值个数 """ 分析个股分红与行情数据。 @@ -110,23 +109,11 @@ def analyze_stock_dividend_and_price(ts_code, start_date=START_DATE, end_date=EN # 填充空值 df_merged_temp['cash_div_tax'] = df_merged_temp['cash_div_tax'].fillna(0.0) - # 计算 cash_div_year (TTM) + # 计算 cash_div_year (TTM) — 向量化 rolling 窗口 df_merged_temp = df_merged_temp.sort_values('trade_date').reset_index(drop=True) - # 使用滚动窗口计算过去 GAP_DAYS 天的总和 - # rolling的window参数是基于行数的,所以我们需要先确保日期是连续的交易日 - # 由于trade_date已经是交易日,我们可以直接使用rolling - # 但需要处理时间窗口,确保是360天而不是360行(因为可能有节假日) - # 更精确的方法是使用一个自定义函数来累加过去360天内的值 - - # 使用更精确的日期差计算 - def calculate_ttm_div(row_idx): - current_date = df_merged_temp.loc[row_idx, 'trade_date'] - start_window_date = current_date - pd.Timedelta(days=GAP_DAYS) - # 筛选出窗口期内的记录 - mask = (df_merged_temp['trade_date'] > start_window_date) & (df_merged_temp['trade_date'] <= current_date) - return df_merged_temp.loc[mask, 'cash_div_tax'].sum() - - df_merged_temp['cash_div_year'] = [calculate_ttm_div(i) for i in range(len(df_merged_temp))] + df_temp = df_merged_temp.set_index('trade_date') + df_temp['cash_div_year'] = df_temp['cash_div_tax'].rolling(f'{GAP_DAYS}D', min_periods=1).sum() + df_merged_temp['cash_div_year'] = df_temp['cash_div_year'].values # 截取原始请求的起止时间内的数据 @@ -155,37 +142,6 @@ def analyze_stock_dividend_and_price(ts_code, start_date=START_DATE, end_date=EN df_result['cash_div_year'] = df_result['cash_div_year'].fillna(0.0) - ''' - 向前填充cash_div_year(最多填充最近n个非零值) - 如果遇到0值,依次往下查询直到查询到非0数字为止,如果数字个数<=n个,就置last_valid_value, 否则保持不变 - ''' - # 确保按trade_date倒序遍历(日期从大到小) - df_result = df_result.sort_values('trade_date', ascending=False).reset_index(drop=True) - - last_valid_value = None # 初始化最后一个有效值变量 - for idx in range(len(df_result)): # 遍历DataFrame的每一行 - current_value = df_result.loc[idx, 'cash_div_year'] # 获取当前行的TTM分红值 - if current_value > 0: # 如果当前值大于0 - last_valid_value = current_value # 更新最后一个有效值 - else: - # 向下查找最多n个位置内的非零值 - found_value = None # 初始化找到的值 - search_count = 0 # 初始化搜索计数 - # 从下一行开始搜索,最多搜索n行 - for search_idx in range(idx + 1, min(idx + n + 1, len(df_result))): - search_value = df_result.loc[search_idx, 'cash_div_year'] # 获取搜索行的值 - search_count += 1 # 增加搜索计数 - if search_value > 0: # 如果找到非零值 - found_value = search_value # 记录找到的值 - break # 跳出搜索循环 - - # 如果找到非零值且在n个位置内 - if found_value is not None and search_count <= n: - df_result.loc[idx, 'cash_div_year'] = found_value - last_valid_value = found_value - elif last_valid_value is not None: - df_result.loc[idx, 'cash_div_year'] = last_valid_value - # 毛刺平滑处理 cash_div_year 列 df_result=smooth_dataframe_brush(df_result, target_columns=['cash_div_year'], window_size=31, threshold_factor=0.5, max_brush_length=15 ) # 恢复原始日期顺序 diff --git a/djapi/api/stock/smoothBrush.py b/djapi/api/stock/smoothBrush.py index fa9971d..0dd11a5 100644 --- a/djapi/api/stock/smoothBrush.py +++ b/djapi/api/stock/smoothBrush.py @@ -90,9 +90,9 @@ def smooth_series_brush(series: pd.Series, window_size: int = 7, threshold_facto # 决定使用哪个值填充 if prev_valid_val is not None: - fill_value = next_valid_val - elif next_valid_val is not None: fill_value = prev_valid_val + elif next_valid_val is not None: + fill_value = next_valid_val else: print(f"警告: 毛刺段 {brush_indices} 无有效邻居,使用全局中位数填充。") fill_value = series.median() diff --git a/djapi/api/urls.py b/djapi/api/urls.py index 8a12328..c4b24bd 100644 --- a/djapi/api/urls.py +++ b/djapi/api/urls.py @@ -18,7 +18,7 @@ urlpatterns = [ path('stockep/', views.stockep, name='stockep'), path('finance/', views.getFinaData, name='getFinaData'), path('getdiv/', views.getDivData, name='getDivData'), - path('getdivak/', views.getDivDataAkshare, name='getDivDataAkshare'), + path('xwlbNews/', views.xwlbNews, name='xwlbNews'), path('xwlbFine/', views.xwlbFine, name='xwlbFine'), ] \ No newline at end of file diff --git a/djapi/api/video/audioRead.py b/djapi/api/video/audioRead.py index d493cf1..d3a37f9 100644 --- a/djapi/api/video/audioRead.py +++ b/djapi/api/video/audioRead.py @@ -166,7 +166,7 @@ def transcribe_audio(audio_path): dashscope.api_key = os.getenv('DASHSCOPE_API_KEY', '') # 创建识别对象 recognition = Recognition( - model='paraformer-realtime-v2', # 使用实时识别模型 + model=os.getenv('DASHSCOPE_ASR_MODEL', 'paraformer-realtime-v2'), format='wav', sample_rate=16000, language_hints=['zh','en'], # 中文和英文 @@ -258,7 +258,7 @@ def text_correction(text): logger.info("调用通义千问模型进行文本修正...") # 调用DashScope文本生成接口 response = Generation.call( - model="qwen-plus", + model=os.getenv('DASHSCOPE_LLM_MODEL', 'qwen-plus'), messages=messages, max_tokens=30000, temperature=0.1, # 使用较低的温度以提高确定性 @@ -316,7 +316,7 @@ def analyze_text(text, prompt): logger.info("调用通义千问模型进行文本分析...") # 调用DashScope文本生成接口 response = Generation.call( - model="qwen-plus", # 使用通义千问Plus模型进行分析 + model=os.getenv('DASHSCOPE_LLM_MODEL', 'qwen-plus'), messages=messages, max_tokens=8190, # 控制生成文本的最大长度 temperature=0.3, # 控制生成文本的确定性 diff --git a/djapi/api/video/deepseek.py b/djapi/api/video/deepseek.py index df2448b..c833a08 100644 --- a/djapi/api/video/deepseek.py +++ b/djapi/api/video/deepseek.py @@ -127,13 +127,14 @@ class DeepSeekAPI: else: raise Exception("API请求失败,未知错误") - def process_text(self, - prompt: str, - text: str, + def process_text(self, + prompt: str, + text: str, system_prompt: Optional[str] = None, model: str = "deepseek-chat", temperature: float = 0.7, - max_tokens: int = 2000) -> str: + max_tokens: int = 2000, + response_format: Optional[Dict] = None) -> str: """ 处理文本的通用方法 @@ -179,6 +180,8 @@ class DeepSeekAPI: "max_tokens": max_tokens, "stream": False } + if response_format: + payload["response_format"] = response_format try: # 发送API请求 @@ -236,14 +239,15 @@ def deepseek_text(text, prompt): custom_system_prompt = "你是一个专业的文本分析助手,擅长根据提示词对长文本进行深入分析。" try: - # 处理文本 + # 处理文本(使用 response_format 强制返回 JSON) result = api_client.process_text( - model="deepseek-reasoner", + model=os.getenv('DEEPSEEK_MODEL', 'deepseek-chat'), prompt=prompt, text=text, system_prompt=custom_system_prompt, temperature=0.5, - max_tokens=20000 + max_tokens=20000, + response_format={"type": "json_object"} ) #print("处理结果:") diff --git a/djapi/api/views.py b/djapi/api/views.py index b183bdd..e2c9b67 100644 --- a/djapi/api/views.py +++ b/djapi/api/views.py @@ -14,7 +14,6 @@ from .stock.stockMargin import getStockMargin, getDailyMargin from .stock.getStockFina import get_finance_data_range from .stock.getStockDiv2 import analyze_stock_dividend_and_price from .stock.xwlbDaily import get_xwlb, get_xwlb_fine -from .stock.getDivData_AK import get_akshare_dividend_yield from .serializers import ( StockDailySerializer, StockInfoSerializer, IndustryStockSerializer, StockParamSerializer, StockEpSerializer, QuarterlyEpsSerializer, @@ -213,15 +212,6 @@ def getDivData(request): return viewFunc_tsCodeAndDate(request, analyze_stock_dividend_and_price) -@extend_schema( - parameters=[_PARAM_TSCODE, _PARAM_START, _PARAM_END], - responses={200: DividendSerializer(many=True)}, - description='获取个股股息率数据(akshare 数据源,无需 token)', - tags=['分红'], -) -@api_view(['GET']) -def getDivDataAkshare(request): - return viewFunc_tsCodeAndDate(request, get_akshare_dividend_yield) def _xwlb_view(request, data_func):