QuantMaxi

Auto-ingest data

Send your strategy output as JSON and QuantMaxi plots it. The fastest path is to connect the MCP server and let your own AI agent do the publishing.

Connect your AI agent

Start here

QuantMaxi ships an MCP server, so your agent gets tools instead of instructions and nobody writes a serializer. One line to connect it, then point the agent at your strategy’s output and say publish.

1. Connect it, in Claude Code
claude mcp add quantmaxi \
  --env API_TOKEN=qmx_YOUR_TOKEN \
  -- npx -y quantmaxi-mcp
or in Codex
codex mcp add quantmaxi \
  --env API_TOKEN=qmx_YOUR_TOKEN \
  -- npx -y quantmaxi-mcp
or any client that reads mcpServers JSON
{
  "mcpServers": {
    "quantmaxi": {
      "command": "npx",
      "args": ["-y", "quantmaxi-mcp"],
      "env": { "API_TOKEN": "qmx_YOUR_TOKEN" }
    }
  }
}

Nothing to download: npx fetches the server on first use and keeps it current. Create the token on Access tokens. It lives in the client config, never in the chat. Both CLIs can read back what they registered, with claude mcp list and codex mcp list.

2. Ask for it in plain language
publish backtest.parquet to QuantMaxi as "SMA cross", BTC-USD daily
Point it at whatever your strategy already writes: CSV, TSV, JSON, JSONL, Excel or Parquet. Column names, timestamp units and pane layout are inferred, and the language the strategy is written in does not matter.
  • inspect_dataset
    What it doesReads the column names and a few sample rows, then proposes the plot layout. The dataset itself never enters the conversation.
  • validate_payload
    What it doesConverts locally and returns the exact rejection reason, in milliseconds, instead of a 400 after the upload.
  • upload_dataset
    What it doesPublishes to your account. Rows stream from disk to the API, not through the model.
  • generate_adapter_script
    What it doesWrites a standalone script for the mapping you just confirmed, so the next run needs no AI at all.
  • contract_reference
    What it doesThe full contract, for the edge cases below.
Show the same entry as Codex TOML, for ~/.codex/config.toml
mcp_servers entry
[mcp_servers.quantmaxi]
command = "npx"
args = ["-y", "quantmaxi-mcp"]

[mcp_servers.quantmaxi.env]
API_TOKEN = "qmx_YOUR_TOKEN"

Publish from a script

For a strategy you re-run on a schedule, skip the agent and call the adapter directly. It enforces every rule below locally, before anything goes over the wire. Python only.

Download the adapter
curl -fsSL -O https://www.quantmaxi.com/quantmaxi_adapter.py
Two lines in your strategy
from quantmaxi_adapter import build_payload, upload

# A DataFrame, a list of dicts, or a path to a CSV.
payload = build_payload(df, ticker="BTC-USD", interval="1d")

# Reads API_TOKEN from the environment. Gzips the body on the way out.
upload(payload, chart_name="SMA cross")
Show a full strategy.py, SMA crossover to chart
Install dependencies
pip install yfinance pandas numpy requests
strategy.py
#!/usr/bin/env python3
import os, json, requests
import yfinance as yf
import pandas as pd
import numpy as np

TICKER       = os.getenv("TICKER", "ETH-USD")
INTERVAL     = os.getenv("YF_INTERVAL", "1d")
PERIOD       = os.getenv("YF_PERIOD", "2y")
INITIAL_CASH = float(os.getenv("INITIAL_CASH", "1000000"))
PAYLOAD_OUT  = os.getenv("PAYLOAD_OUT", "quantmaxi_payload.json")

