From VibeCode to Production: Building an LLM-Driven Trading Blog with Automated Chart Integration #
In this comprehensive guide, I’ll walk you through how we transformed a basic trading blog automation script into a sophisticated system that uses Large Language Models to dynamically generate chart requests, fetch TradingView screenshots via the Chart-IMG API, and embed them directly into Hugo-compatible blog posts. This session represents a fascinating exploration of how AI can drive not just content generation but also the tool-calling patterns needed to fetch external resources in real-time.
The journey began when we wanted to enhance our weekly trading analysis posts with visual chart snapshots from TradingView, automatically highlighting the relevant timeframes for each week’s market activity. What started as a simple API integration evolved into a full-fledged tool-calling system where the LLM decides which charts to request based on the trading data, outputs structured JSON that our bash script intercepts, and the system automatically fetches, saves, and embeds the visualizations.
TL;DR #
- Implemented LLM-driven tool calling using JSON output that the bash script parses and executes via curl commands to the Chart-IMG API
- Integrated TradingView chart fetching with automatic date range highlighting using vertical lines
- Resolved multiple API endpoint issues (Chart-IMG v2 endpoint, authentication headers, date formatting)
- Fixed Hugo asset path issues by saving charts to the correct directory structure
- Added rate limiting handling and error recovery for robust production use
The Challenge: Adding Dynamic Charts to Trading Posts #
Our existing trading-to-blog.sh script already handled extracting data from screenshots using vision models, generating markdown summaries with week-over-week comparisons, and writing blog posts in Hugo format. However, we wanted to automatically include TradingView charts showing the relevant timeframes for the week’s trading activity.
The key questions we needed to answer:
- Can we use the TradingView API? Yes, through third-party services like Chart-IMG that provide REST API access to TradingView charts
- Can the LLM output tool calls? Yes, by having it output structured JSON that we parse and execute
- How do we implement this technically? The LLM outputs JSON with chart specifications, we intercept and execute using curl, then embed the resulting images
Research Phase: Understanding the TradingView API Options #
Our initial research revealed several approaches for fetching TradingView charts:
Chart-IMG Service #
From the Chart-IMG documentation, we found they provide a REST API with different versions:
- API v1: Uses
Authorization: Bearerheader - API v2: Uses
x-api-keyheader (the version we ultimately used) - Advanced Chart endpoint:
https://api.chart-img.com/v2/tradingview/advanced-chart
Key parameters for the v2 Advanced Chart endpoint:
{
"height": 300,
"theme": "dark",
"interval": "1h",
"style": "baseline"
}The API supports various cryptocurrencies, stocks (with some limitations), forex, and futures. We also discovered that you can add drawings like vertical lines to highlight specific dates.
Tool Calling Architecture #
The core insight was that LLMs can output structured data (JSON) that our script can interpret as “tool calls” without the LLM actually making network requests. Here’s how we implemented this:
Step 1: Prompt Engineering for Tool Calls #
We modified the summary prompt to request JSON output with two fields:
- "body": the markdown content for the blog post (without frontmatter).
- "charts": an array of chart objects to fetch and embedEach chart object includes:
symbol: TradingView format (e.g., “BINANCE:ETHUSDT”, “NASDAQ:AAPL”)interval: Timeframe (e.g., “1D”, “4H”)from: Start date (YYYY-MM-DD)to: End date (YYYY-MM-DD)
Step 2: Parsing the LLM Output #
Our bash script parses the JSON response:
# Attempt to parse as JSON; if fails, treat raw as body and no charts
POST_CONTENT=$(echo "$POST_CONTENT_RAW" | jq -r '.body // empty' 2>/dev/null)
CHARTS_JSON=$(echo "$POST_CONTENT_RAW" | jq -c '.charts // []' 2>/dev/null)
if [[ -z "$POST_CONTENT" ]]; then
# Fallback: use raw content as body, no charts
POST_CONTENT="$POST_CONTENT_RAW"
CHARTS_JSON='[]'
fiStep 3: Fetching Charts via the Chart-IMG API #
The script iterates through each chart request and fetches the image:
# Process each chart object using mapfile to avoid subshell issues
mapfile -t chartobjs < <(echo "$CHARTS_JSON" | jq -c '.[]')
for chartobj in "${chartobjs[@]}"; do
SYMBOL=$(echo "$chartobj" | jq -r '.symbol // empty')
INTERVAL=$(echo "$chartobj" | jq -r '.interval // empty')
FROM_DATE=$(echo "$chartobj" | jq -r '.from // empty')
TO_DATE=$(echo "$chartobj" | jq -r '.to // empty')
ALT_TEXT=$(echo "$chartobj" | jq -r --arg SYMBOL "$SYMBOL" --arg INTERVAL "$INTERVAL" '.alt // "\($SYMBOL) \($INTERVAL) chart"')
# Validate required fields
if [[ -z "$SYMBOL" || -z "$INTERVAL" || -z "$FROM_DATE" || -z "$TO_DATE" ]]; then
log_warn "Skipping chart due to missing fields: $chartobj"
continue
fi
# Convert dates to ISO 8601 format with time component for Chart-IMG API
FROM_ISO=$(date -d "$FROM_DATE" +%Y-%m-%dT00:00:00.000Z) 2>/dev/null || { log_warn "Invalid from date: $FROM_DATE"; continue; }
TO_ISO=$(date -d "$TO_DATE" +%Y-%m-%dT23:59:59.999Z) 2>/dev/null || { log_warn "Invalid to date: $TO_DATE"; continue; }
# Output filename with date prefix to avoid clashes between weeks
SAFE_SYMBOL=$(echo "$SYMBOL" | tr '/' '_')
IMG_FILE="$CHART_ASSET_DIR/${DATE_DISPLAY}_${SAFE_SYMBOL}_${INTERVAL}.png"
# Call Chart-IMG API, writing output directly to file to avoid null byte issues
log_debug "Fetching chart: $SYMBOL $INTERVAL from $FROM_DATE to $TO_DATE"
http_status=$(curl -s -w "\nHTTPSTATUS:%{http_code}" -o "$IMG_FILE" \
-H "Authorization: Bearer $CHART_IMG_KEY" \
-H "Content-Type: application/json" \
-d "{
\"symbol\":\"$SYMBOL\",
\"interval\":\"$INTERVAL\",
\"from\":\"$FROM_ISO\",
\"to\":\"$TO_ISO\",
\"width\":800,
\"height\":400,
\"theme\":\"dark\",
\"drawings\": [
{\"name\": \"Vertical Line\", \"input\": {\"datetime\": \"$FROM_ISO\"}, \"override\": {\"color\": \"rgba(0, 150, 255, 0.5)\", \"linewidth\": 1}},
{\"name\": \"Vertical Line\", \"input\": {\"datetime\": \"$TO_ISO\"}, \"override\": {\"color\": \"rgba(0, 150, 255, 0.5)\", \"linewidth\": 1}}
]
}" "$CHART_IMG_API_BASE" | tail -n1)
# Extract HTTP status
http_status=$(echo "$http_status" | sed -e 's/.*HTTPSTATUS://')
if [[ "$http_status" != "200" ]]; then
log_warn "Chart-IMG API returned status $http_status for $SYMBOL $INTERVAL"
rm -f "$IMG_FILE"
continue
fi
# Verify file was saved
if [[ ! -s "$IMG_FILE" ]]; then
log_warn "Failed to save image for $SYMBOL $INTERVAL (empty file)"
continue
fi
# Relative path from trading post to assets directory
REL_PATH="../assets/${DATE_DISPLAY}_${SAFE_SYMBOL}_${INTERVAL}.png"
# Append markdown line
echo "" >> "$CHART_MD_FILE"
log_info "Saved chart: $IMG_FILE"
doneKey points about this implementation:
- Using mapfile: Avoids subshell issues that occur with piped while loops
- Direct file writing: Writes curl output directly to file to handle binary PNG data
- Error handling: Continues processing other charts if one fails
- Date prefix: Filenames include the week date to prevent clashes
- Vertical line drawings: Adds thin blue lines at start/end of week for visual highlighting
Step 4: Embedding Charts in the Blog Post #
The accumulated chart markdown is read from the temp file and embedded:
# Read accumulated chart markdown
CHART_MD=$(cat "$CHART_MD_FILE" 2>/dev/null || echo "")
# Write blog post with chart images embedded after the summary
cat > "$OUTPUT_FILE" <<EOF
+++
title = 'Trading Week $WEEK_DATE'
date = '$DATE_TZ'
draft = true
categories = ['Trading']
tags = ['options', 'crypto', 'weekly']
description = 'Weekly trading analysis'
+++
$POST_CONTENT
---
## 📊 Charts
$CHART_MD
<!-- Add TradingView screenshots here -->
## 📝 Notes
<!-- Add your analysis/thesis notes here -->
EOFConfiguration and Environment Variables #
The script uses several environment variables:
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
OUTPUT_DIR="${PROJECT_ROOT}/content/trading"
IMGMODEL="kimi-k2.5"
API_BASE="https://opencode.ai/zen/v1/chat/completions"
CHART_IMG_API_BASE="https://api.chart-img.com/v2/tradingview/advanced-chart"
CHART_IMG_KEY="${CHART_IMG_API_KEY:-}"
FINAL_MODEL="big-pickle" # Default final modelTo use the chart fetching feature:
export CHART_IMG_API_KEY=$(cat ~/chartimg.api.key.txt)
./scripts/trading-to-blog-with-charts.sh 2026-03-15 --model nemotron-3-super-free --use-debugHugo Asset Path Resolution #
A critical issue we encountered was where to save the chart images so they’re served correctly by Hugo. We found that:
- Images must go in
content/trading/assets/to be served at/trading/assets/ - The markdown must reference them as
../assets/filename.png(relative from the post) - The Hugo build copies these to the published site
Error Handling and Rate Limiting #
We added several robustness features:
API Rate Limiting #
# Retry logic for rate limits
max_attempts=3
attempt=1
while [[ $attempt -le $max_attempts ]]; do
final_response=$(curl -s --max-time 30 -X POST "$API_BASE" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d @"$TMPDIR/payload.json")
if echo "$final_response" | jq -e '.error' >/dev/null 2>&1; then
error_type=$(echo "$final_response" | jq -r '.error.type' 2>/dev/null || echo "Unknown")
if [[ "$error_type" == "FreeUsageLimitError" ]]; then
wait_time=$((attempt * 30))
log_info "Rate limited on final model, waiting ${wait_time}s... (attempt $attempt/$max_attempts)"
sleep $wait_time
((attempt++))
continue
else
log_error "Final model API error: $(echo "$final_response" | jq -r '.error.message')"
exit 1
fi
fi
break
doneChart API Errors #
if [[ "$http_status" != "200" ]]; then
log_warn "Chart-IMG API returned status $http_status for $SYMBOL $INTERVAL: $RESPONSE_BODY"
continue
fiRunning the Script #
Here’s a typical execution:
# Set the API key
export CHART_IMG_API_KEY=$(cat ~/chartimg.api.key.txt)
# Run with the nemotron model and debug mode
./scripts/trading-to-blog-with-charts.sh 2026-03-15 \
--model nemotron-3-super-free \
--use-debug \
--debugFor testing without model API calls:
export DEBUG_FINAL_RESULT='{"id":"test","object":"chat.completion","created":1773585389,"choices":[{"index":0,"message":{"role":"assistant","content":"{\"body\":\"Test with ETH chart\",\"charts\":[{\"symbol\":\"BINANCE:ETHUSDT\",\"interval\":\"1D\",\"from\":\"2026-03-08\",\"to\":\"2026-03-15\"}]}"}}]}'
./scripts/trading-to-blog-with-charts.sh 2026-03-15 --model nemotron-3-super-free --use-debugSummary: Key Technical Decisions #
-
LLM Tool Calling via JSON: The LLM outputs structured JSON that our script interprets as tool calls. This allows the AI to decide what charts are relevant without executing network requests directly.
-
Chart-IMG API v2: We use the Advanced Chart endpoint with the correct
x-api-keyauthentication header. -
Date Format: Chart-IMG requires ISO 8601 format with timezone (
2026-03-08T00:00:00.000Z). -
Asset Paths: Charts go in
content/trading/assets/with date-prefixed filenames to avoid conflicts between weeks. -
Visual Highlighting: Two thin translucent blue vertical lines mark the start and end of the week on each chart.
-
Error Resilience: The script logs warnings but continues processing when individual charts fail, allowing partial success.
-
Hugo Integration: Proper relative paths ensure charts render correctly in the generated static site.
This system demonstrates how LLMs can drive complex automation workflows by outputting structured data that downstream scripts interpret as executable actions—a pattern that extends far beyond just chart fetching to any external API integration.