QuantMaxi Auto-Ingest Contract: Exact data + metadata format

This page documents current renderer behavior only. It clarifies exactly what format traders must send.

1) Payload Envelope (Top-level JSON)

Use this exact top-level payload shape:

{
  ticker: string,
  interval: string,
  columns: string[],
  data: Array<Record<string, number | string | null>>,
  metadata: Record<string, any>
}

2) Data Array Contract (rows)

  1. data must be an array of row objects.
  2. Each row must include time.
  3. time must be Unix seconds (UTC integer).
  4. Rows must be sorted ascending by time.
  5. Candlestick requires numeric Open, High, Low, Close.
  6. Volume is optional.
  7. Indicator columns are optional but must be numeric if plotted.
  8. Clean NaN, Infinity, -Infinity before ingest.

3) Time Format Contract

  1. Accepted format is Unix seconds.
  2. Milliseconds must be divided by 1000.
  3. Nanoseconds must be divided by 1_000_000_000.
  4. Rows should remain UTC and monotonic by time.

4) Metadata Contract (series + styling)

  1. Metadata keys are series keys (Open, SMA20, Volume, and so on).
  2. Supported type: candlestick, line, area, histogram, markers.
  3. Candlestick entries should include component: open|high|low|close.
  4. Pane placement should use paneIndex (0 is main pane).
  5. Style keys: color, upColor, downColor by series type.
  6. Unknown keys are ignored unless supported by the current renderer.

5) Legacy Mapping Rules (chart/overlay/chart1...)

  1. chart: 1|2|3 maps to paneIndex: 0|1|2.
  2. overlay is currently ignored by renderer behavior.
  3. chart1/chart2/chart3 layout config (height, timescale) is currently ignored.
  4. Date metadata key is currently ignored.

Legacy before:

{
  "chart1": { "height": 60, "timescale": false },
  "Date": { "plot": true, "type": "time" },
  "Open": { "plot": true, "type": "candlestick", "component": "open", "chart": 1 },
  "SMA20": { "plot": true, "type": "line", "overlay": true, "color": "#f07b01", "chart": 1 },
  "Equity": { "plot": true, "type": "area", "overlay": true, "color": "#2962ff", "chart": 2 }
}

Normalized after:

{
  "Open": { "plot": true, "type": "candlestick", "component": "open" },
  "SMA20": { "plot": true, "type": "line", "color": "#f07b01", "paneIndex": 0 },
  "Equity": { "plot": true, "type": "area", "color": "#2962ff", "paneIndex": 1 }
}
Use legacy normalization before ingest to avoid rendering surprises.

6) Canonical Valid Example

{
  "ticker": "BTC-USD",
  "interval": "1d",
  "columns": ["time", "Open", "High", "Low", "Close", "Volume", "SMA20", "SMA50", "Signal", "Holdings", "Cash", "Equity"],
  "data": [
    {
      "time": 1713657600,
      "Open": 64123.4,
      "High": 64980.2,
      "Low": 63881.7,
      "Close": 64720.1,
      "Volume": 22812.6,
      "SMA20": 63110.2,
      "SMA50": 61922.5,
      "Signal": 1,
      "Holdings": 915240.4,
      "Cash": 84759.6,
      "Equity": 1000000.0
    }
  ],
  "metadata": {
    "ticker": "BTC-USD",
    "interval": "1d",
    "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 }
  }
}

7) Invalid Example + Why It Fails

{
  "ticker": "BTC-USD",
  "interval": "1d",
  "columns": ["time", "Open", "High", "Low", "Close", "SMA20"],
  "data": [
    { "time": 1713657600000, "Open": "bad", "High": 64980.2, "Low": 63881.7, "Close": 64720.1, "SMA20": "NaN" }
  ],
  "metadata": {
    "Open": { "plot": true, "type": "candlestick" },
    "SMA20": { "plot": true, "type": "line", "paneIndex": 0 }
  }
}
  1. time is milliseconds, not seconds.
  2. Open is non-numeric.
  3. SMA20 is non-numeric.
  4. Candlestick metadata is incomplete for full OHLC candle setup.

8) Quick Conversion Checklist (for AI/manual edits)

AI adapter prompt:

Convert my existing strategy output to the QuantMaxi payload contract.
Do not change strategy logic.
Only convert output formatting to QuantMaxi payload fields.
Map legacy metadata keys (chart/overlay/chart1...) to canonical keys (paneIndex, supported series keys).
Return final JSON payload and adapter code only.
Do not include secrets, API tokens, or environment variables.

Verification checklist:

  1. time is Unix seconds.
  2. data is an array of rows.
  3. Plotted fields are numeric.
  4. Legacy chart is converted to paneIndex.
  5. No NaN, Infinity, -Infinity left in payload.

9) Documentation URL

  1. https://www.quantmaxi.com/documentation
  2. https://www.quantmaxi.com/documentation/auto-ingest-data

10) Run `strategy.py` Only (Automatic Upload)

Recommended flow:

  1. Build payload in your script.
  2. Save quantmaxi_payload.json for debugging.
  3. Auto-upload inside the same script using API_TOKEN.

Environment variables used by script:

  1. API_TOKEN (required for upload).
  2. CHART_NAME (default default).
  3. CHART_ID (optional; if set, by-id endpoint is used).

Run:

API_TOKEN=<YOUR_API_TOKEN> \
CHART_NAME=default \
python3 strategy.py
If API_TOKEN is missing, script should skip upload and still save local JSON.