def build_payload() -> dict:
    data = yf.download(TICKER, interval=INTERVAL, period=PERIOD,
                       auto_adjust=False, progress=False)
    if data is None or data.empty:
        raise RuntimeError(f"No data for {TICKER}")
    if isinstance(data.columns, pd.MultiIndex):
        data.columns = data.columns.get_level_values(0)

    data["SMA20"] = data["Close"].rolling(20).mean()
    data["SMA50"] = data["Close"].rolling(50).mean()

    data["Signal"] = 0
    data.loc[(data["SMA20"] > data["SMA50"]) &
             (data["SMA20"].shift(1) <= data["SMA50"].shift(1)), "Signal"] = 1
    data.loc[(data["SMA20"] < data["SMA50"]) &
             (data["SMA20"].shift(1) >= data["SMA50"].shift(1)), "Signal"] = -1

    data["Position"] = data["Signal"].shift(1).fillna(0)
    data["Units"] = (data["Position"].diff().fillna(0) *
                     INITIAL_CASH / data["Close"]).cumsum()
    data["Holdings"] = data["Units"] * data["Close"]
    data["Cash"] = INITIAL_CASH - (data["Units"].diff().fillna(0) *
                                    data["Close"]).cumsum()
    data["Equity"] = data["Holdings"] + data["Cash"]

    data.index = pd.to_datetime(data.index, utc=True)
    data["time"] = (data.index.view("int64") // 10**9).astype("int64")

    cols = ["time","Open","High","Low","Close","Volume",
            "SMA20","SMA50","Signal","Holdings","Cash","Equity"]
    data = data[cols]
    data.replace([np.inf, -np.inf], np.nan, inplace=True)
    data.fillna(0, inplace=True)
    data.sort_values("time", inplace=True)

    return {
        "ticker": TICKER, "interval": INTERVAL,
        "columns": cols,
        "data": data.to_dict(orient="records"),
        "metadata": {
            "ticker": TICKER, "interval": INTERVAL,
            "Open":     {"plot": True, "type": "candlestick", "component": "open"},
            "High":     {"plot": True, "type": "candlestick", "component": "high"},
            "Low":      {"plot": True, "type": "candlestick", "component": "low"},
            "Close":    {"plot": True, "type": "candlestick", "component": "close"},
            "SMA20":    {"plot": True, "type": "line", "color": "#f07b01", "paneIndex": 0},
            "SMA50":    {"plot": True, "type": "line", "color": "#2962ff", "paneIndex": 0},
            "Volume":   {"plot": True, "type": "histogram",
                         "upColor": "#089981", "downColor": "#f23645", "paneIndex": 0},
            "Equity":   {"plot": True, "type": "area", "color": "#7c83fd", "paneIndex": 1},
            "Holdings": {"plot": True, "type": "line", "color": "#22c55e", "paneIndex": 2},
        },
    }

def upload(payload: dict) -> None:
    token = os.getenv("API_TOKEN", "").strip()
    chart_name = os.getenv("CHART_NAME", "default").strip() or "default"
    if not token:
        print("No API_TOKEN -> upload skipped (JSON saved locally).")
        return
    resp = requests.post(
        "https://www.quantmaxi.com/api/ingest",
        headers={"Authorization": f"Bearer {token}",
                 "Content-Type": "application/json"},
        json={"chartName": chart_name, "payload": payload},
        timeout=60)
    print(f"Upload: {resp.status_code} {resp.text}")

if __name__ == "__main__":
    payload = build_payload()
    with open(PAYLOAD_OUT, "w") as f:
        json.dump(payload, f, indent=2)
    print(f"Saved {PAYLOAD_OUT}")
    upload(payload)
Run
API_TOKEN=<YOUR_TOKEN> CHART_NAME=default python3 strategy.py
  • API_TOKEN
    Default—
    DescriptionRequired for upload. The script skips uploading if it is missing.
  • CHART_NAME
    Defaultdefault
    DescriptionName of the chart to create or update.
  • TICKER
    DefaultETH-USD
    DescriptionAsset symbol for yfinance.
No agent and no adapter? Paste this prompt into a chat

The last resort, for a notebook with no repo behind it. It asks an assistant to hand-write the export step, which is the one part of this you would rather not have generated: nothing validates the result until the API rejects it.

AI prompt
You are a trading-strategy code assistant.
I have an existing strategy script that produces chart data.
Adapt ONLY the output / export step so it produces a JSON payload matching the QuantMaxi format below.
Do NOT change any strategy logic, indicators, or calculations.

--- QuantMaxi Payload Contract ---

Top-level shape (TypeScript):
{
  ticker: string          // e.g. "BTC-USD"
  interval: string        // e.g. "1d", "1h", "15m"
  columns: string[]       // must include "time" + every data column name
  data: Array<Record<string, number | null>>
  metadata: Record<string, object>
}

Data rules:
- "time" = Unix seconds (UTC integer). If you have milliseconds, divide by 1000.
- Rows sorted ascending by time, no duplicates.
- All plotted values must be numeric. Replace NaN / Infinity with null or 0.
- Max 200 000 rows, 512 columns per payload.

Metadata rules:
- One key per data column you want to plot.
- Each key = { "plot": true, "type": "<type>", ... }
- Supported types and their required fields:

  candlestick  ->  4 entries with "component": "open" | "high" | "low" | "close"
  line         ->  "color": "#hex", "paneIndex": 0
  area         ->  "color": "#hex", "paneIndex": 0
  histogram    ->  "upColor": "#hex", "downColor": "#hex", "paneIndex": 0
  markers      ->  "color": "#hex", "shape": "circle", "position": "aboveBar"

- paneIndex 0 = main chart pane. Use 1, 2, ... for separate panes below.
- Candlestick always goes on pane 0 (no paneIndex needed).
- Volume histogram typically goes on pane 0 with upColor/downColor.
- Legacy "chart: N" maps to paneIndex: N-1 automatically.

Return ONLY:
1. The adapted code (minimal changes to my script).
2. One example JSON output snippet showing the payload structure.

--- My existing code is below ---

Payload reference

TypeScript shape
{
  ticker: string,
  interval: string,
  columns: string[],
  data: Array<Record<string, number | string | null>>,
  metadata: Record<string, any>
}
  • time is Unix seconds: divide milliseconds by 1,000, nanoseconds by 1e9
  • Rows sorted ascending by time, with no duplicates
  • Every plotted value must be numeric, so clean NaN and Infinity first
  • Candlestick requires all four OHLC components in metadata
  • At most 200,000 rows and 512 columns per payload
Legacy chart: N is mapped to paneIndex: N-1 automatically. Use paneIndex directly in new code.
Valid payload example
{
  "ticker": "BTC-USD",
  "interval": "1d",
  "columns": ["time","Open","High","Low","Close","Volume","SMA20","Equity"],
  "data": [
    {
      "time": 1713657600,
      "Open": 64123.4, "High": 64980.2, "Low": 63881.7, "Close": 64720.1,
      "Volume": 22812.6, "SMA20": 63110.2, "Equity": 1000000.0
    }
  ],
  "metadata": {
    "Open":   { "plot": true, "type": "candlestick", "component": "open" },
    "High":   { "plot": true, "type": "candlestick", "component": "high" },
    "Low":    { "plot": true, "type": "candlestick", "component": "low" },
    "Close":  { "plot": true, "type": "candlestick", "component": "close" },
    "SMA20":  { "plot": true, "type": "line", "color": "#f07b01", "paneIndex": 0 },
    "Volume": { "plot": true, "type": "histogram", "upColor": "#089981", "downColor": "#f23645", "paneIndex": 0 },
    "Equity": { "plot": true, "type": "area", "color": "#7c83fd", "paneIndex": 1 }
  }
}

Series types

  • candlestick
    Required fields4 entries: component: open|high|low|close
    Style keysbuilt-in
    NotesAlways pane 0
  • line
    Required fieldsnumeric column
    Style keyscolor
    NotespaneIndex
  • area
    Required fieldsnumeric column
    Style keyscolor
    NotespaneIndex
  • histogram
    Required fieldsnumeric column
    Style keysupColor, downColor
    NotespaneIndex
  • markers
    Required fieldsnumeric column
    Style keyscolor, shape, position
    NotespaneIndex

API endpoints

  • /api/ingest
    AuthBearer token
    Body shapeWrapped: {"chartName":"...","payload":{...}}
  • /api/charts/by-id/{id}/ingest
    AuthBearer token
    Body shapeRaw payload
  • /api/charts/{name}/ingest
    AuthBrowser session
    Body shapeRaw payload
Show curl examples
curl examples
# /api/ingest  (token auth, wrapped body)
curl -X POST "https://www.quantmaxi.com/api/ingest" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"chartName":"default","payload":{...}}'

