Expand recommend table with gap, daily stats, and client-side sorting

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 17:36:31 +08:00
parent a56370d2af
commit b641a4eaa0
6 changed files with 324 additions and 28 deletions
+124 -7
View File
@@ -64,6 +64,68 @@ def _direction_from_closes(bars: list) -> str:
return TREND_RANGE
def _bar_ohlcv(bar: dict) -> tuple[float, float, float, float, float]:
o, h, l, c = _bar_ohlc(bar)
v = float(bar.get("v") or bar.get("volume") or 0)
return o, h, l, c, v
def compute_daily_quote_stats(bars: list) -> dict:
"""从日线提取:跳空、昨收、今开、昨涨跌、昨振幅、成交量。"""
empty = {
"gap": "",
"gap_label": "",
"gap_pct": None,
"prev_close": None,
"today_open": None,
"yesterday_change": None,
"yesterday_change_pct": None,
"yesterday_amplitude_pct": None,
"volume": None,
}
if len(bars) < 2:
return empty
t_o, _, _, _, t_v = _bar_ohlcv(bars[-1])
y_o, y_h, y_l, y_c, y_v = _bar_ohlcv(bars[-2])
if y_c <= 0:
return empty
prev_close = round(y_c, 4)
today_open = round(t_o, 4) if t_o > 0 else None
gap, gap_label, gap_pct = "none", "", 0.0
if today_open is not None and today_open > y_c:
gap, gap_label = "up", "跳空高开"
gap_pct = (today_open - y_c) / y_c * 100
elif today_open is not None and today_open < y_c:
gap, gap_label = "down", "跳空低开"
gap_pct = (today_open - y_c) / y_c * 100
if len(bars) >= 3:
_, _, _, p_c, _ = _bar_ohlcv(bars[-3])
base = p_c if p_c > 0 else y_o
else:
base = y_o if y_o > 0 else y_c
y_change = y_c - base if base > 0 else None
y_change_pct = (y_change / base * 100) if y_change is not None and base > 0 else None
y_amp = ((y_h - y_l) / base * 100) if base > 0 and y_h >= y_l else None
vol = y_v if y_v > 0 else (t_v if t_v > 0 else None)
return {
"gap": gap,
"gap_label": gap_label,
"gap_pct": round(gap_pct, 2) if gap != "none" else 0.0,
"prev_close": prev_close,
"today_open": today_open,
"yesterday_change": round(y_change, 4) if y_change is not None else None,
"yesterday_change_pct": round(y_change_pct, 2) if y_change_pct is not None else None,
"yesterday_amplitude_pct": round(y_amp, 2) if y_amp is not None else None,
"volume": int(vol) if vol is not None else None,
}
def analyze_daily_trend(bars: list, *, overlap_threshold: float = OVERLAP_RANGE_THRESHOLD) -> dict:
"""根据近一周日线判断走势;最近三天重叠度≥阈值视为震荡。"""
empty = {
@@ -120,6 +182,7 @@ def _normalize_daily_bars(raw: list) -> list:
"h": float(row[2]),
"l": float(row[3]),
"c": float(row[4]),
"v": float(row[5]) if len(row) > 5 and row[5] else 0.0,
})
elif isinstance(row, dict) and row.get("d"):
out.append({
@@ -128,6 +191,7 @@ def _normalize_daily_bars(raw: list) -> list:
"h": float(row.get("h", 0) or 0),
"l": float(row.get("l", 0) or 0),
"c": float(row.get("c", 0) or 0),
"v": float(row.get("v", 0) or 0),
})
return out
@@ -181,17 +245,70 @@ def fetch_week_daily_bars(
return bars[-DAILY_LOOKBACK:] if bars else []
def analyze_product_daily(
symbol: str,
*,
fetch_fn: Callable[[str, str], list] | None = None,
) -> dict:
"""拉取主力合约一周日线:走势 + 跳空/量价统计。"""
sym = (symbol or "").strip()
if not sym:
out = analyze_daily_trend([])
out.update(compute_daily_quote_stats([]))
return out
bars = fetch_week_daily_bars(sym, fetch_fn=fetch_fn)
out = analyze_daily_trend(bars)
out.update(compute_daily_quote_stats(bars))
return out
def analyze_product_trend(
symbol: str,
*,
fetch_fn: Callable[[str, str], list] | None = None,
) -> dict:
"""拉取主力合约一周日线并分析走势。"""
sym = (symbol or "").strip()
if not sym:
return analyze_daily_trend([])
bars = fetch_week_daily_bars(sym, fetch_fn=fetch_fn)
return analyze_daily_trend(bars)
return analyze_product_daily(symbol, fetch_fn=fetch_fn)
GAP_SORT_RANK = {"up": 2, "down": 1, "none": 0, "": -1}
TREND_SORT_RANK = {
TREND_BREAK_LONG: 0,
TREND_BREAK_SHORT: 0,
TREND_LONG: 1,
TREND_SHORT: 2,
TREND_RANGE: 3,
"": 9,
}
def recommend_sort_key(row: dict, sort_by: str = "trend", *, desc: bool = True) -> tuple:
"""可排序字段:trend / gap / volume / amplitude。"""
key = (sort_by or "trend").strip().lower()
if key == "gap":
primary = GAP_SORT_RANK.get(row.get("gap") or "", -1)
secondary = abs(float(row.get("gap_pct") or 0))
elif key == "volume":
primary = float(row.get("volume") or 0)
secondary = 0.0
elif key == "amplitude":
primary = float(row.get("yesterday_amplitude_pct") or 0)
secondary = 0.0
else:
primary = TREND_SORT_RANK.get(row.get("trend") or "", 9)
secondary = -(int(row.get("max_lots") or 0))
if desc:
return (-primary, -secondary, row.get("name") or "")
return (primary, secondary, row.get("name") or "")
def sort_recommend_rows(
rows: list[dict],
*,
sort_by: str = "trend",
desc: bool = True,
) -> list[dict]:
return sorted(rows, key=lambda r: recommend_sort_key(r, sort_by, desc=desc))
def trend_sort_key(row: dict) -> tuple:
@@ -213,4 +330,4 @@ def trend_sort_key(row: dict) -> tuple:
def sort_recommend_by_trend(rows: list[dict]) -> list[dict]:
return sorted(rows, key=trend_sort_key)
return sort_recommend_rows(rows, sort_by="trend", desc=True)