import io import os import numpy as np from scipy.interpolate import PchipInterpolator import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.dates as mdates from datetime import datetime, timezone, timedelta from fastapi import FastAPI, Header, HTTPException from fastapi.responses import Response from pydantic import BaseModel from typing import List, Tuple app = FastAPI() # Set this to a strong password. You will put this same password in your bot's Render dashboard. API_SECRET = os.environ.get("API_SECRET", "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") class ChartRequest(BaseModel): symbol: str history: List[Tuple[str, float]] current_price: float def verify_request(secret: str): if secret != API_SECRET: raise HTTPException(status_code=401, detail="Unauthorized") @app.get("/") def health_check(): return {"status": "alive"} @app.post("/generate_chart") def generate_chart(req: ChartRequest, secret: str = Header(None)): verify_request(secret) # Reconstruct the timestamps parsed_history = [] for ts_str, price in req.history: dt = datetime.fromisoformat(ts_str) parsed_history.append((dt, price)) bg_color, text_color, grid_color = '#111214', '#b5bac1', '#2b2d31' fig, ax = plt.subplots(figsize=(7, 3)) fig.patch.set_facecolor(bg_color) ax.set_facecolor(bg_color) if not parsed_history: now = datetime.now(timezone.utc) timestamps = [now - timedelta(minutes=5), now] prices = [req.current_price, req.current_price] else: timestamps = [row[0] for row in parsed_history] + [datetime.now(timezone.utc).replace(tzinfo=None)] prices = [row[1] for row in parsed_history] + [req.current_price] unique_data = {t: p for t, p in zip(timestamps, prices)} sorted_times = sorted(unique_data.keys()) sorted_prices = [unique_data[t] for t in sorted_times] color = '#00ff55' if sorted_prices[-1] >= sorted_prices[0] else '#ff2a2a' min_price, max_price = min(sorted_prices), max(sorted_prices) padding = max((max_price - min_price) * 0.1, req.current_price * 0.05) bottom_bound = max(0, min_price - padding) ax.set_ylim(bottom_bound, max_price + padding) if len(sorted_times) >= 3: date_nums = mdates.date2num(sorted_times) x_new = np.linspace(date_nums.min(), date_nums.max(), 300) spl = PchipInterpolator(date_nums, sorted_prices) y_smooth = spl(x_new) ax.plot(x_new, y_smooth, color=color, linewidth=2.5) ax.fill_between(x_new, y_smooth, bottom_bound, color=color, alpha=0.15) ax.xaxis_date() else: ax.plot(sorted_times, sorted_prices, color=color, linewidth=2.5, marker='o', markersize=4) ax.fill_between(sorted_times, sorted_prices, bottom_bound, color=color, alpha=0.15) ax.set_ylabel('Price ($)', color=text_color, fontsize=9) ax.tick_params(axis='x', colors=text_color, labelsize=8) ax.tick_params(axis='y', colors=text_color, labelsize=8) ax.grid(True, linestyle='--', color=grid_color, alpha=0.8) ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) fig.autofmt_xdate(rotation=30) for spine in ax.spines.values(): spine.set_color(grid_color) plt.title(f"{req.symbol} Price Action", color='#ffffff', fontsize=10, pad=10) fig.tight_layout() buf = io.BytesIO() plt.savefig(buf, format='png', dpi=120, bbox_inches='tight', facecolor=fig.get_facecolor()) buf.seek(0) plt.close(fig) return Response(content=buf.read(), media_type="image/png")