# /api/charts/by-id/{id}/ingest  (token auth, raw payload body)
curl -X POST "https://www.quantmaxi.com/api/charts/by-id/<ID>/ingest" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{...payload...}'

# /api/charts/{name}/ingest  (browser session only, raw payload body)
curl -X POST "https://www.quantmaxi.com/api/charts/default/ingest" \
  -H "Content-Type: application/json" \
  -d '{...payload...}'

Troubleshooting

  • missing token
    CauseNo auth header on /api/ingest
    FixAdd Authorization: Bearer <TOKEN>
  • invalid payload
    CauseWrong payload shape
    FixUse the full envelope with payload.data[]
  • Invalid JSON body
    CauseBody is not valid JSON
    FixValidate the JSON before sending
  • invalid token
    CauseToken hash lookup failed
    FixCreate a new token and retry
  • Unauthorized
    CauseMissing auth context
    FixSign in, or send a bearer token
  • Forbidden
    CauseChart belongs to another user
    FixUse your own chart ID
  • Chart not found
    CauseInvalid chart ID
    FixCopy the chart ID from the workspace
  • Connection refused
    CauseNetwork or domain issue
    FixCheck the URL and internet access
  • time is milliseconds
    CauseTimestamps too large
    FixDivide by 1000 for seconds
  • Non-numeric plotted field
    CauseA string or NaN in a data column
    FixEnsure every plotted value is a number
  • Incomplete candlestick
    CauseMissing OHLC metadata entries
    FixInclude all four component entries