Refine signal formatting, simplify indicators, and fix chart layout
This commit is contained in:
@@ -9,6 +9,7 @@ matplotlib.use("Agg") # non-interactive backend — must be set before other im
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as ticker
|
||||
import mplfinance as mpf
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@@ -35,6 +36,24 @@ def _rsi_series(close: pd.Series, period: int = 14) -> pd.Series:
|
||||
return (100.0 - (100.0 / (1.0 + rs))).fillna(100.0)
|
||||
|
||||
|
||||
def _price_ylim(data: pd.DataFrame, pad_pct: float = 0.15) -> tuple[float, float]:
|
||||
"""Return a padded Y-axis range so candle bodies/wicks are never clipped.
|
||||
|
||||
Without this, mplfinance sometimes auto-zooms too tight and the candles
|
||||
appear as flat lines because the visible high-low range is a tiny fraction
|
||||
of the axis height.
|
||||
|
||||
pad_pct=0.15 adds 15 % of the visible price range above and below.
|
||||
"""
|
||||
lo = float(data["Low"].min())
|
||||
hi = float(data["High"].max())
|
||||
rng = hi - lo
|
||||
if rng == 0:
|
||||
rng = hi * 0.01 # fallback for perfectly flat data
|
||||
pad = rng * pad_pct
|
||||
return lo - pad, hi + pad
|
||||
|
||||
|
||||
def generate(
|
||||
df: pd.DataFrame,
|
||||
pair: str,
|
||||
@@ -46,52 +65,65 @@ def generate(
|
||||
"""Render a candlestick chart with EMA/BB overlays and RSI panel.
|
||||
|
||||
Returns raw PNG bytes for direct use with Telegram reply_photo.
|
||||
|
||||
Key fix: we explicitly set the price-panel Y limits with padding so
|
||||
candle bodies always take up the full vertical space and are never crushed.
|
||||
"""
|
||||
# Compute indicators on full df (warmup), then slice for display
|
||||
# ── Compute indicators on full df for warm-up, then slice for display ──
|
||||
close = df["close"]
|
||||
ema20_full = close.ewm(span=20, adjust=False).mean()
|
||||
ema50_full = close.ewm(span=50, adjust=False).mean()
|
||||
mid_full = close.rolling(20).mean()
|
||||
std_full = close.rolling(20).std(ddof=0)
|
||||
ema20_full = close.ewm(span=20, adjust=False).mean()
|
||||
ema50_full = close.ewm(span=50, adjust=False).mean()
|
||||
mid_full = close.rolling(20).mean()
|
||||
std_full = close.rolling(20).std(ddof=0)
|
||||
bb_upper_full = mid_full + 2.0 * std_full
|
||||
bb_lower_full = mid_full - 2.0 * std_full
|
||||
rsi_full = _rsi_series(close)
|
||||
rsi_full = _rsi_series(close)
|
||||
|
||||
n = min(candles, len(df))
|
||||
n = min(candles, len(df))
|
||||
data = df.tail(n).copy()
|
||||
data.columns = [c.capitalize() for c in data.columns] # mplfinance expects Open/High/Low/Close
|
||||
# mplfinance requires capitalised column names
|
||||
data.columns = [c.capitalize() for c in data.columns]
|
||||
|
||||
ema20 = ema20_full.tail(n).values
|
||||
ema50 = ema50_full.tail(n).values
|
||||
ema20 = ema20_full.tail(n).values
|
||||
ema50 = ema50_full.tail(n).values
|
||||
bb_upper = bb_upper_full.tail(n).values
|
||||
bb_lower = bb_lower_full.tail(n).values
|
||||
rsi_vals = rsi_full.tail(n).values
|
||||
|
||||
# ── Determine padded price range BEFORE plotting ──
|
||||
ylim = _price_ylim(data)
|
||||
|
||||
addplots = [
|
||||
mpf.make_addplot(ema20, color="#00bfff", width=1.4),
|
||||
mpf.make_addplot(ema50, color="#ffa500", width=1.4),
|
||||
mpf.make_addplot(bb_upper, color="#888888", width=0.8, linestyle="--", alpha=0.7),
|
||||
mpf.make_addplot(bb_lower, color="#888888", width=0.8, linestyle="--", alpha=0.7),
|
||||
mpf.make_addplot(rsi_vals, panel=1, color="#b388ff", width=1.2, ylabel="RSI"),
|
||||
mpf.make_addplot(ema20, color="#00bfff", width=1.5),
|
||||
mpf.make_addplot(ema50, color="#ffa500", width=1.5),
|
||||
mpf.make_addplot(bb_upper, color="#888888", width=0.9,
|
||||
linestyle="--", alpha=0.7),
|
||||
mpf.make_addplot(bb_lower, color="#888888", width=0.9,
|
||||
linestyle="--", alpha=0.7),
|
||||
mpf.make_addplot(rsi_vals, panel=1, color="#b388ff", width=1.2,
|
||||
ylabel="RSI"),
|
||||
]
|
||||
|
||||
verdict_label = f" {verdict} {confidence}%" if verdict else ""
|
||||
title = f"{pair} {tf}{verdict_label}"
|
||||
|
||||
# ── Plot — taller figure + tight_layout gives price panel full room ──
|
||||
fig, axes = mpf.plot(
|
||||
data,
|
||||
type="candle",
|
||||
style=_STYLE,
|
||||
addplot=addplots,
|
||||
volume=False,
|
||||
panel_ratios=(3, 1),
|
||||
panel_ratios=(4, 1), # give price panel even more vertical share
|
||||
title=title,
|
||||
returnfig=True,
|
||||
figsize=(11, 6.5),
|
||||
tight_layout=False,
|
||||
figsize=(12, 7), # taller figure so candles breathe
|
||||
tight_layout=False, # must be False to avoid mplfinance spurious vertical line bug
|
||||
ylim=ylim, # ← the critical fix: explicit padded range
|
||||
)
|
||||
|
||||
# axes: [price_ax, price_twin, rsi_ax, ...]
|
||||
# ── RSI panel decoration ──
|
||||
# axes order: [price_ax, price_twin, rsi_ax, ...]
|
||||
rsi_ax = axes[2]
|
||||
rsi_ax.axhline(70, color="#ff5252", linewidth=0.8, linestyle="--", alpha=0.8)
|
||||
rsi_ax.axhline(50, color="#777777", linewidth=0.5, linestyle="--", alpha=0.5)
|
||||
@@ -99,11 +131,12 @@ def generate(
|
||||
rsi_ax.set_ylim(0, 100)
|
||||
rsi_ax.yaxis.set_major_locator(ticker.MultipleLocator(20))
|
||||
|
||||
# EMA legend on price panel
|
||||
# ── EMA / BB legend on price panel ──
|
||||
price_ax = axes[0]
|
||||
price_ax.plot([], [], color="#00bfff", linewidth=1.4, label="EMA 20")
|
||||
price_ax.plot([], [], color="#ffa500", linewidth=1.4, label="EMA 50")
|
||||
price_ax.plot([], [], color="#888888", linewidth=0.8, linestyle="--", label="BB(20,2)")
|
||||
price_ax.plot([], [], color="#00bfff", linewidth=1.5, label="EMA 20 (short-term trend)")
|
||||
price_ax.plot([], [], color="#ffa500", linewidth=1.5, label="EMA 50 (long-term trend)")
|
||||
price_ax.plot([], [], color="#888888", linewidth=0.9, linestyle="--",
|
||||
label="Bollinger Bands")
|
||||
price_ax.legend(
|
||||
loc="upper left",
|
||||
fontsize=7,
|
||||
@@ -112,10 +145,10 @@ def generate(
|
||||
labelcolor="white",
|
||||
)
|
||||
|
||||
fig.subplots_adjust(left=0.08, right=0.97, top=0.92, bottom=0.10, hspace=0.05)
|
||||
fig.subplots_adjust(left=0.08, right=0.92, top=0.92, bottom=0.10, hspace=0.05)
|
||||
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf, format="png", dpi=130, facecolor=fig.get_facecolor())
|
||||
fig.savefig(buf, format="png", dpi=140, facecolor=fig.get_facecolor())
|
||||
plt.close(fig)
|
||||
buf.seek(0)
|
||||
return buf.read()
|
||||
|
||||
Reference in New Issue
Block a user