1
This commit is contained in:
parent
27c6794aeb
commit
0902091f08
Binary file not shown.
Binary file not shown.
@ -245,9 +245,9 @@ async def _load_today_from_db() -> dict:
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# 加载市场温度
|
# 加载市场温度(按 trade_date 取最新交易日)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("SELECT * FROM market_temperature ORDER BY created_at DESC LIMIT 1")
|
text("SELECT * FROM market_temperature ORDER BY trade_date DESC LIMIT 1")
|
||||||
)
|
)
|
||||||
mt_row = result.fetchone()
|
mt_row = result.fetchone()
|
||||||
market_temp = None
|
market_temp = None
|
||||||
@ -264,12 +264,14 @@ async def _load_today_from_db() -> dict:
|
|||||||
temperature=m["temperature"],
|
temperature=m["temperature"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# 加载推荐(按 ts_code 去重,取最新一条)
|
# 加载推荐(取最近一个有数据的日期,按 ts_code 去重)
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("SELECT * FROM recommendations WHERE date(created_at) = :today "
|
text("SELECT * FROM recommendations "
|
||||||
"AND id IN (SELECT MAX(id) FROM recommendations WHERE date(created_at) = :today GROUP BY ts_code) "
|
"WHERE date(created_at) = (SELECT date(created_at) FROM recommendations ORDER BY created_at DESC LIMIT 1) "
|
||||||
"ORDER BY score DESC"),
|
"AND id IN (SELECT MAX(id) FROM recommendations "
|
||||||
{"today": today}
|
" WHERE date(created_at) = (SELECT date(created_at) FROM recommendations ORDER BY created_at DESC LIMIT 1) "
|
||||||
|
" GROUP BY ts_code) "
|
||||||
|
"ORDER BY score DESC")
|
||||||
)
|
)
|
||||||
rows = result.fetchall()
|
rows = result.fetchall()
|
||||||
recommendations = []
|
recommendations = []
|
||||||
|
|||||||
@ -1,11 +1,16 @@
|
|||||||
"""趋势突破统一筛选器
|
"""趋势突破统一筛选器(自上而下方案)
|
||||||
|
|
||||||
三阶段管道:全市场批量预筛 → 资金流过滤 → 逐股深度分析
|
三阶段管道:
|
||||||
评分公式:趋势&时机30% + 资金流25% + 供需20% + 板块共振15% + 市场温度10%
|
Step 1: 板块定位 — 找到有资金流入的热门板块 (3-5个)
|
||||||
|
Step 2: 板块内选股 — 在热门板块成分股中筛出有资金流入的候选 (30-50只)
|
||||||
|
Step 3: 深度分析 — 供需 + 价格行为 + 趋势 (10-15只推荐)
|
||||||
|
|
||||||
自动检测是否在交易时段:
|
评分公式:供需关系 40% + 价格行为 35% + 趋势 25%
|
||||||
- 盘中模式:用前一日 Tushare 数据 + 腾讯实时行情混合筛选
|
板块和资金流作为前置过滤条件,不参与评分。
|
||||||
- 盘后模式:用当日 Tushare 完整数据筛选
|
|
||||||
|
数据源:
|
||||||
|
- 盘中模式:Tushare 日线 + 腾讯实时行情混合
|
||||||
|
- 盘后模式:Tushare 当日完整数据
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -47,23 +52,40 @@ async def run_screening(trade_date: str = None) -> dict:
|
|||||||
|
|
||||||
market_temp_score = market_temp.temperature
|
market_temp_score = market_temp.temperature
|
||||||
|
|
||||||
# ── 板块热度(用于板块共振评分) ──
|
# ── Step 1: 板块定位 ──
|
||||||
logger.info("=== 板块热度扫描 ===")
|
logger.info("=== Step 1: 板块定位 ===")
|
||||||
all_sectors = scan_hot_sectors(trade_date)
|
all_sectors = scan_hot_sectors(trade_date)
|
||||||
hot_sectors = all_sectors[:settings.top_sector_count]
|
|
||||||
|
# 前置过滤:只保留有资金流入 + 非末期的板块
|
||||||
|
hot_sectors = [
|
||||||
|
s for s in all_sectors
|
||||||
|
if s.capital_inflow > 0 and s.stage not in ("end",)
|
||||||
|
][:settings.top_sector_count]
|
||||||
|
|
||||||
|
if not hot_sectors:
|
||||||
|
logger.info("无合格热门板块(需要资金流入+非末期),回退到全部板块")
|
||||||
|
hot_sectors = all_sectors[:settings.top_sector_count]
|
||||||
|
|
||||||
|
for s in hot_sectors:
|
||||||
|
logger.info(f" 目标板块: {s.sector_name} 涨幅{s.pct_change}% 资金{s.capital_inflow:.0f}万 "
|
||||||
|
f"涨停{s.limit_up_count} 阶段={s.stage}")
|
||||||
|
|
||||||
# 盘中用实时行情更新板块涨幅和涨停数
|
# 盘中用实时行情更新板块涨幅和涨停数
|
||||||
if intraday:
|
if intraday:
|
||||||
hot_sectors = await intraday_sector_scan(hot_sectors)
|
hot_sectors = await intraday_sector_scan(hot_sectors)
|
||||||
|
|
||||||
# ── 趋势突破三阶段管道 ──
|
# ── Step 2: 板块内选股 ──
|
||||||
logger.info("=== 趋势突破扫描 ===")
|
logger.info("=== Step 2: 板块内选股 ===")
|
||||||
candidates = await scan_trend_breakout(
|
candidates = await _select_from_hot_sectors(hot_sectors, trade_date, intraday)
|
||||||
trade_date=trade_date,
|
|
||||||
market_temp=market_temp,
|
if not candidates:
|
||||||
hot_sectors=hot_sectors,
|
logger.info("=== Step 2 无候选,回退到全市场扫描 ===")
|
||||||
intraday=intraday,
|
candidates = await scan_trend_breakout(
|
||||||
)
|
trade_date=trade_date,
|
||||||
|
market_temp=market_temp,
|
||||||
|
hot_sectors=hot_sectors,
|
||||||
|
intraday=intraday,
|
||||||
|
)
|
||||||
|
|
||||||
if not candidates:
|
if not candidates:
|
||||||
logger.info("=== 筛选完成: 0 只股票 ===")
|
logger.info("=== 筛选完成: 0 只股票 ===")
|
||||||
@ -74,8 +96,9 @@ async def run_screening(trade_date: str = None) -> dict:
|
|||||||
"scan_mode": scan_mode,
|
"scan_mode": scan_mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 构建推荐列表 ──
|
# ── Step 3: 供需 + 价格行为 + 趋势评分 ──
|
||||||
recommendations = _build_trend_recommendations(
|
logger.info("=== Step 3: 深度分析 ===")
|
||||||
|
recommendations = _build_recommendations(
|
||||||
candidates, market_temp, hot_sectors, market_temp_score, intraday,
|
candidates, market_temp, hot_sectors, market_temp_score, intraday,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,147 +119,498 @@ async def run_screening(trade_date: str = None) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _build_trend_recommendations(
|
async def _select_from_hot_sectors(
|
||||||
|
hot_sectors: list[SectorInfo],
|
||||||
|
trade_date: str,
|
||||||
|
intraday: bool,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Step 2: 从热门板块成分股中选出有资金流入的候选
|
||||||
|
|
||||||
|
流程:
|
||||||
|
1. 收集所有热门板块的成分股代码
|
||||||
|
2. 用 get_daily_all + get_daily_basic 过滤市值/换手率
|
||||||
|
3. 用 get_moneyflow_batch 过滤主力净流入 > 0
|
||||||
|
4. 对候选做入场信号初筛(只需满足任一信号类型)
|
||||||
|
"""
|
||||||
|
from app.data.tushare_client import tushare_client
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
if not trade_date:
|
||||||
|
trade_date = tushare_client.get_latest_trade_date()
|
||||||
|
|
||||||
|
# 收集热门板块成分股代码
|
||||||
|
sector_member_codes: set[str] = set()
|
||||||
|
sector_code_map: dict[str, str] = {} # ts_code -> sector_name
|
||||||
|
for s in hot_sectors:
|
||||||
|
try:
|
||||||
|
members_df = tushare_client.get_ths_members(s.sector_code)
|
||||||
|
if not members_df.empty and "con_code" in members_df.columns:
|
||||||
|
codes = members_df["con_code"].tolist()
|
||||||
|
sector_member_codes.update(codes)
|
||||||
|
for c in codes:
|
||||||
|
sector_code_map[c] = s.sector_name
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"获取板块 {s.sector_name} 成分股失败: {e}")
|
||||||
|
|
||||||
|
if not sector_member_codes:
|
||||||
|
logger.info("Step 2: 无板块成分股数据")
|
||||||
|
return []
|
||||||
|
|
||||||
|
logger.info(f"Step 2: 热门板块共 {len(sector_member_codes)} 只成分股")
|
||||||
|
|
||||||
|
# 过滤市值/换手率/ST/次新
|
||||||
|
stock_basic = tushare_client.get_stock_basic()
|
||||||
|
exclude_codes = set()
|
||||||
|
if not stock_basic.empty:
|
||||||
|
st_codes = set(stock_basic[stock_basic["name"].str.contains("ST", na=False)]["ts_code"])
|
||||||
|
exclude_codes.update(st_codes)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=settings.min_list_days)).strftime("%Y%m%d")
|
||||||
|
new_codes = set(stock_basic[stock_basic["list_date"] > cutoff]["ts_code"])
|
||||||
|
exclude_codes.update(new_codes)
|
||||||
|
|
||||||
|
# 行业映射
|
||||||
|
industry_map = {}
|
||||||
|
if not stock_basic.empty:
|
||||||
|
for _, row in stock_basic.iterrows():
|
||||||
|
industry_map[row["ts_code"]] = row.get("industry", "")
|
||||||
|
|
||||||
|
# 用 daily_basic 过滤
|
||||||
|
basic = tushare_client.get_daily_basic(trade_date)
|
||||||
|
if basic.empty:
|
||||||
|
logger.info("Step 2: daily_basic 无数据")
|
||||||
|
return []
|
||||||
|
|
||||||
|
basic["circ_mv"] = basic["circ_mv"] / 10000 # 万元 → 亿元
|
||||||
|
|
||||||
|
filtered_basic = basic[
|
||||||
|
(basic["ts_code"].isin(sector_member_codes)) &
|
||||||
|
(~basic["ts_code"].isin(exclude_codes)) &
|
||||||
|
(basic["circ_mv"] >= settings.min_circ_mv) &
|
||||||
|
(basic["circ_mv"] <= settings.max_circ_mv) &
|
||||||
|
(basic["turnover_rate"] >= settings.min_turnover_rate) &
|
||||||
|
(basic["turnover_rate"] <= settings.max_turnover_rate)
|
||||||
|
].copy()
|
||||||
|
|
||||||
|
logger.info(f"Step 2 基本面过滤: {len(sector_member_codes)} 只 → {len(filtered_basic)} 只")
|
||||||
|
|
||||||
|
if filtered_basic.empty:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 资金流过滤:主力净流入 > 0
|
||||||
|
mf = tushare_client.get_moneyflow_batch(trade_date)
|
||||||
|
if mf.empty:
|
||||||
|
logger.info("Step 2: 资金流数据为空,跳过资金过滤")
|
||||||
|
candidate_codes = set(filtered_basic["ts_code"].tolist())
|
||||||
|
else:
|
||||||
|
mf["main_net_inflow"] = (
|
||||||
|
(mf["buy_elg_amount"] - mf["sell_elg_amount"]) +
|
||||||
|
(mf["buy_lg_amount"] - mf["sell_lg_amount"])
|
||||||
|
)
|
||||||
|
total = (
|
||||||
|
mf["buy_elg_amount"] + mf["sell_elg_amount"] +
|
||||||
|
mf["buy_lg_amount"] + mf["sell_lg_amount"] +
|
||||||
|
mf["buy_md_amount"] + mf["sell_md_amount"] +
|
||||||
|
mf["buy_sm_amount"] + mf["sell_sm_amount"]
|
||||||
|
)
|
||||||
|
mf["inflow_ratio"] = (mf["main_net_inflow"] / total.replace(0, float("nan")) * 100).fillna(0)
|
||||||
|
|
||||||
|
mf_positive = mf[
|
||||||
|
(mf["ts_code"].isin(set(filtered_basic["ts_code"]))) &
|
||||||
|
(mf["main_net_inflow"] > 0)
|
||||||
|
].sort_values("main_net_inflow", ascending=False)
|
||||||
|
|
||||||
|
candidate_codes = set(mf_positive["ts_code"].tolist())
|
||||||
|
|
||||||
|
# 构建资金流查找表
|
||||||
|
mf_lookup = {}
|
||||||
|
for _, row in mf_positive.iterrows():
|
||||||
|
mf_lookup[row["ts_code"]] = {
|
||||||
|
"main_net_inflow": float(row["main_net_inflow"]),
|
||||||
|
"inflow_ratio": float(row.get("inflow_ratio", 0)),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"Step 2 资金流过滤: → {len(candidate_codes)} 只主力净流入 > 0")
|
||||||
|
|
||||||
|
if not candidate_codes:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 构建候选列表
|
||||||
|
import numpy as np
|
||||||
|
candidates = []
|
||||||
|
for ts_code in candidate_codes:
|
||||||
|
name = ""
|
||||||
|
if not stock_basic.empty:
|
||||||
|
row = stock_basic[stock_basic["ts_code"] == ts_code]
|
||||||
|
if not row.empty:
|
||||||
|
name = row.iloc[0]["name"]
|
||||||
|
|
||||||
|
sector_name = sector_code_map.get(ts_code, industry_map.get(ts_code, ""))
|
||||||
|
b_row = filtered_basic[filtered_basic["ts_code"] == ts_code]
|
||||||
|
turnover_rate = float(b_row.iloc[0]["turnover_rate"]) if not b_row.empty else 0
|
||||||
|
circ_mv = float(b_row.iloc[0]["circ_mv"]) if not b_row.empty else 0
|
||||||
|
pe = float(b_row.iloc[0]["pe"]) if not b_row.empty and pd.notna(b_row.iloc[0].get("pe")) else None
|
||||||
|
pb = float(b_row.iloc[0]["pb"]) if not b_row.empty and pd.notna(b_row.iloc[0].get("pb")) else None
|
||||||
|
volume_ratio = float(b_row.iloc[0]["volume_ratio"]) if not b_row.empty and pd.notna(b_row.iloc[0].get("volume_ratio")) else None
|
||||||
|
|
||||||
|
try:
|
||||||
|
mf_info = mf_lookup.get(ts_code, {})
|
||||||
|
except NameError:
|
||||||
|
mf_info = {}
|
||||||
|
|
||||||
|
candidates.append({
|
||||||
|
"ts_code": ts_code,
|
||||||
|
"name": name,
|
||||||
|
"sector": sector_name,
|
||||||
|
"turnover_rate": turnover_rate,
|
||||||
|
"circ_mv": circ_mv,
|
||||||
|
"pe": pe,
|
||||||
|
"pb": pb,
|
||||||
|
"volume_ratio": volume_ratio,
|
||||||
|
"main_net_inflow": mf_info.get("main_net_inflow", 0),
|
||||||
|
"inflow_ratio": mf_info.get("inflow_ratio", 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(f"Step 2 候选: {len(candidates)} 只")
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def _build_recommendations(
|
||||||
candidates: list[dict],
|
candidates: list[dict],
|
||||||
market_temp: MarketTemperature,
|
market_temp: MarketTemperature,
|
||||||
hot_sectors: list[SectorInfo],
|
hot_sectors: list[SectorInfo],
|
||||||
market_temp_score: float = 0,
|
market_temp_score: float = 0,
|
||||||
intraday: bool = False,
|
intraday: bool = False,
|
||||||
) -> list[Recommendation]:
|
) -> list[Recommendation]:
|
||||||
"""从趋势突破扫描结果构建推荐列表
|
"""Step 3: 对候选做供需 + 价格行为 + 趋势深度分析
|
||||||
|
|
||||||
评分公式:趋势&时机30% + 资金流25% + 供需20% + 板块共振15% + 市场温度10%
|
评分公式:供需关系 40% + 价格行为 35% + 趋势 25%
|
||||||
|
板块和资金流已在前置过滤中处理。
|
||||||
"""
|
"""
|
||||||
|
from app.data.tushare_client import tushare_client
|
||||||
|
from app.analysis.technical import add_all_indicators
|
||||||
|
from app.analysis.breakout_signals import (
|
||||||
|
classify_entry_signal,
|
||||||
|
score_supply_demand,
|
||||||
|
analyze_volume_pattern,
|
||||||
|
EntrySignal,
|
||||||
|
)
|
||||||
|
from app.analysis.signals import generate_signals
|
||||||
|
from app.analysis.capital_flow import _score_valuation
|
||||||
|
|
||||||
|
# 名称和行业映射
|
||||||
|
stock_basic = tushare_client.get_stock_basic()
|
||||||
|
name_map = {}
|
||||||
|
industry_map = {}
|
||||||
|
if not stock_basic.empty:
|
||||||
|
for _, row in stock_basic.iterrows():
|
||||||
|
name_map[row["ts_code"]] = row["name"]
|
||||||
|
industry_map[row["ts_code"]] = row.get("industry", "")
|
||||||
|
|
||||||
recommendations = []
|
recommendations = []
|
||||||
|
total = len(candidates)
|
||||||
|
signal_counts = {"breakout": 0, "pullback": 0, "launch": 0, "none": 0}
|
||||||
|
|
||||||
for stock in candidates:
|
for idx, stock in enumerate(candidates):
|
||||||
ts_code = stock["ts_code"]
|
ts_code = stock.get("ts_code", "")
|
||||||
name = stock["name"]
|
if not ts_code:
|
||||||
sector = stock["sector"]
|
continue
|
||||||
entry_signal_type = stock.get("entry_signal_type", "none")
|
|
||||||
entry_signal_score = stock.get("entry_signal_score", 0)
|
|
||||||
tech_signal = stock.get("tech_signal")
|
|
||||||
|
|
||||||
# 各维度得分
|
name = stock.get("name") or name_map.get(ts_code, ts_code)
|
||||||
trend_timing_score = stock.get("trend_timing_score", 50)
|
sector = stock.get("sector") or industry_map.get(ts_code, "")
|
||||||
supply_demand_score = stock.get("supply_demand_score", 50)
|
|
||||||
capital_score = stock.get("capital_score", 50)
|
|
||||||
position_score = stock.get("position_score", 50)
|
|
||||||
valuation_score = stock.get("valuation_score", 50)
|
|
||||||
|
|
||||||
# 板块共振评分
|
try:
|
||||||
sector_score = _score_sector_resonance(sector, hot_sectors)
|
# 获取 120 日 K 线
|
||||||
sector_stage = _get_sector_stage(sector, hot_sectors)
|
df = tushare_client.get_stock_daily(ts_code, 120)
|
||||||
|
if df.empty or len(df) < 30:
|
||||||
|
continue
|
||||||
|
|
||||||
# 综合评分(新权重)
|
# 添加技术指标
|
||||||
final_score = (
|
df = add_all_indicators(df)
|
||||||
trend_timing_score * 0.30 +
|
|
||||||
capital_score * 0.25 +
|
|
||||||
supply_demand_score * 0.20 +
|
|
||||||
sector_score * 0.15 +
|
|
||||||
market_temp_score * 0.10
|
|
||||||
)
|
|
||||||
|
|
||||||
# 风险乘数
|
# ── 入场信号分类 ──
|
||||||
if tech_signal:
|
entry_signal = classify_entry_signal(df)
|
||||||
if tech_signal.rally_pct_5d > 20:
|
signal_type = entry_signal["signal_type"]
|
||||||
final_score *= 0.65
|
if signal_type == EntrySignal.NONE:
|
||||||
elif tech_signal.rally_pct_5d > 15:
|
signal_counts["none"] += 1
|
||||||
final_score *= 0.80
|
continue
|
||||||
|
signal_counts[signal_type.value] += 1
|
||||||
|
|
||||||
if sector_stage == "end":
|
# ── 三维度评分 ──
|
||||||
final_score *= 0.70
|
|
||||||
elif sector_stage == "late":
|
|
||||||
final_score *= 0.88
|
|
||||||
|
|
||||||
if market_temp_score < 30:
|
# 1. 供需关系评分 (40%)
|
||||||
final_score *= 0.75
|
supply_demand_score = score_supply_demand(df)
|
||||||
|
|
||||||
# 入场信号高置信度奖励
|
# 2. 价格行为评分 (35%)
|
||||||
if entry_signal_score >= 80:
|
price_action_score = _score_price_action(df, entry_signal)
|
||||||
final_score *= 1.10
|
|
||||||
|
|
||||||
# 确定信号和等级
|
# 3. 趋势评分 (25%)
|
||||||
level = _score_to_level(final_score)
|
trend_score = _score_trend(df)
|
||||||
signal = "HOLD"
|
|
||||||
if entry_signal_type != "none" and entry_signal_score >= 50 and position_score >= 30 and final_score >= 60:
|
|
||||||
signal = "BUY"
|
|
||||||
|
|
||||||
# 价格参考
|
# 综合评分
|
||||||
entry_price = None
|
final_score = (
|
||||||
target_price = None
|
supply_demand_score * 0.40 +
|
||||||
stop_loss = None
|
price_action_score * 0.35 +
|
||||||
if tech_signal:
|
trend_score * 0.25
|
||||||
entry_price = tech_signal.support_price
|
)
|
||||||
target_price = tech_signal.resist_price
|
|
||||||
stop_loss = tech_signal.stop_loss_price
|
|
||||||
|
|
||||||
# 根据入场信号类型调整参考价
|
# 风险乘数
|
||||||
details = stock.get("entry_signal_details", {})
|
tech_signal = generate_signals(ts_code, name)
|
||||||
if entry_signal_type == "breakout" and details.get("resist_level"):
|
|
||||||
entry_price = details["resist_level"]
|
|
||||||
target_price = round(entry_price * 1.05, 2)
|
|
||||||
elif entry_signal_type == "pullback" and details.get("support_price"):
|
|
||||||
entry_price = details["support_price"]
|
|
||||||
target_price = round(entry_price * 1.05, 2)
|
|
||||||
elif entry_signal_type == "launch" and details.get("resist_level"):
|
|
||||||
entry_price = round(details["resist_level"] * 1.01, 2)
|
|
||||||
target_price = round(details["resist_level"] * 1.08, 2)
|
|
||||||
|
|
||||||
# 生成推荐理由
|
if tech_signal:
|
||||||
reasons = _generate_reasons(stock, tech_signal, market_temp, intraday)
|
if tech_signal.rally_pct_5d > 20:
|
||||||
|
final_score *= 0.65
|
||||||
|
elif tech_signal.rally_pct_5d > 15:
|
||||||
|
final_score *= 0.80
|
||||||
|
|
||||||
# 风险提示
|
# 板块末期惩罚(板块信息来自 hot_sectors)
|
||||||
risk_note = _generate_risk_note(market_temp, tech_signal, stock)
|
sector_stage = _get_sector_stage(sector, hot_sectors)
|
||||||
|
if sector_stage == "end":
|
||||||
|
final_score *= 0.70
|
||||||
|
elif sector_stage == "late":
|
||||||
|
final_score *= 0.88
|
||||||
|
|
||||||
rec = Recommendation(
|
# 市场温度风控
|
||||||
ts_code=ts_code,
|
if market_temp_score < 30:
|
||||||
name=name,
|
final_score *= 0.75
|
||||||
sector=sector,
|
elif market_temp_score < 50:
|
||||||
score=round(final_score, 1),
|
final_score *= 0.88
|
||||||
market_temp_score=round(market_temp_score, 1),
|
|
||||||
sector_score=round(sector_score, 1),
|
# 高置信度入场信号奖励
|
||||||
capital_score=round(capital_score, 1),
|
if entry_signal.get("signal_score", 0) >= 80:
|
||||||
technical_score=round(stock.get("technical_score", 50), 1),
|
final_score *= 1.10
|
||||||
position_score=round(position_score, 1),
|
|
||||||
valuation_score=round(valuation_score, 1),
|
# 估值评分(辅助参考,不参与主评分)
|
||||||
signal=signal,
|
pe = stock.get("pe")
|
||||||
entry_price=entry_price,
|
pb = stock.get("pb")
|
||||||
target_price=target_price,
|
valuation_score = _score_valuation(pe, pb)
|
||||||
stop_loss=stop_loss,
|
|
||||||
reasons=reasons,
|
# 确定信号和等级
|
||||||
risk_note=risk_note,
|
level = _score_to_level(final_score)
|
||||||
level=level,
|
signal = "HOLD"
|
||||||
strategy="trend_breakout",
|
position_score = tech_signal.position_score if tech_signal else 50
|
||||||
entry_signal_type=entry_signal_type,
|
if (signal_type != EntrySignal.NONE
|
||||||
)
|
and entry_signal.get("signal_score", 0) >= 50
|
||||||
recommendations.append(rec)
|
and position_score >= 30
|
||||||
|
and final_score >= 60):
|
||||||
|
signal = "BUY"
|
||||||
|
|
||||||
|
# 价格参考
|
||||||
|
entry_price = None
|
||||||
|
target_price = None
|
||||||
|
stop_loss = None
|
||||||
|
if tech_signal:
|
||||||
|
entry_price = tech_signal.support_price
|
||||||
|
target_price = tech_signal.resist_price
|
||||||
|
stop_loss = tech_signal.stop_loss_price
|
||||||
|
|
||||||
|
details = entry_signal.get("details", {})
|
||||||
|
st = signal_type.value
|
||||||
|
if st == "breakout" and details.get("resist_level"):
|
||||||
|
entry_price = details["resist_level"]
|
||||||
|
target_price = round(entry_price * 1.05, 2)
|
||||||
|
elif st == "pullback" and details.get("support_price"):
|
||||||
|
entry_price = details["support_price"]
|
||||||
|
target_price = round(entry_price * 1.05, 2)
|
||||||
|
elif st == "launch" and details.get("resist_level"):
|
||||||
|
entry_price = round(details["resist_level"] * 1.01, 2)
|
||||||
|
target_price = round(details["resist_level"] * 1.08, 2)
|
||||||
|
|
||||||
|
# 生成推荐理由
|
||||||
|
reasons = _generate_reasons(stock, entry_signal, tech_signal, df, intraday)
|
||||||
|
risk_note = _generate_risk_note(market_temp, tech_signal, stock)
|
||||||
|
|
||||||
|
# 量价模式
|
||||||
|
vol_pattern = analyze_volume_pattern(df)
|
||||||
|
|
||||||
|
rec = Recommendation(
|
||||||
|
ts_code=ts_code,
|
||||||
|
name=name,
|
||||||
|
sector=sector,
|
||||||
|
score=round(final_score, 1),
|
||||||
|
market_temp_score=round(market_temp_score, 1),
|
||||||
|
sector_score=round(_get_sector_heat(sector, hot_sectors), 1),
|
||||||
|
capital_score=round(_score_capital_simple(stock), 1),
|
||||||
|
technical_score=round(trend_score, 1),
|
||||||
|
position_score=round(position_score, 1),
|
||||||
|
valuation_score=round(valuation_score, 1),
|
||||||
|
signal=signal,
|
||||||
|
entry_price=entry_price,
|
||||||
|
target_price=target_price,
|
||||||
|
stop_loss=stop_loss,
|
||||||
|
reasons=reasons,
|
||||||
|
risk_note=risk_note,
|
||||||
|
level=level,
|
||||||
|
strategy="trend_breakout",
|
||||||
|
entry_signal_type=signal_type.value,
|
||||||
|
)
|
||||||
|
recommendations.append(rec)
|
||||||
|
|
||||||
|
if len(recommendations) >= settings.top_stock_count:
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"深度分析 {ts_code} 失败: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 让出控制权(同步函数中无法 await,跳过)
|
||||||
|
# idx % 10 == 0 的让步在 _select_from_hot_sectors 的上层 async 函数中处理
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Step 3 入场信号分布: "
|
||||||
|
f"突破={signal_counts['breakout']} 回踩={signal_counts['pullback']} "
|
||||||
|
f"启动={signal_counts['launch']} 无信号={signal_counts['none']} "
|
||||||
|
f"(共分析{total}只)"
|
||||||
|
)
|
||||||
|
|
||||||
return recommendations
|
return recommendations
|
||||||
|
|
||||||
|
|
||||||
def _score_sector_resonance(sector_name: str, hot_sectors: list[SectorInfo]) -> float:
|
# ── 价格行为评分 ──
|
||||||
"""板块共振评分 (0-100)"""
|
|
||||||
for s in hot_sectors:
|
|
||||||
if s.sector_name == sector_name:
|
|
||||||
score = 40 # 在热门板块列表中
|
|
||||||
score += s.heat_score * 0.3 # 板块热度贡献
|
|
||||||
if s.stage == "early":
|
|
||||||
score += 30
|
|
||||||
elif s.stage == "mid":
|
|
||||||
score += 20
|
|
||||||
elif s.stage == "late":
|
|
||||||
score += 5
|
|
||||||
return min(score, 100)
|
|
||||||
return 10.0 # 不在热门板块
|
|
||||||
|
|
||||||
|
|
||||||
def _get_sector_score(sector_name: str, hot_sectors: list[SectorInfo]) -> float:
|
def _score_price_action(df, entry_signal: dict) -> float:
|
||||||
"""获取板块在热门板块中的得分"""
|
"""价格行为学评分 (0-100)
|
||||||
for s in hot_sectors:
|
|
||||||
if s.sector_name == sector_name:
|
维度:
|
||||||
return s.heat_score
|
- 入场信号类型质量 (40): 突破型/回踩型/启动型各自的得分
|
||||||
return 30.0
|
- K线形态强度 (30): 突破日/回踩日的K线实体占比、下影线、收盘位置
|
||||||
|
- 支撑阻力位质量 (30): 关键价格位置的测试情况
|
||||||
|
"""
|
||||||
|
score = 0
|
||||||
|
last = df.iloc[-1]
|
||||||
|
details = entry_signal.get("details", {})
|
||||||
|
signal_type = entry_signal.get("signal_type")
|
||||||
|
|
||||||
|
# 入场信号类型质量 (40)
|
||||||
|
signal_score = entry_signal.get("signal_score", 0)
|
||||||
|
score += signal_score * 0.40
|
||||||
|
|
||||||
|
# K线形态强度 (30)
|
||||||
|
day_range = last["high"] - last["low"]
|
||||||
|
if day_range > 0:
|
||||||
|
# 实体占比(实体/全振幅)
|
||||||
|
body = abs(last["close"] - last["open"])
|
||||||
|
body_ratio = body / day_range
|
||||||
|
if body_ratio > 0.7:
|
||||||
|
score += 20 # 大实体,方向明确
|
||||||
|
elif body_ratio > 0.4:
|
||||||
|
score += 12
|
||||||
|
elif body_ratio > 0.2:
|
||||||
|
score += 6
|
||||||
|
|
||||||
|
# 收盘位置(越接近高点越好)
|
||||||
|
close_position = (last["close"] - last["low"]) / day_range
|
||||||
|
if close_position > 0.8:
|
||||||
|
score += 10 # 收在上部 20%
|
||||||
|
elif close_position > 0.6:
|
||||||
|
score += 6
|
||||||
|
elif close_position > 0.4:
|
||||||
|
score += 3
|
||||||
|
|
||||||
|
# 支撑阻力位质量 (30)
|
||||||
|
if signal_type and signal_type.value == "breakout":
|
||||||
|
# 突破型:阻力位被突破的力度
|
||||||
|
breakout_pct = details.get("breakout_pct", 0)
|
||||||
|
vol_ratio = details.get("volume_ratio", 1)
|
||||||
|
if breakout_pct > 2 and vol_ratio > 2:
|
||||||
|
score += 30 # 强力突破
|
||||||
|
elif breakout_pct > 1 and vol_ratio > 1.5:
|
||||||
|
score += 20
|
||||||
|
elif breakout_pct > 0:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
elif signal_type and signal_type.value == "pullback":
|
||||||
|
# 回踩型:支撑位的精确度
|
||||||
|
support_ma = details.get("support_ma", "")
|
||||||
|
shrink = details.get("volume_shrink_ratio", 1)
|
||||||
|
if support_ma == "MA20" and shrink < 0.6:
|
||||||
|
score += 30 # 精确回踩 MA20 且大幅缩量
|
||||||
|
elif support_ma == "MA20":
|
||||||
|
score += 22
|
||||||
|
elif support_ma == "MA10" and shrink < 0.6:
|
||||||
|
score += 18
|
||||||
|
else:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
elif signal_type and signal_type.value == "launch":
|
||||||
|
# 启动型:整理的充分度
|
||||||
|
range_pct = details.get("price_range_pct", 10)
|
||||||
|
shrink = details.get("volume_shrink_ratio", 1)
|
||||||
|
if range_pct < 3 and shrink < 0.4:
|
||||||
|
score += 30 # 极度缩量窄幅整理
|
||||||
|
elif range_pct < 5 and shrink < 0.6:
|
||||||
|
score += 20
|
||||||
|
else:
|
||||||
|
score += 10
|
||||||
|
else:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
return min(score, 100)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 趋势评分 ──
|
||||||
|
|
||||||
|
|
||||||
|
def _score_trend(df) -> float:
|
||||||
|
"""趋势评分 (0-100)
|
||||||
|
|
||||||
|
维度:
|
||||||
|
- 均线排列 (40): MA5>MA10>MA20>MA60
|
||||||
|
- 更高高点/更高低点结构 (35): 近 20 日价格结构
|
||||||
|
- MA20 方向 (25): MA20 是否持续上行
|
||||||
|
"""
|
||||||
|
import pandas as pd
|
||||||
|
score = 0
|
||||||
|
last = df.iloc[-1]
|
||||||
|
|
||||||
|
# 均线排列 (40)
|
||||||
|
ma_cols = [c for c in ["ma5", "ma10", "ma20", "ma60"] if c in df.columns]
|
||||||
|
if len(ma_cols) >= 4 and not any(pd.isna(last[c]) for c in ma_cols):
|
||||||
|
if last["ma5"] > last["ma10"] > last["ma20"] > last["ma60"]:
|
||||||
|
score += 40 # 完美多头
|
||||||
|
elif last["ma5"] > last["ma10"] > last["ma20"]:
|
||||||
|
score += 28
|
||||||
|
elif last["ma5"] > last["ma20"]:
|
||||||
|
score += 15
|
||||||
|
elif "ma5" in df.columns and "ma20" in df.columns:
|
||||||
|
if not pd.isna(last["ma5"]) and not pd.isna(last["ma20"]) and last["ma5"] > last["ma20"]:
|
||||||
|
score += 15
|
||||||
|
|
||||||
|
# 更高高点/更高低点结构 (35)
|
||||||
|
if len(df) >= 20:
|
||||||
|
recent = df.tail(20)
|
||||||
|
# 检查高点抬升
|
||||||
|
first_10_high = recent["high"].iloc[:10].max()
|
||||||
|
second_10_high = recent["high"].iloc[10:].max()
|
||||||
|
# 检查低点抬升
|
||||||
|
first_10_low = recent["low"].iloc[:10].min()
|
||||||
|
second_10_low = recent["low"].iloc[10:].min()
|
||||||
|
|
||||||
|
if second_10_high > first_10_high and second_10_low > first_10_low:
|
||||||
|
score += 35 # 既抬高点又抬低点,最健康
|
||||||
|
elif second_10_high > first_10_high:
|
||||||
|
score += 20 # 至少高点抬升
|
||||||
|
elif second_10_low > first_10_low:
|
||||||
|
score += 12 # 至少低点抬升
|
||||||
|
|
||||||
|
# MA20 方向 (25)
|
||||||
|
if "ma20" in df.columns and len(df) >= 5:
|
||||||
|
ma20_now = last["ma20"]
|
||||||
|
ma20_5d = df.iloc[-5]["ma20"]
|
||||||
|
if not pd.isna(ma20_now) and not pd.isna(ma20_5d) and ma20_5d > 0:
|
||||||
|
ma20_pct = (ma20_now - ma20_5d) / ma20_5d * 100
|
||||||
|
if ma20_pct > 2:
|
||||||
|
score += 25
|
||||||
|
elif ma20_pct > 1:
|
||||||
|
score += 18
|
||||||
|
elif ma20_pct > 0:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
return min(score, 100)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 辅助函数 ──
|
||||||
|
|
||||||
|
|
||||||
def _get_sector_stage(sector_name: str, hot_sectors: list[SectorInfo]) -> str:
|
def _get_sector_stage(sector_name: str, hot_sectors: list[SectorInfo]) -> str:
|
||||||
@ -247,6 +621,41 @@ def _get_sector_stage(sector_name: str, hot_sectors: list[SectorInfo]) -> str:
|
|||||||
return "mid"
|
return "mid"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_sector_heat(sector_name: str, hot_sectors: list[SectorInfo]) -> float:
|
||||||
|
"""获取板块热度得分"""
|
||||||
|
for s in hot_sectors:
|
||||||
|
if s.sector_name == sector_name:
|
||||||
|
return s.heat_score
|
||||||
|
return 30.0
|
||||||
|
|
||||||
|
|
||||||
|
def _score_capital_simple(stock: dict) -> float:
|
||||||
|
"""资金流简单评分(仅基于已有数据,不额外调 API)"""
|
||||||
|
main_net = stock.get("main_net_inflow", 0) or 0
|
||||||
|
inflow_ratio = stock.get("inflow_ratio", 0) or 0
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
if main_net > 10000:
|
||||||
|
score += 60
|
||||||
|
elif main_net > 5000:
|
||||||
|
score += 45
|
||||||
|
elif main_net > 2000:
|
||||||
|
score += 30
|
||||||
|
elif main_net > 0:
|
||||||
|
score += 15
|
||||||
|
|
||||||
|
if inflow_ratio > 15:
|
||||||
|
score += 40
|
||||||
|
elif inflow_ratio > 10:
|
||||||
|
score += 30
|
||||||
|
elif inflow_ratio > 5:
|
||||||
|
score += 20
|
||||||
|
elif inflow_ratio > 0:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
return min(score, 100)
|
||||||
|
|
||||||
|
|
||||||
def _score_to_level(score: float) -> str:
|
def _score_to_level(score: float) -> str:
|
||||||
if score >= 80:
|
if score >= 80:
|
||||||
return "强烈推荐"
|
return "强烈推荐"
|
||||||
@ -259,38 +668,45 @@ def _score_to_level(score: float) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _generate_reasons(
|
def _generate_reasons(
|
||||||
stock: dict, tech: TechnicalSignal | None,
|
stock: dict, entry_signal: dict, tech: TechnicalSignal | None,
|
||||||
market: MarketTemperature, intraday: bool = False,
|
df, intraday: bool = False,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""生成推荐理由"""
|
"""生成推荐理由"""
|
||||||
|
import pandas as pd
|
||||||
reasons = []
|
reasons = []
|
||||||
entry_type = stock.get("entry_signal_type", "none")
|
signal_type = entry_signal.get("signal_type")
|
||||||
signal_map = {"breakout": "突破型", "pullback": "回踩型", "launch": "启动型"}
|
details = entry_signal.get("details", {})
|
||||||
entry_label = signal_map.get(entry_type, "")
|
signal_map = {EntrySignal.BREAKOUT: "突破型", EntrySignal.PULLBACK: "回踩型", EntrySignal.LAUNCH: "启动型"}
|
||||||
|
entry_label = signal_map.get(signal_type, "")
|
||||||
|
|
||||||
# 入场信号
|
# 入场信号
|
||||||
if entry_label:
|
if entry_label and signal_type:
|
||||||
details = stock.get("entry_signal_details", {})
|
st = signal_type.value
|
||||||
if entry_type == "breakout":
|
if st == "breakout":
|
||||||
breakout_pct = details.get("breakout_pct", 0)
|
breakout_pct = details.get("breakout_pct", 0)
|
||||||
vol_ratio = details.get("volume_ratio", 0)
|
vol_ratio = details.get("volume_ratio", 0)
|
||||||
reasons.append(f"放量突破20日阻力位(涨幅{breakout_pct:.1f}%,量比{vol_ratio:.1f}倍)")
|
reasons.append(f"放量突破20日阻力位(涨幅{breakout_pct:.1f}%,量比{vol_ratio:.1f}倍)")
|
||||||
elif entry_type == "pullback":
|
elif st == "pullback":
|
||||||
support = details.get("support_ma", "")
|
support = details.get("support_ma", "")
|
||||||
shrink = details.get("volume_shrink_ratio", 0)
|
shrink = details.get("volume_shrink_ratio", 0)
|
||||||
reasons.append(f"缩量回踩{support}支撑(量能收缩至{shrink:.0%})")
|
reasons.append(f"缩量回踩{support}支撑(量能收缩至{shrink:.0%})")
|
||||||
elif entry_type == "launch":
|
elif st == "launch":
|
||||||
range_pct = details.get("price_range_pct", 0)
|
range_pct = details.get("price_range_pct", 0)
|
||||||
shrink = details.get("volume_shrink_ratio", 0)
|
shrink = details.get("volume_shrink_ratio", 0)
|
||||||
reasons.append(f"高位缩量整理{range_pct:.1f}%后即将变盘(量缩至{shrink:.0%})")
|
reasons.append(f"高位缩量整理{range_pct:.1f}%后即将变盘(量缩至{shrink:.0%})")
|
||||||
|
|
||||||
# 供需分析
|
# 供需分析
|
||||||
vol_trend = stock.get("volume_trend", "")
|
if len(df) >= 10:
|
||||||
ds_ratio = stock.get("demand_supply_ratio", 1)
|
recent = df.tail(10)
|
||||||
if ds_ratio > 1.5:
|
up_days = recent[recent["pct_chg"] > 0]
|
||||||
reasons.append(f"需求主导(上涨均量/下跌均量={ds_ratio:.1f})")
|
down_days = recent[recent["pct_chg"] <= 0]
|
||||||
elif vol_trend == "expanding":
|
if len(up_days) > 0 and len(down_days) > 0:
|
||||||
reasons.append("量能逐步放大,资金持续介入")
|
avg_up_vol = up_days["vol"].mean()
|
||||||
|
avg_down_vol = down_days["vol"].mean()
|
||||||
|
if avg_down_vol > 0:
|
||||||
|
ds_ratio = avg_up_vol / avg_down_vol
|
||||||
|
if ds_ratio > 1.5:
|
||||||
|
reasons.append(f"需求主导(上涨均量/下跌均量={ds_ratio:.1f})")
|
||||||
|
|
||||||
# 资金流
|
# 资金流
|
||||||
main_net = stock.get("main_net_inflow", 0)
|
main_net = stock.get("main_net_inflow", 0)
|
||||||
@ -302,19 +718,7 @@ def _generate_reasons(
|
|||||||
# 板块
|
# 板块
|
||||||
sector = stock.get("sector", "")
|
sector = stock.get("sector", "")
|
||||||
if sector:
|
if sector:
|
||||||
reasons.append(f"所属板块【{sector}】")
|
reasons.append(f"所属热门板块【{sector}】")
|
||||||
|
|
||||||
# 技术面
|
|
||||||
if tech:
|
|
||||||
tech_reasons = []
|
|
||||||
if tech.ma_bullish:
|
|
||||||
tech_reasons.append("均线多头排列")
|
|
||||||
if tech.macd_golden:
|
|
||||||
tech_reasons.append("MACD金叉")
|
|
||||||
if tech.pullback_support:
|
|
||||||
tech_reasons.append("缩量回踩支撑")
|
|
||||||
if tech_reasons:
|
|
||||||
reasons.append("技术面: " + "、".join(tech_reasons))
|
|
||||||
|
|
||||||
return reasons[:3]
|
return reasons[:3]
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -42,6 +42,11 @@ CHAT_SYSTEM_PROMPT = """\
|
|||||||
3. 搜索股票代码
|
3. 搜索股票代码
|
||||||
4. 基于数据给出专业的市场分析和投资建议
|
4. 基于数据给出专业的市场分析和投资建议
|
||||||
|
|
||||||
|
重要提醒:
|
||||||
|
- 回答用户关于"今天市场怎么样"之类的问题时,必须调用 get_realtime_indices 获取实时指数数据
|
||||||
|
- 盘中时段(9:30-15:00)必须使用实时数据,盘后时段使用当日收盘数据
|
||||||
|
- 不要使用过时的数据,必须先调用工具获取最新数据再回答
|
||||||
|
|
||||||
回答要求:
|
回答要求:
|
||||||
1. 使用工具获取最新数据后再回答,不要凭空编造数据
|
1. 使用工具获取最新数据后再回答,不要凭空编造数据
|
||||||
2. 分析要结合 A 股市场特点(资金驱动、板块轮动、情绪周期)
|
2. 分析要结合 A 股市场特点(资金驱动、板块轮动、情绪周期)
|
||||||
|
|||||||
@ -33,6 +33,8 @@ async def execute_tool(name: str, arguments: dict) -> str:
|
|||||||
return await _get_stock_technical_signal(arguments["ts_code"])
|
return await _get_stock_technical_signal(arguments["ts_code"])
|
||||||
elif name == "get_sector_performance":
|
elif name == "get_sector_performance":
|
||||||
return await _get_sector_performance(arguments["sector_name"])
|
return await _get_sector_performance(arguments["sector_name"])
|
||||||
|
elif name == "get_realtime_indices":
|
||||||
|
return await _get_realtime_indices()
|
||||||
else:
|
else:
|
||||||
return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False)
|
return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -146,3 +148,41 @@ async def _get_sector_performance(sector_name: str) -> str:
|
|||||||
return json.dumps({"matched": False, "available_sectors": data}, ensure_ascii=False, default=str)
|
return json.dumps({"matched": False, "available_sectors": data}, ensure_ascii=False, default=str)
|
||||||
data = _clean_for_json([s.model_dump() for s in matched])
|
data = _clean_for_json([s.model_dump() for s in matched])
|
||||||
return json.dumps({"matched": True, "sectors": data}, ensure_ascii=False, default=str)
|
return json.dumps({"matched": True, "sectors": data}, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_realtime_indices() -> str:
|
||||||
|
"""获取指数实时行情数据(盘中用腾讯实时数据)"""
|
||||||
|
from app.data.tencent_client import get_index_realtime
|
||||||
|
from app.config import is_market_session
|
||||||
|
|
||||||
|
is_trading = is_market_session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
index_data = await get_index_realtime()
|
||||||
|
if not index_data:
|
||||||
|
return json.dumps({"error": "获取指数数据失败"}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 格式化数据
|
||||||
|
results = []
|
||||||
|
for ts_code, data in index_data.items():
|
||||||
|
results.append({
|
||||||
|
"ts_code": ts_code,
|
||||||
|
"name": data.get("name", ts_code),
|
||||||
|
"price": round(data.get("price", 0), 2),
|
||||||
|
"pct_chg": round(data.get("pct_chg", 0), 2),
|
||||||
|
"volume": data.get("volume", 0),
|
||||||
|
"amount": data.get("amount", 0),
|
||||||
|
"high": round(data.get("high", 0), 2),
|
||||||
|
"low": round(data.get("low", 0), 2),
|
||||||
|
"open": round(data.get("open", 0), 2),
|
||||||
|
"pre_close": round(data.get("pre_close", 0), 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"is_realtime": is_trading,
|
||||||
|
"mode": "盘中实时" if is_trading else "盘后收盘",
|
||||||
|
"indices": results
|
||||||
|
}, ensure_ascii=False, default=str)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取实时指数失败: {e}")
|
||||||
|
return json.dumps({"error": f"获取指数数据失败: {e}"}, ensure_ascii=False)
|
||||||
|
|||||||
@ -138,4 +138,16 @@ CHAT_TOOLS = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_realtime_indices",
|
||||||
|
"description": "获取指数实时行情数据,包括上证指数、深证成指、创业板指的实时涨跌幅和成交数据。盘中时段返回实时数据,盘后返回当日收盘数据",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -20,6 +20,11 @@
|
|||||||
"static/chunks/webpack.js",
|
"static/chunks/webpack.js",
|
||||||
"static/chunks/main-app.js",
|
"static/chunks/main-app.js",
|
||||||
"static/chunks/app/sectors/page.js"
|
"static/chunks/app/sectors/page.js"
|
||||||
|
],
|
||||||
|
"/_not-found/page": [
|
||||||
|
"static/chunks/webpack.js",
|
||||||
|
"static/chunks/main-app.js",
|
||||||
|
"static/chunks/app/_not-found/page.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
|
"/_not-found/page": "app/_not-found/page.js",
|
||||||
"/page": "app/page.js",
|
"/page": "app/page.js",
|
||||||
"/sectors/page": "app/sectors/page.js",
|
"/recommendations/page": "app/recommendations/page.js",
|
||||||
"/recommendations/page": "app/recommendations/page.js"
|
"/sectors/page": "app/sectors/page.js"
|
||||||
}
|
}
|
||||||
@ -125,7 +125,7 @@
|
|||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/getFullHash */
|
/******/ /* webpack/runtime/getFullHash */
|
||||||
/******/ (() => {
|
/******/ (() => {
|
||||||
/******/ __webpack_require__.h = () => ("b931683b67a20916")
|
/******/ __webpack_require__.h = () => ("37ba5b2074c7bffb")
|
||||||
/******/ })();
|
/******/ })();
|
||||||
/******/
|
/******/
|
||||||
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user