
Charting
- 4.4k installs
- 18 repo stars
- Updated July 27, 2026
- starchild-ai-agent/official-skills
charting is an agent skill for Generate TradingView-style candlestick charts with indicators. Use when the user wants a visual chart, price visualizati
About
The charting skill generate TradingView-style candlestick charts with indicators. Use when the user wants a visual chart, price visualization, or technical analysis plot.. Charting ⚠️ CRITICAL: DO NOT CALL DATA TOOLS NEVER call price_chart , get_coin_ohlc_range_by_id , twelvedata_time_series , or ANY market data tools when creating charts. Chart scripts fetch data internally. Calling these tools floods your context with 78KB+ of unnecessary data. Read template from skills/charting/scripts/ 2. Call read_file on the output PNG, then display it using markdown image syntax: !Chart description --- You generate TradingView-quality candlestick charts. Dark theme, clean layout, professional colors. Every chart is a standalone Python script - no internal imports. Additional Rules: - Chart scripts run in workspace and cannot import from core . Use it when developers need charting tasks covered in the upstream ECC or community skill documentation with concrete commands, prerequisites, and workflow steps rather than guessing APIs or conventions.
- Generate TradingView-style candlestick charts with indicators. Use when the user wants a visual chart, price visualizati
- Chart scripts run in workspace and cannot import from `core`. Use `requests` library directly, NOT `proxied_get()`.
- Templates include proxy auto-configuration. If `PROXY_HOST` env var exists, scripts automatically configure `HTTP_PROXY`
- `chart_template.py` - Baseline candlestick chart with TradingView styling (crypto via CoinGecko)
- `chart_with_indicators.py` - RSI, MACD, Bollinger Bands, EMA/SMA examples (crypto via CoinGecko)
Charting by the numbers
- 4,424 all-time installs (skills.sh)
- Ranked #285 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: CRITICAL risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
charting capabilities & compatibility
- Capabilities
- generate tradingview style candlestick charts wi · chart scripts run in workspace and cannot import · templates include proxy auto configuration. if `
What charting says it does
IMPORTANT:** Twelve Data returns data in **reverse chronological order** (newest first).
CRITICAL: Do NOT use proxied_get() in chart scripts.
npx skills add https://github.com/starchild-ai-agent/official-skills --skill chartingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4.4k |
|---|---|
| repo stars | ★ 18 |
| Security audit | 1 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | starchild-ai-agent/official-skills ↗ |
How do I run charting tasks with correct setup and documented commands?
Generate TradingView-style candlestick charts with indicators. Use when the user wants a visual chart, price visualization, or technical analysis plot.
Who is it for?
Developers automating charting via agent-guided SKILL.md workflows.
Skip if: Skip when unrelated tooling already covers the task without this skill's documented flow.
When should I use this skill?
Generate TradingView-style candlestick charts with indicators. Use when the user wants a visual chart, price visualization, or technical analysis plot.
What you get
Repeatable charting workflows with grounded commands and expected outputs.
- candlestick chart image
- technical indicator overlay
- TradingView-style price visualization
By the numbers
- Skill version 1.0.0
- Installs 3 pip packages: mplfinance, pandas, numpy
- Requires COINGECKO_API_KEY environment variable
Files
Charting
⚠️ CRITICAL: DO NOT CALL DATA TOOLS
NEVER call price_chart, get_coin_ohlc_range_by_id, twelvedata_time_series, or ANY market data tools when creating charts. Chart scripts fetch data internally. Calling these tools floods your context with 78KB+ of unnecessary data.
Workflow (4 steps): 1. Read template from skills/charting/scripts/ 2. Write script to scripts/ 3. Run script with bash 4. Call read_file on the output PNG, then display it using markdown image syntax: 
---
You generate TradingView-quality candlestick charts. Dark theme, clean layout, professional colors. Every chart is a standalone Python script — no internal imports.
Additional Rules:
- Chart scripts run in workspace and cannot import from
core. Userequestslibrary directly, NOTproxied_get(). - Templates include proxy auto-configuration. If
PROXY_HOSTenv var exists, scripts automatically configureHTTP_PROXY/HTTPS_PROXY.
Tools: write_file, bash, read_file
When to Use Which Chart
Simple price action → Candlestick with no indicators. Good for "show me BTC this month." Trend analysis → Add EMA/SMA overlays. Good for "is ETH in an uptrend?" Momentum check → Add RSI or MACD as subplots. Good for "is SOL overbought?" Full technical view → Candles + Bollinger Bands + RSI + MACD. Good for "give me the full picture on BTC." Volume analysis → Requires separate fetch from market_chart endpoint (OHLC endpoint has no volume). Asset comparison → Line chart comparing two assets (BTC vs Gold, ETH vs S&P500, etc.). Use comparison template for normalized or percentage-based comparisons.
How to Build Charts
Read and customize the template scripts in skills/charting/scripts/:
chart_template.py— Baseline candlestick chart with TradingView styling (crypto via CoinGecko)chart_with_indicators.py— RSI, MACD, Bollinger Bands, EMA/SMA examples (crypto via CoinGecko)chart_stock_template.py— Stock/forex chart using Twelve Data APIchart_comparison_template.py— Compare two assets (crypto vs commodity, stock vs crypto, etc.)
Copy the relevant template to scripts/, customize the config section (coin, days, indicators), and run it.
The templates handle all data fetching internally with retry logic and error handling.
Note: These templates are for market data visualization (price charts, indicators). For backtest result charts (equity curves, drawdowns, performance dashboards), add matplotlib charting directly to your backtest script — the data is already there, no need to re-fetch or create a separate file.
TradingView Color Palette
| Element | Color | Hex |
|---|---|---|
| Up candles | Teal | #26a69a |
| Down candles | Red | #ef5350 |
| Background | Dark | #131722 |
| Grid | Subtle dotted | #1e222d |
| Text / axes | Light gray | #d1d4dc |
| MA lines | Blue / Orange | #2196f3 / #ff9800 |
| RSI line | Purple | #b39ddb |
| MACD line | Blue | #2196f3 |
| Signal line | Orange | #ff9800 |
Do not deviate from this palette unless the user asks.
Data Source APIs
CoinGecko (Crypto Only)
Endpoint: https://pro-api.coingecko.com/api/v3/coins/{coin_id}/ohlc/range Auth: Header x-cg-pro-api-key: {COINGECKO_API_KEY} Use for: BTC, ETH, SOL, and all cryptocurrencies
Example:
url = f"https://pro-api.coingecko.com/api/v3/coins/{COIN_ID}/ohlc/range"
params = {"vs_currency": "usd", "from": from_ts, "to": now, "interval": "daily"}
headers = {"x-cg-pro-api-key": os.getenv("COINGECKO_API_KEY")}
resp = requests.get(url, params=params, headers=headers)
raw = resp.json() # [[timestamp_ms, open, high, low, close], ...]Twelve Data (Stocks, Forex, Commodities)
Endpoint: https://api.twelvedata.com/time_series Auth: Query param apikey={TWELVEDATA_API_KEY} Use for: Stocks (AAPL, MSFT), Forex (EUR/USD), Commodities (XAU/USD for gold)
Common Symbols:
- Stocks:
AAPL,MSFT,GOOGL,TSLA,SPY - Forex:
EUR/USD,GBP/JPY,USD/CHF - Commodities:
XAU/USD(gold),XAG/USD(silver),CL/USD(crude oil)
Intervals: 1min, 5min, 15min, 30min, 1h, 4h, 1day, 1week, 1month
Example:
url = "https://api.twelvedata.com/time_series"
params = {
"symbol": "XAU/USD", # Gold spot price
"interval": "1day",
"outputsize": 90, # Number of candles
"apikey": os.getenv("TWELVEDATA_API_KEY")
}
resp = requests.get(url, params=params)
data = resp.json()
# data["values"] = [{"datetime": "2024-01-01", "open": "2050.00", "high": "2060.00", ...}, ...]IMPORTANT: Twelve Data returns data in reverse chronological order (newest first). Always reverse the list before creating a DataFrame:
values = data["values"][::-1] # Reverse to oldest-firstInterval Selection Strategy
The templates now auto-select optimal intervals to minimize data volume while maintaining visual quality:
| Time Range | Auto-Selected Interval | Rationale |
|---|---|---|
| ≤31 days | Hourly | High granularity for short-term analysis |
| 32-365 days | Daily | Sufficient detail, lower data volume |
| >365 days | Daily | Daily is optimal for long-term trends |
Override: Set INTERVAL = "daily" or INTERVAL = "hourly" in the config to override auto-selection.
Key Gotchas
- `savefig` facecolor: You MUST set
facecolor='#131722'andedgecolor='#131722'insavefig, or the saved PNG reverts to white background. - Title spacing: Prefix titles with
\nto add spacing from the top edge. - `returnfig=True`: Use when you need post-plot customization (price formatting, annotations). When using it, call
fig.savefig()manually — don't passsavefigtompf.plot(). - No volume in OHLC: CoinGecko OHLC endpoint returns
[timestamp_ms, open, high, low, close]only. Usevolume=Falseor fetch volume separately fromcoin_chartendpoint. - Panel ratios: Set
panel_ratioswhen adding indicator subplots. E.g.,(4, 1, 2)for candles + volume + one indicator,(5, 1, 2, 2)for two indicators. - Figure size: Default
(14, 8). Increase to(14, 10)or(14, 12)when adding subplots.
Rules
- Paths are relative to workspace. Write to
scripts/foo.py, notworkspace/scripts/foo.py. The bash CWD is already workspace. - Always save to `output/` directory. Use
os.makedirs("output", exist_ok=True). - Always run the script with `bash("python3 scripts/<name>.py")` to verify it works.
- Always call `read_file` on the generated PNG, then use markdown image syntax to display it:
 - Scripts must be standalone. Use
requests+os.getenv(). No internal imports, no dotenv. - CRITICAL: Do NOT use proxied_get() in chart scripts. Chart scripts are standalone and run in the workspace - they cannot import from
core.http_client. Always userequests.get()andrequests.post()directly. This is an exception to the PLATFORM.md proxy rules because these scripts execute outside the main Star Child process. The templates demonstrate the correct pattern. - Env vars are inherited.
os.getenv("COINGECKO_API_KEY")works directly. - Default to dark theme unless user asks for light.
- Filename should describe the chart. e.g.
btc_30d_candles.png,eth_7d_rsi_macd.png. - Data sources: Use CoinGecko API for crypto (BTC, ETH, etc). Use Twelve Data API for stocks, forex, and commodities (AAPL, EUR/USD, XAU/USD for gold). Never mix APIs - keep scripts focused on one data source.
- Think about what you're measuring: Before creating a chart, ask yourself: "What question is the user trying to answer?" A normalized chart (all start at 100) shows relative trends but hides actual gain magnitude. If the user wants to know "which gained more" or is comparing investment performance, they need the actual multipliers (e.g., 50x vs 10x), not just lines that look similar.
Troubleshooting
401 Unauthorized Errors
Templates auto-configure proxy from PROXY_HOST/PROXY_PORT env vars. If 401 errors occur:
Check environment:
bash("env | grep -E 'PROXY|REQUESTS_CA'")Expected vars:
PROXY_HOST/PROXY_PORT- Proxy address (templates use these to set HTTP_PROXY/HTTPS_PROXY)REQUESTS_CA_BUNDLE- Proxy CA cert for SSLCOINGECKO_API_KEY/TWELVEDATA_API_KEY- Can be fake in proxied environments
If vars are missing, this is an environment configuration issue, not a script issue.
#!/usr/bin/env python3
"""Compare two assets (crypto vs commodity, stock vs crypto, etc.) with multiple visualization modes.
Supports three comparison modes:
1. Normalized to 100 - Both assets start at 100, shows relative performance
2. Dual-axis - Actual prices on separate Y axes
3. Percentage change - Shows +/- percentage from starting point
Usage: Copy to scripts/, customize the Config section, run with:
python3 scripts/my_comparison.py
"""
import os
import sys
import requests
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import time
# ============================================================
# Proxy Auto-Configuration (for deployed environments)
# ============================================================
if not os.getenv("HTTP_PROXY") and os.getenv("PROXY_HOST"):
host = os.getenv("PROXY_HOST")
port = os.getenv("PROXY_PORT", "8080")
# Handle IPv6 addresses
if ":" in host and not host.startswith("["):
host = f"[{host}]"
proxy_url = f"http://{host}:{port}"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
# ============================================================
# Config — customize these for each comparison
# ============================================================
# Asset 1 (Crypto via CoinGecko)
ASSET1_TYPE = "crypto" # "crypto"
ASSET1_ID = "bitcoin" # CoinGecko coin ID
ASSET1_LABEL = "BTC"
ASSET1_COLOR = "#f7931a" # Bitcoin orange
# Asset 2 (Stock/Forex/Commodity via Twelve Data)
ASSET2_TYPE = "twelvedata" # "twelvedata"
ASSET2_SYMBOL = "XAU/USD" # Twelve Data symbol (XAU/USD for gold, AAPL for Apple, EUR/USD for forex)
ASSET2_LABEL = "Gold"
ASSET2_COLOR = "#ffd700" # Gold color
# Time range
DAYS = 90
# Comparison mode: "normalized", "dual_axis", or "percentage"
COMPARISON_MODE = "normalized"
# Output
OUTPUT_FILE = "output/btc_vs_gold_comparison.png"
# ============================================================
# Fetch Asset 1 (Crypto from CoinGecko)
# ============================================================
def fetch_crypto_data(coin_id, days):
"""Fetch crypto OHLC data from CoinGecko."""
api_key = os.getenv("COINGECKO_API_KEY")
if not api_key:
print("ERROR: COINGECKO_API_KEY not set", file=sys.stderr)
sys.exit(1)
now = int(time.time())
from_ts = now - (days * 86400)
# Auto-select interval
interval = "hourly" if days <= 31 else "daily"
url = f"https://pro-api.coingecko.com/api/v3/coins/{coin_id}/ohlc/range"
params = {"vs_currency": "usd", "from": from_ts, "to": now, "interval": interval}
headers = {"x-cg-pro-api-key": api_key}
# Retry logic
for attempt in range(3):
try:
resp = requests.get(url, params=params, headers=headers, timeout=15)
resp.raise_for_status()
raw = resp.json()
if not isinstance(raw, list) or len(raw) == 0:
raise ValueError("Empty or invalid data received from CoinGecko")
# Convert to DataFrame
df = pd.DataFrame(raw, columns=["Timestamp", "Open", "High", "Low", "Close"])
df["Date"] = pd.to_datetime(df["Timestamp"], unit="ms")
df.set_index("Date", inplace=True)
df.drop(columns=["Timestamp"], inplace=True)
return df
except Exception as e:
if attempt == 2:
print(f"ERROR: Failed to fetch {coin_id} data: {e}", file=sys.stderr)
sys.exit(1)
time.sleep(2 ** attempt)
# ============================================================
# Fetch Asset 2 (Stock/Forex/Commodity from Twelve Data)
# ============================================================
def fetch_twelvedata(symbol, days):
"""Fetch time series data from Twelve Data."""
api_key = os.getenv("TWELVEDATA_API_KEY")
if not api_key:
print("ERROR: TWELVEDATA_API_KEY not set", file=sys.stderr)
sys.exit(1)
# Auto-select interval
interval = "1h" if days <= 31 else "1day"
# Calculate outputsize (Twelve Data returns newest first)
outputsize = days * 24 if interval == "1h" else days
outputsize = min(outputsize, 5000) # Max 5000 points
url = "https://api.twelvedata.com/time_series"
params = {
"symbol": symbol,
"interval": interval,
"outputsize": outputsize,
"apikey": api_key
}
# Retry logic
for attempt in range(3):
try:
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
if "status" in data and data["status"] == "error":
raise ValueError(f"Twelve Data API Error: {data.get('message', 'Unknown error')}")
if "values" not in data or len(data["values"]) == 0:
raise ValueError("Empty or invalid data received from Twelve Data")
# Reverse data (Twelve Data returns newest first)
values = data["values"][::-1]
# Convert to DataFrame
df = pd.DataFrame(values)
df["Date"] = pd.to_datetime(df["datetime"])
df.set_index("Date", inplace=True)
df = df[["open", "high", "low", "close"]].astype(float)
df.columns = ["Open", "High", "Low", "Close"]
return df
except Exception as e:
if attempt == 2:
print(f"ERROR: Failed to fetch {symbol} data: {e}", file=sys.stderr)
sys.exit(1)
time.sleep(2 ** attempt)
# ============================================================
# Fetch Data
# ============================================================
print(f"Fetching {ASSET1_LABEL} data...")
df1 = fetch_crypto_data(ASSET1_ID, DAYS)
print(f"Fetching {ASSET2_LABEL} data...")
df2 = fetch_twelvedata(ASSET2_SYMBOL, DAYS)
# Align dates (use Close prices for comparison)
df1_close = df1[["Close"]].rename(columns={"Close": ASSET1_LABEL})
df2_close = df2[["Close"]].rename(columns={"Close": ASSET2_LABEL})
# Merge on date index (inner join to get overlapping dates)
df = pd.merge(df1_close, df2_close, left_index=True, right_index=True, how="inner")
if len(df) == 0:
print("ERROR: No overlapping data between assets", file=sys.stderr)
sys.exit(1)
print(f"Comparing {len(df)} data points...")
# ============================================================
# TradingView Dark Theme
# ============================================================
plt.style.use('dark_background')
fig, ax = plt.subplots(figsize=(14, 8))
fig.patch.set_facecolor('#131722')
ax.set_facecolor('#131722')
# Grid
ax.grid(color='#1e222d', linestyle='--', linewidth=0.5)
# ============================================================
# Plot Based on Comparison Mode
# ============================================================
if COMPARISON_MODE == "normalized":
# Normalize both to 100 at start
df_norm = df / df.iloc[0] * 100
ax.plot(df_norm.index, df_norm[ASSET1_LABEL], color=ASSET1_COLOR, linewidth=2, label=ASSET1_LABEL)
ax.plot(df_norm.index, df_norm[ASSET2_LABEL], color=ASSET2_COLOR, linewidth=2, label=ASSET2_LABEL)
ax.set_ylabel("Normalized Value (Start = 100)", color='#d1d4dc', fontsize=12)
ax.axhline(100, color='#d1d4dc', linestyle='--', linewidth=0.7, alpha=0.5)
title = f"{ASSET1_LABEL} vs {ASSET2_LABEL} — Normalized Comparison ({DAYS}D)"
elif COMPARISON_MODE == "dual_axis":
# Dual axis with actual prices
ax.plot(df.index, df[ASSET1_LABEL], color=ASSET1_COLOR, linewidth=2, label=ASSET1_LABEL)
ax.set_ylabel(f"{ASSET1_LABEL} Price (USD)", color=ASSET1_COLOR, fontsize=12)
ax.tick_params(axis='y', labelcolor=ASSET1_COLOR)
# Create second Y axis
ax2 = ax.twinx()
ax2.plot(df.index, df[ASSET2_LABEL], color=ASSET2_COLOR, linewidth=2, label=ASSET2_LABEL)
ax2.set_ylabel(f"{ASSET2_LABEL} Price (USD)", color=ASSET2_COLOR, fontsize=12)
ax2.tick_params(axis='y', labelcolor=ASSET2_COLOR)
ax2.set_facecolor('#131722')
# Combine legends
lines1, labels1 = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax.legend(lines1 + lines2, labels1 + labels2, loc='upper left',
facecolor='#131722', edgecolor='#1e222d', fontsize=10)
title = f"{ASSET1_LABEL} vs {ASSET2_LABEL} — Dual-Axis Comparison ({DAYS}D)"
elif COMPARISON_MODE == "percentage":
# Percentage change from start
df_pct = (df / df.iloc[0] - 1) * 100
ax.plot(df_pct.index, df_pct[ASSET1_LABEL], color=ASSET1_COLOR, linewidth=2, label=ASSET1_LABEL)
ax.plot(df_pct.index, df_pct[ASSET2_LABEL], color=ASSET2_COLOR, linewidth=2, label=ASSET2_LABEL)
ax.set_ylabel("Change from Start (%)", color='#d1d4dc', fontsize=12)
ax.axhline(0, color='#d1d4dc', linestyle='--', linewidth=0.7, alpha=0.5)
title = f"{ASSET1_LABEL} vs {ASSET2_LABEL} — Percentage Change ({DAYS}D)"
else:
print(f"ERROR: Invalid COMPARISON_MODE '{COMPARISON_MODE}'. Use 'normalized', 'dual_axis', or 'percentage'.", file=sys.stderr)
sys.exit(1)
# ============================================================
# Styling
# ============================================================
ax.set_title(f"\n{title}", color='#d1d4dc', fontsize=14, fontweight='bold')
ax.set_xlabel("Date", color='#d1d4dc', fontsize=12)
ax.tick_params(colors='#d1d4dc')
# Date formatting
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
ax.xaxis.set_major_locator(mdates.AutoDateLocator())
fig.autofmt_xdate()
# Legend (skip if dual_axis since we handled it above)
if COMPARISON_MODE != "dual_axis":
ax.legend(loc='upper left', facecolor='#131722', edgecolor='#1e222d', fontsize=10)
# ============================================================
# Save
# ============================================================
os.makedirs("output", exist_ok=True)
plt.tight_layout()
plt.savefig(OUTPUT_FILE, dpi=150, facecolor='#131722', edgecolor='#131722')
print(f"Chart saved: {OUTPUT_FILE}")
# Print summary stats
start_val1 = df.iloc[0][ASSET1_LABEL]
end_val1 = df.iloc[-1][ASSET1_LABEL]
change1 = ((end_val1 / start_val1) - 1) * 100
start_val2 = df.iloc[0][ASSET2_LABEL]
end_val2 = df.iloc[-1][ASSET2_LABEL]
change2 = ((end_val2 / start_val2) - 1) * 100
print(f"\n{ASSET1_LABEL}: ${start_val1:.2f} → ${end_val1:.2f} ({change1:+.2f}%)")
print(f"{ASSET2_LABEL}: ${start_val2:.2f} → ${end_val2:.2f} ({change2:+.2f}%)")
#!/usr/bin/env python3
"""Generate TradingView-style candlestick chart for stocks, forex, or commodities using Twelve Data.
Usage: Copy to scripts/, customize the Config section, run with:
python3 scripts/my_stock_chart.py
"""
import os
import sys
import requests
import pandas as pd
import mplfinance as mpf
import time
# ============================================================
# Proxy Auto-Configuration (for deployed environments)
# ============================================================
if not os.getenv("HTTP_PROXY") and os.getenv("PROXY_HOST"):
host = os.getenv("PROXY_HOST")
port = os.getenv("PROXY_PORT", "8080")
# Handle IPv6 addresses
if ":" in host and not host.startswith("["):
host = f"[{host}]"
proxy_url = f"http://{host}:{port}"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
# ============================================================
# Config — customize these for each chart
# ============================================================
SYMBOL = "AAPL" # Stock (AAPL, MSFT, TSLA), Forex (EUR/USD), or Commodity (XAU/USD for gold)
DAYS = 30
INTERVAL = None # None for auto-select, or "1h", "1day", etc.
OUTPUT_FILE = "output/aapl_30d_chart.png"
# ============================================================
# Smart Interval Selection
# ============================================================
def select_interval(days):
"""Auto-select optimal interval based on time range."""
if days <= 31:
return "1h" # Hourly for short term
else:
return "1day" # Daily for longer periods
if INTERVAL is None:
INTERVAL = select_interval(DAYS)
# ============================================================
# Fetch OHLC from Twelve Data with Error Handling
# ============================================================
API_KEY = os.getenv("TWELVEDATA_API_KEY")
if not API_KEY:
print("ERROR: TWELVEDATA_API_KEY not set", file=sys.stderr)
sys.exit(1)
# Calculate outputsize
if INTERVAL == "1h":
outputsize = DAYS * 24
else: # daily or longer
outputsize = DAYS
outputsize = min(outputsize, 5000) # Max 5000 points
url = "https://api.twelvedata.com/time_series"
params = {
"symbol": SYMBOL,
"interval": INTERVAL,
"outputsize": outputsize,
"apikey": API_KEY
}
# Retry logic with exponential backoff
raw = None
for attempt in range(3):
try:
resp = requests.get(url, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
# Check for API errors
if "status" in data and data["status"] == "error":
raise ValueError(f"Twelve Data API Error: {data.get('message', 'Unknown error')}")
# Validate response
if "values" not in data or len(data["values"]) == 0:
raise ValueError("Empty or invalid data received from API")
raw = data["values"]
break # Success
except Exception as e:
if attempt == 2: # Last attempt
print(f"ERROR: Failed to fetch data after 3 attempts: {e}", file=sys.stderr)
sys.exit(1)
time.sleep(2 ** attempt) # Exponential backoff: 1s, 2s
# ============================================================
# Build DataFrame
# ============================================================
# IMPORTANT: Twelve Data returns newest first, so reverse the list
raw = raw[::-1]
df = pd.DataFrame(raw)
df["Date"] = pd.to_datetime(df["datetime"])
df.set_index("Date", inplace=True)
# Convert to float and rename columns
df = df[["open", "high", "low", "close"]].astype(float)
df.columns = ["Open", "High", "Low", "Close"]
# Optional: Add volume if available (Twelve Data includes volume for stocks)
if "volume" in raw[0]:
df["Volume"] = [float(x["volume"]) for x in raw]
# ============================================================
# TradingView Style
# ============================================================
mc = mpf.make_marketcolors(
up='#26a69a', down='#ef5350',
edge='inherit', wick='inherit', volume='inherit', ohlc='inherit',
)
style = mpf.make_mpf_style(
marketcolors=mc,
facecolor='#131722', edgecolor='#131722', figcolor='#131722',
gridcolor='#1e222d', gridstyle='--', y_on_right=True,
rc={'axes.labelcolor': '#d1d4dc', 'xtick.color': '#d1d4dc',
'ytick.color': '#d1d4dc', 'font.size': 10},
)
# ============================================================
# Plot
# ============================================================
os.makedirs("output", exist_ok=True)
# Display volume if available
show_volume = "Volume" in df.columns
mpf.plot(
df, type='candle', style=style, volume=show_volume,
title=f'\n{SYMBOL} — {DAYS}D Candlestick',
figsize=(14, 8),
savefig=dict(fname=OUTPUT_FILE, dpi=150, bbox_inches='tight',
facecolor='#131722', edgecolor='#131722'),
)
print(f"Chart saved: {OUTPUT_FILE}")
#!/usr/bin/env python3
"""Generate TradingView-style candlestick chart.
Usage: Copy to scripts/, customize the Config section, run with:
python3 scripts/my_chart.py
"""
import os
import sys
import requests
import pandas as pd
import mplfinance as mpf
import time
# ============================================================
# Proxy Auto-Configuration (for deployed environments)
# ============================================================
if not os.getenv("HTTP_PROXY") and os.getenv("PROXY_HOST"):
host = os.getenv("PROXY_HOST")
port = os.getenv("PROXY_PORT", "8080")
# Handle IPv6 addresses
if ":" in host and not host.startswith("["):
host = f"[{host}]"
proxy_url = f"http://{host}:{port}"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
# ============================================================
# Config — customize these for each chart
# ============================================================
COIN_ID = "bitcoin"
DAYS = 30
INTERVAL = None # None for auto-select, or "daily"/"hourly"
OUTPUT_FILE = "output/btc_30d_chart.png"
# ============================================================
# Smart Interval Selection
# ============================================================
def select_interval(days):
"""Auto-select optimal interval based on time range."""
if days <= 31:
return "hourly" # Max granularity for short term
else:
return "daily" # Efficient for longer periods
if INTERVAL is None:
INTERVAL = select_interval(DAYS)
# ============================================================
# Fetch OHLC from CoinGecko with Error Handling
# ============================================================
API_KEY = os.getenv("COINGECKO_API_KEY")
if not API_KEY:
print("ERROR: COINGECKO_API_KEY not set", file=sys.stderr)
sys.exit(1)
now = int(time.time())
from_ts = now - (DAYS * 86400)
url = f"https://pro-api.coingecko.com/api/v3/coins/{COIN_ID}/ohlc/range"
params = {"vs_currency": "usd", "from": from_ts, "to": now, "interval": INTERVAL}
headers = {"x-cg-pro-api-key": API_KEY}
# Retry logic with exponential backoff
raw = None
for attempt in range(3):
try:
resp = requests.get(url, params=params, headers=headers, timeout=15)
resp.raise_for_status()
raw = resp.json()
# Validate response
if not isinstance(raw, list) or len(raw) == 0:
raise ValueError("Empty or invalid data received from API")
# Validate data format
if not all(isinstance(item, list) and len(item) == 5 for item in raw):
raise ValueError("Invalid OHLC data format")
break # Success
except Exception as e:
if attempt == 2: # Last attempt
print(f"ERROR: Failed to fetch data after 3 attempts: {e}", file=sys.stderr)
sys.exit(1)
time.sleep(2 ** attempt) # Exponential backoff: 1s, 2s
# ============================================================
# Build DataFrame
# ============================================================
df = pd.DataFrame(raw, columns=["Timestamp", "Open", "High", "Low", "Close"])
df["Date"] = pd.to_datetime(df["Timestamp"], unit="ms")
df.set_index("Date", inplace=True)
df.drop(columns=["Timestamp"], inplace=True)
# ============================================================
# TradingView Style
# ============================================================
mc = mpf.make_marketcolors(
up='#26a69a', down='#ef5350',
edge='inherit', wick='inherit', volume='inherit', ohlc='inherit',
)
style = mpf.make_mpf_style(
marketcolors=mc,
facecolor='#131722', edgecolor='#131722', figcolor='#131722',
gridcolor='#1e222d', gridstyle='--', y_on_right=True,
rc={'axes.labelcolor': '#d1d4dc', 'xtick.color': '#d1d4dc',
'ytick.color': '#d1d4dc', 'font.size': 10},
)
# ============================================================
# Plot
# ============================================================
os.makedirs("output", exist_ok=True)
mpf.plot(
df, type='candle', style=style, volume=False,
title=f'\n{COIN_ID.upper()} — {DAYS}D Candlestick',
figsize=(14, 8),
savefig=dict(fname=OUTPUT_FILE, dpi=150, bbox_inches='tight',
facecolor='#131722', edgecolor='#131722'),
)
print(f"Chart saved: {OUTPUT_FILE}")
#!/usr/bin/env python3
"""Generate TradingView-style candlestick chart with technical indicators.
Includes: RSI, MACD, Bollinger Bands, EMA/SMA overlays.
Usage: Copy to scripts/, enable the indicators you want in Config, run with:
python3 scripts/my_chart.py
"""
import os
import sys
import requests
import pandas as pd
import mplfinance as mpf
import time
# ============================================================
# Proxy Auto-Configuration (for deployed environments)
# ============================================================
if not os.getenv("HTTP_PROXY") and os.getenv("PROXY_HOST"):
host = os.getenv("PROXY_HOST")
port = os.getenv("PROXY_PORT", "8080")
# Handle IPv6 addresses
if ":" in host and not host.startswith("["):
host = f"[{host}]"
proxy_url = f"http://{host}:{port}"
os.environ["HTTP_PROXY"] = proxy_url
os.environ["HTTPS_PROXY"] = proxy_url
# ============================================================
# Config — customize these for each chart
# ============================================================
COIN_ID = "bitcoin"
DAYS = 90
INTERVAL = None # None for auto-select, or "daily"/"hourly"
OUTPUT_FILE = "output/btc_90d_indicators.png"
# Toggle indicators (set True/False)
SHOW_EMA = True # EMA 20 overlay on candles
SHOW_SMA = True # SMA 50 overlay on candles
SHOW_BBANDS = False # Bollinger Bands overlay on candles
SHOW_RSI = True # RSI subplot
SHOW_MACD = True # MACD subplot
# ============================================================
# Smart Interval Selection
# ============================================================
def select_interval(days):
"""Auto-select optimal interval based on time range."""
if days <= 31:
return "hourly" # Max granularity for short term
else:
return "daily" # Efficient for longer periods
if INTERVAL is None:
INTERVAL = select_interval(DAYS)
# ============================================================
# Fetch OHLC from CoinGecko with Error Handling
# ============================================================
API_KEY = os.getenv("COINGECKO_API_KEY")
if not API_KEY:
print("ERROR: COINGECKO_API_KEY not set", file=sys.stderr)
sys.exit(1)
now = int(time.time())
from_ts = now - (DAYS * 86400)
url = f"https://pro-api.coingecko.com/api/v3/coins/{COIN_ID}/ohlc/range"
params = {"vs_currency": "usd", "from": from_ts, "to": now, "interval": INTERVAL}
headers = {"x-cg-pro-api-key": API_KEY}
# Retry logic with exponential backoff
raw = None
for attempt in range(3):
try:
resp = requests.get(url, params=params, headers=headers, timeout=15)
resp.raise_for_status()
raw = resp.json()
# Validate response
if not isinstance(raw, list) or len(raw) == 0:
raise ValueError("Empty or invalid data received from API")
# Validate data format
if not all(isinstance(item, list) and len(item) == 5 for item in raw):
raise ValueError("Invalid OHLC data format")
break # Success
except Exception as e:
if attempt == 2: # Last attempt
print(f"ERROR: Failed to fetch data after 3 attempts: {e}", file=sys.stderr)
sys.exit(1)
time.sleep(2 ** attempt) # Exponential backoff: 1s, 2s
df = pd.DataFrame(raw, columns=["Timestamp", "Open", "High", "Low", "Close"])
df["Date"] = pd.to_datetime(df["Timestamp"], unit="ms")
df.set_index("Date", inplace=True)
df.drop(columns=["Timestamp"], inplace=True)
# ============================================================
# TradingView Style
# ============================================================
mc = mpf.make_marketcolors(
up='#26a69a', down='#ef5350',
edge='inherit', wick='inherit', volume='inherit', ohlc='inherit',
)
style = mpf.make_mpf_style(
marketcolors=mc,
facecolor='#131722', edgecolor='#131722', figcolor='#131722',
gridcolor='#1e222d', gridstyle='--', y_on_right=True,
rc={'axes.labelcolor': '#d1d4dc', 'xtick.color': '#d1d4dc',
'ytick.color': '#d1d4dc', 'font.size': 10},
)
# ============================================================
# Indicator Calculations
# ============================================================
def calc_rsi(series, period=14):
delta = series.diff()
gain = delta.where(delta > 0, 0.0)
loss = -delta.where(delta < 0, 0.0)
avg_gain = gain.ewm(com=period - 1, min_periods=period).mean()
avg_loss = loss.ewm(com=period - 1, min_periods=period).mean()
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
def calc_macd(series, fast=12, slow=26, signal=9):
ema_fast = series.ewm(span=fast).mean()
ema_slow = series.ewm(span=slow).mean()
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal).mean()
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
# ============================================================
# Build Addplots
# ============================================================
addplots = []
# Track how many subplots we need (panel 1 is reserved for volume if used)
next_panel = 2
# Overlay indicators (on main chart, panel 0)
if SHOW_EMA:
df["EMA_20"] = df["Close"].ewm(span=20).mean()
addplots.append(mpf.make_addplot(df["EMA_20"], color='#2196f3', width=1.0))
if SHOW_SMA:
df["SMA_50"] = df["Close"].rolling(window=50).mean()
addplots.append(mpf.make_addplot(df["SMA_50"], color='#ff9800', width=1.0))
if SHOW_BBANDS:
period = 20
df["BB_Mid"] = df["Close"].rolling(window=period).mean()
df["BB_Upper"] = df["BB_Mid"] + 2 * df["Close"].rolling(window=period).std()
df["BB_Lower"] = df["BB_Mid"] - 2 * df["Close"].rolling(window=period).std()
addplots.append(mpf.make_addplot(df["BB_Mid"], color='#ff9800', width=0.8))
addplots.append(mpf.make_addplot(df["BB_Upper"], color='#78909c', width=0.6, linestyle='--'))
addplots.append(mpf.make_addplot(df["BB_Lower"], color='#78909c', width=0.6, linestyle='--'))
# Subplot indicators
panel_ratios = [4, 1] # Main chart + volume placeholder
if SHOW_RSI:
df["RSI"] = calc_rsi(df["Close"])
addplots.append(mpf.make_addplot(df["RSI"], panel=next_panel, color='#b39ddb',
ylabel='RSI', ylim=(0, 100), secondary_y=False))
addplots.append(mpf.make_addplot([70] * len(df), panel=next_panel, color='#ef5350',
linestyle='--', secondary_y=False, width=0.7))
addplots.append(mpf.make_addplot([30] * len(df), panel=next_panel, color='#26a69a',
linestyle='--', secondary_y=False, width=0.7))
panel_ratios.append(2)
next_panel += 1
if SHOW_MACD:
df["MACD"], df["Signal"], df["Hist"] = calc_macd(df["Close"])
hist_colors = ['#26a69a' if v >= 0 else '#ef5350' for v in df["Hist"]]
addplots.append(mpf.make_addplot(df["MACD"], panel=next_panel, color='#2196f3',
ylabel='MACD', secondary_y=False))
addplots.append(mpf.make_addplot(df["Signal"], panel=next_panel, color='#ff9800',
secondary_y=False))
addplots.append(mpf.make_addplot(df["Hist"], panel=next_panel, type='bar',
color=hist_colors, secondary_y=False, width=0.7))
panel_ratios.append(2)
next_panel += 1
# ============================================================
# Plot
# ============================================================
os.makedirs("output", exist_ok=True)
# Adjust figure height based on number of panels
fig_height = 8 + 2 * (next_panel - 2)
mpf.plot(
df, type='candle', style=style, volume=False,
title=f'\n{COIN_ID.upper()} — {DAYS}D Technical Analysis',
addplot=addplots if addplots else None,
panel_ratios=tuple(panel_ratios),
figsize=(14, fig_height),
savefig=dict(fname=OUTPUT_FILE, dpi=150, bbox_inches='tight',
facecolor='#131722', edgecolor='#131722'),
)
print(f"Chart saved: {OUTPUT_FILE}")
Related skills
FAQ
Who is charting for?
Developers using agents to execute charting workflows from SKILL.md.
When should I use charting?
Generate TradingView-style candlestick charts with indicators. Use when the user wants a visual chart, price visualization, or technical analysis plot.
Is charting safe to install?
Review the Security Audits panel on this page before installing in production.