Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Generate TradingView-style dark-theme candlestick charts with RSI, MACD, Bollinger Bands, and EMA/SMA using mplfinance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/chart_server.py
1#!/usr/bin/env python32"""3Chart Skill — project-based static server + save APIs45Usage:6python3 chart_server.py [serve_dir] [port]78Defaults:9serve_dir = /data/workspace/output/chart-html10port = 78601112Endpoints:13- POST /save-chart : Save PNG to current project directory as screenshot.png (or filename)14- GET / : Static files from serve_dir1516Notes:17- Gallery and library APIs are intentionally removed.18- Each chart should live in: output/chart-html/<project>/index.html19"""2021import sys22import os23import json24import base6425import re26from pathlib import Path27from urllib.parse import urlsplit28from http.server import HTTPServer, SimpleHTTPRequestHandler2930WORKSPACE = Path("/data/workspace")31DEFAULT_SERVE_DIR = WORKSPACE / "output" / "chart-html"32DEFAULT_SERVE_DIR.mkdir(parents=True, exist_ok=True)333435def _safe_filename(name: str, ext: str) -> str:36name = re.sub(r"[^\w\u4e00-\u9fff.\-]", "_", name or "")37if not name:38name = f"screenshot{ext}"39if not name.endswith(ext):40name += ext41return name424344class ChartHandler(SimpleHTTPRequestHandler):45def __init__(self, *args, serve_dir=None, **kwargs):46self._serve_dir = Path(serve_dir or DEFAULT_SERVE_DIR)47super().__init__(*args, directory=str(self._serve_dir), **kwargs)4849def log_message(self, format, *args):50pass5152def _normalized_path(self):53# Preview proxy may forward as /preview/{id}/<endpoint>54p = self.path.split('?', 1)[0]55m = re.match(r"^/preview/[^/]+/(.*)$", p)56if m:57p = '/' + m.group(1)58return p5960def _translate_preview_path(self, raw_path: str) -> str:61"""Map /preview/<id>/<project>/... to /<project>/... before static lookup."""62# keep query string when present63split = urlsplit(raw_path)64p = split.path65m = re.match(r"^/preview/[^/]+/(.*)$", p)66if m:67mapped = '/' + m.group(1)68else:69mapped = p70if split.query:71mapped = f"{mapped}?{split.query}"72return mapped7374def translate_path(self, path):75# SimpleHTTPRequestHandler resolves filesystem paths via this method.76# We normalize preview-prefixed URLs first so static files map correctly.77return super().translate_path(self._translate_preview_path(path))7879def do_OPTIONS(self):80self.send_response(200)81self.send_header("Access-Control-Allow-Origin", "*")82self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")83self.send_header("Access-Control-Allow-Headers", "Content-Type")84self.end_headers()8586def do_GET(self):87return super().do_GET()8889def do_POST(self):90p = self._normalized_path()91if p in ("/save-chart", "/save-chart/"):92return self._handle_save_chart()93self.send_error(404, "Not found")9495def _read_json_body(self):96length = int(self.headers.get("Content-Length", 0))97raw = self.rfile.read(length)98return json.loads(raw)99100def _send_json(self, data, status=200):101body = json.dumps(data, ensure_ascii=False)102self.send_response(status)103self.send_header("Content-Type", "application/json; charset=utf-8")104self.send_header("Content-Length", str(len(body.encode("utf-8"))))105self.send_header("Access-Control-Allow-Origin", "*")106self.end_headers()107self.wfile.write(body.encode("utf-8"))108109def _resolve_project_dir(self, project: str):110# project like "btc-90d-20260401" or "btc-90d-20260401/subdir"111project = (project or "").strip().strip('/')112if not project:113return None114target = (self._serve_dir / project).resolve()115116# Prevent path traversal117try:118target.relative_to(self._serve_dir.resolve())119except ValueError:120return None121122if not target.exists() or not target.is_dir():123return None124return target125126def _handle_save_chart(self):127try:128data = self._read_json_body()129data_url = data.get("dataUrl", "")130filename = _safe_filename(data.get("filename", "screenshot"), ".png")131project = data.get("project", "")132133project_dir = self._resolve_project_dir(project)134if project_dir is None:135return self._send_json({"ok": False, "error": "Invalid or missing project"}, status=400)136137if "," in data_url:138_, b64 = data_url.split(",", 1)139else:140b64 = data_url141png_bytes = base64.b64decode(b64)142143out_path = project_dir / filename144out_path.write_bytes(png_bytes)145146rel_path = out_path.relative_to(self._serve_dir).as_posix()147self._send_json({"ok": True, "filename": filename, "path": str(out_path), "url": f"/{rel_path}"})148except Exception as e:149self._send_json({"ok": False, "error": str(e)}, status=500)150151152def run(serve_dir, port):153os.chdir(serve_dir)154server = HTTPServer(("127.0.0.1", port), lambda *a, **k: ChartHandler(*a, serve_dir=serve_dir, **k))155print(f"[chart-server] serving {serve_dir} on port {port}", flush=True)156server.serve_forever()157158159if __name__ == "__main__":160serve_dir = sys.argv[1] if len(sys.argv) > 1 else str(DEFAULT_SERVE_DIR)161port = int(sys.argv[2]) if len(sys.argv) > 2 else 7860162Path(serve_dir).mkdir(parents=True, exist_ok=True)163run(serve_dir, port)164