复盘盈亏比自动计算与K线自动生成;居中页头导航

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 12:58:24 +08:00
parent 105f630388
commit a35a08d2f6
10 changed files with 352 additions and 56 deletions
+257
View File
@@ -0,0 +1,257 @@
"""复盘 K 线:新浪拉取 + matplotlib 生成截图。"""
import json
import logging
import os
import re
from datetime import datetime
from typing import Optional
from zoneinfo import ZoneInfo
import requests
from symbols import ths_to_codes
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
PERIOD_MINUTES = {
"1m": "1",
"3m": "3",
"5m": "5",
"15m": "15",
"30m": "30",
"1h": "60",
"4h": "240",
}
def ths_to_sina_chart_symbol(symbol: str) -> Optional[str]:
"""ag2608 -> AG2608(新浪 K 线接口合约代码)。"""
code = (symbol or "").strip()
if not code:
return None
codes = ths_to_codes(code)
if codes:
sina = codes.get("sina_code", "")
if sina.startswith("nf_"):
return sina[3:]
if sina.startswith("CFF_RE_"):
return sina[7:]
ths = codes.get("ths_code", "")
return ths.upper() if ths else None
m = re.match(r"^([A-Za-z]+)(\d+)$", code)
if m:
return m.group(1).upper() + m.group(2)
return None
def _parse_jsonp(text: str) -> Optional[list]:
m = re.search(r"\((.*)\)\s*;?\s*$", text.strip(), re.DOTALL)
if not m:
return None
try:
data = json.loads(m.group(1))
return data if isinstance(data, list) else None
except json.JSONDecodeError:
return None
def fetch_sina_klines(symbol: str, period: str) -> list:
"""拉取新浪期货分钟 K 线。"""
chart_sym = ths_to_sina_chart_symbol(symbol)
if not chart_sym:
return []
if period == "1d":
return _fetch_sina_daily(chart_sym)
typ = PERIOD_MINUTES.get(period)
if not typ:
return []
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
url = (
"https://stock2.finance.sina.com.cn/futures/api/jsonp.php/"
f"var_{chart_sym}_{typ}_{ts}=/InnerFuturesNewService.getFewMinLine"
f"?symbol={chart_sym}&type={typ}"
)
try:
resp = requests.get(
url,
timeout=20,
headers={"Referer": "https://finance.sina.com.cn"},
)
bars = _parse_jsonp(resp.text)
return bars or []
except Exception as exc:
logger.warning("fetch kline failed %s %s: %s", chart_sym, period, exc)
return []
def _fetch_sina_daily(chart_sym: str) -> list:
url = (
"https://stock2.finance.sina.com.cn/futures/api/json.php/"
f"IndexService.getInnerFuturesDailyKLine?symbol={chart_sym}"
)
try:
resp = requests.get(url, timeout=20, headers={"Referer": "https://finance.sina.com.cn"})
raw = resp.json()
if not raw:
return []
out = []
for row in raw:
if isinstance(row, list) and len(row) >= 5:
out.append({
"d": row[0],
"o": row[1],
"h": row[2],
"l": row[3],
"c": row[4],
})
return out
except Exception as exc:
logger.warning("fetch daily kline failed %s: %s", chart_sym, exc)
return []
def _parse_dt(value: str) -> Optional[datetime]:
if not value:
return None
v = value.strip().replace("T", " ")
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
try:
return datetime.strptime(v, fmt).replace(tzinfo=TZ)
except ValueError:
continue
try:
return datetime.fromisoformat(value.strip()).replace(tzinfo=TZ)
except ValueError:
return None
def _bar_datetime(bar: dict) -> Optional[datetime]:
d = bar.get("d")
if not d:
return None
try:
return datetime.strptime(d, "%Y-%m-%d %H:%M:%S").replace(tzinfo=TZ)
except ValueError:
return None
def _select_bars(
bars: list,
cutoff: datetime,
count: int,
) -> list:
filtered = []
for bar in bars:
dt = _bar_datetime(bar)
if dt and dt <= cutoff:
filtered.append(bar)
if not filtered:
filtered = bars
if count > 0 and len(filtered) > count:
filtered = filtered[-count:]
return filtered
def generate_review_kline_chart(
symbol: str,
periods: list[str],
count: int,
cutoff_label: str,
open_time: str,
close_time: str,
entry_price: Optional[float],
stop_loss: Optional[float],
take_profit: Optional[float],
close_price: Optional[float],
upload_dir: str,
) -> Optional[str]:
"""生成双周期 K 线复盘图,返回 uploads 目录下的文件名。"""
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
now = datetime.now(TZ)
if cutoff_label == "开仓时间":
cutoff = _parse_dt(open_time) or now
elif cutoff_label == "当前时间":
cutoff = now
else:
cutoff = _parse_dt(close_time) or now
open_dt = _parse_dt(open_time)
close_dt = _parse_dt(close_time)
valid_periods = [p for p in periods if p]
if not valid_periods:
valid_periods = ["15m", "1h"]
fig, axes = plt.subplots(
len(valid_periods), 1,
figsize=(14, 4.5 * len(valid_periods)),
facecolor="#0a0a10",
squeeze=False,
)
plotted = False
for idx, period in enumerate(valid_periods):
ax = axes[idx, 0]
bars = fetch_sina_klines(symbol, period)
bars = _select_bars(bars, cutoff, count)
if not bars:
ax.set_facecolor("#12121a")
ax.text(0.5, 0.5, f"No {period} data", ha="center", va="center", color="#888")
ax.set_xticks([])
ax.set_yticks([])
continue
times = [_bar_datetime(b) for b in bars]
closes = [float(b["c"]) for b in bars]
highs = [float(b["h"]) for b in bars]
lows = [float(b["l"]) for b in bars]
ax.set_facecolor("#12121a")
ax.plot(times, closes, color="#4cc2ff", linewidth=1.2)
ax.fill_between(
times, lows, highs,
color="#4cc2ff", alpha=0.12,
)
levels = [
(entry_price, "#eac147", "Entry"),
(stop_loss, "#ff6666", "SL"),
(take_profit, "#4cd97f", "TP"),
(close_price, "#c4c4ff", "Close"),
]
for price, color, label in levels:
if price is not None:
ax.axhline(price, color=color, linewidth=0.9, linestyle="--", alpha=0.85)
ax.text(times[-1], price, label, color=color, fontsize=8, va="bottom")
if open_dt:
ax.axvline(open_dt, color="#888", linewidth=0.8, linestyle=":", alpha=0.7)
if close_dt:
ax.axvline(close_dt, color="#aaa", linewidth=0.8, linestyle=":", alpha=0.7)
chart_sym = ths_to_sina_chart_symbol(symbol) or symbol
ax.set_title(f"{chart_sym} {period}", color="#eaeaea", fontsize=11, pad=8)
ax.tick_params(colors="#888", labelsize=8)
for spine in ax.spines.values():
spine.set_color("#2e2e45")
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d %H:%M"))
ax.grid(True, color="#1e1e30", linewidth=0.5)
plotted = True
if not plotted:
plt.close(fig)
return None
fig.tight_layout()
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
chart_sym = ths_to_sina_chart_symbol(symbol) or "chart"
filename = f"{ts}_kline_{chart_sym}.png"
path = os.path.join(upload_dir, filename)
fig.savefig(path, dpi=120, facecolor=fig.get_facecolor())
plt.close(fig)
return filename