↓ Skip to main content

Thin Blue Lines

·1563 words·8 mins
Max Severance
Author
Max Severance
Architecting fabric convergence at hyperscale.

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:

  1. Can we use the TradingView API? Yes, through third-party services like Chart-IMG that provide REST API access to TradingView charts
  2. Can the LLM output tool calls? Yes, by having it output structured JSON that we parse and execute
  3. 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: Bearer header
  • API v2: Uses x-api-key header (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 embed

Each 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='[]'
fi

Step 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 "![${ALT_TEXT}](${REL_PATH})" >> "$CHART_MD_FILE"
    log_info "Saved chart: $IMG_FILE"
done

Key points about this implementation:

  1. Using mapfile: Avoids subshell issues that occur with piped while loops
  2. Direct file writing: Writes curl output directly to file to handle binary PNG data
  3. Error handling: Continues processing other charts if one fails
  4. Date prefix: Filenames include the week date to prevent clashes
  5. 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 -->
EOF

Configuration 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 model

To 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-debug

Hugo 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:

  1. Images must go in content/trading/assets/ to be served at /trading/assets/
  2. The markdown must reference them as ../assets/filename.png (relative from the post)
  3. 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
done

Chart API Errors
#

if [[ "$http_status" != "200" ]]; then
    log_warn "Chart-IMG API returned status $http_status for $SYMBOL $INTERVAL: $RESPONSE_BODY"
    continue
fi

Running 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 \
    --debug

For 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-debug

Summary: Key Technical Decisions
#

  1. 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.

  2. Chart-IMG API v2: We use the Advanced Chart endpoint with the correct x-api-key authentication header.

  3. Date Format: Chart-IMG requires ISO 8601 format with timezone (2026-03-08T00:00:00.000Z).

  4. Asset Paths: Charts go in content/trading/assets/ with date-prefixed filenames to avoid conflicts between weeks.

  5. Visual Highlighting: Two thin translucent blue vertical lines mark the start and end of the week on each chart.

  6. Error Resilience: The script logs warnings but continues processing when individual charts fail, allowing partial success.

  7. 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.