Install dependencies once:

python3 -m pip install yfinance pandas numpy requests

Working strategy.py example:

#!/usr/bin/env python3
import os
import json
import requests

import yfinance as yf
import pandas as pd
import numpy as np

# ----------------------------
# Strategy parameters (env-overridable)
# ----------------------------
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 returned for {TICKER}")

    if isinstance(data.columns, pd.MultiIndex):
        data.columns = data.columns.get_level_values(0)

    for col in ("Open", "High", "Low", "Close", "Volume"):
        if col not in data.columns:
            if col == "Volume":
                data[col] = 0.0
            else:
                raise RuntimeError(f"Missing required column: {col}")

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

    data["Signal"] = 0

    cross_up = (
        (data["SMA20"] > data["SMA50"])
        & (data["SMA20"].shift(1) <= data["SMA50"].shift(1))
    )
    cross_down = (
        (data["SMA20"] < data["SMA50"])
        & (data["SMA20"].shift(1) >= data["SMA50"].shift(1))
    )

    data.loc[cross_up, "Signal"] = 1
    data.loc[cross_down, "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")

    columns = [
        "time",
        "Open",
        "High",
        "Low",
        "Close",
        "Volume",
        "SMA20",
        "SMA50",
        "Signal",
        "Holdings",
        "Cash",
        "Equity",
    ]
    data = data[columns]

    data.replace([np.inf, -np.inf], np.nan, inplace=True)
    data.fillna(0, inplace=True)

    for c in columns:
        if c != "time":
            data[c] = pd.to_numeric(data[c], errors="coerce").fillna(0)

    data.sort_values("time", inplace=True)

    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},
    }

    return {
        "ticker": TICKER,
        "interval": INTERVAL,
        "columns": columns,
        "data": data.to_dict(orient="records"),
        "metadata": metadata,
    }


def upload_payload(payload: dict) -> None:
    base_url = "https://www.quantmaxi.com"
    token = os.getenv("API_TOKEN", "").strip()
    chart_name = os.getenv("CHART_NAME", "default").strip() or "default"
    chart_id = os.getenv("CHART_ID", "").strip()

    if not token:
        print("No API_TOKEN set -> upload skipped (JSON still saved).")
        return

    if chart_id:
        url = f"{base_url}/api/charts/by-id/{chart_id}/ingest"
        body = payload
    else:
        url = f"{base_url}/api/ingest"
        body = {"chartName": chart_name, "payload": payload}

    try:
        resp = requests.post(
            url,
            headers={
                "Authorization": f"Bearer {token}",
                "Content-Type": "application/json",
            },
            json=body,
            timeout=60,
        )
        print(f"Upload status: {resp.status_code}")
        print(resp.text)
    except Exception as e:
        print(f"Upload failed: {e}")


def main() -> None:
    payload = build_payload()

    with open(PAYLOAD_OUT, "w", encoding="utf-8") as f:
        json.dump(payload, f, indent=2)

    print(f"Payload saved to {PAYLOAD_OUT}")
    upload_payload(payload)


if __name__ == "__main__":
    main()

11) Broker Integrations (Separate from Ingest)

  1. Use /integrated-brokers to connect and manage OANDA credentials.
  2. /profile and /u/<handle> only display broker performance output.
  3. Strategy auto-ingest and broker performance are separate systems.

12) Advanced Endpoint Usage (Optional)

Use this only if you need direct manual API calls instead of the python3 strategy.py flow.

# Optional manual API mode
API_TOKEN=<YOUR_API_TOKEN>

# /api/ingest (manual call)
jq -n --arg chartName "default" --slurpfile payload quantmaxi_payload.json \
'{"chartName":$chartName,"payload":$payload[0]}' \
| curl -X POST "https://www.quantmaxi.com/api/ingest" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d @-

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

# /api/charts/{name}/ingest (logged-in browser session only)
curl -X POST "https://www.quantmaxi.com/api/charts/default/ingest" \
  -H "Content-Type: application/json" \
  -d '{...payload...}'
What to change:
  1. Change API_TOKEN.
  2. Change chartName only if your chart name is not default.
  3. Keep wrapper shape exactly as-is for /api/ingest.
EndpointBody shape
/api/ingestWrapped body: {"chartName":"...","payload":{...}}
/api/charts/by-id/{id}/ingestRaw payload body: {...payload...}
/api/charts/{name}/ingestRaw payload body + logged-in browser session
Most common mistake:
  1. Raw payload sent directly to /api/ingest returns invalid payload.
  2. Wrapped payload sent to /api/charts/by-id/{id}/ingest is wrong for that endpoint.
Security note:
  1. Never paste real tokens in docs/chat/screenshots.
  2. If exposed, revoke and regenerate immediately.

13) Troubleshooting by Error Message

ErrorMeaningAction
missing token/api/ingest request has no token.Send Authorization: Bearer <API_TOKEN>.
invalid payloadPayload shape is wrong.Send full envelope with payload.data[].
Invalid JSON bodyBody is not valid JSON.Validate JSON before posting.
invalid token or Invalid or revoked tokenToken hash/lookup failed.Rotate token and retry.
Unauthorized / Unauthorized (no session or token)Missing auth context.Sign in or provide bearer token. For remote jobs use token endpoints.
ForbiddenChart belongs to another user.Use your own chart id.
Chart not foundChart id is invalid.Copy chart id again from Chart workspace.
Connection/refused/not foundNetwork/domain issue while calling QuantMaxi.Check internet access and retry.