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)
datamust be an array of row objects.- Each row must include
time. timemust be Unix seconds (UTC integer).- Rows must be sorted ascending by
time. - Candlestick requires numeric
Open,High,Low,Close. Volumeis optional.- Indicator columns are optional but must be numeric if plotted.
- Clean
NaN,Infinity,-Infinitybefore ingest.
3) Time Format Contract
- Accepted format is Unix seconds.
- Milliseconds must be divided by
1000. - Nanoseconds must be divided by
1_000_000_000. - Rows should remain UTC and monotonic by time.
4) Metadata Contract (series + styling)
- Metadata keys are series keys (
Open,SMA20,Volume, and so on). - Supported
type:candlestick,line,area,histogram,markers. - Candlestick entries should include
component: open|high|low|close. - Pane placement should use
paneIndex(0is main pane). - Style keys:
color,upColor,downColorby series type. - Unknown keys are ignored unless supported by the current renderer.
5) Legacy Mapping Rules (chart/overlay/chart1...)
chart: 1|2|3maps topaneIndex: 0|1|2.overlayis currently ignored by renderer behavior.chart1/chart2/chart3layout config (height,timescale) is currently ignored.Datemetadata 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 }
}
}timeis milliseconds, not seconds.Openis non-numeric.SMA20is non-numeric.- 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:
timeis Unix seconds.datais an array of rows.- Plotted fields are numeric.
- Legacy
chartis converted topaneIndex. - No
NaN,Infinity,-Infinityleft in payload.
9) Documentation URL
https://www.quantmaxi.com/documentationhttps://www.quantmaxi.com/documentation/auto-ingest-data
10) Run `strategy.py` Only (Automatic Upload)
Recommended flow:
- Build payload in your script.
- Save
quantmaxi_payload.jsonfor debugging. - Auto-upload inside the same script using
API_TOKEN.
Environment variables used by script:
API_TOKEN(required for upload).CHART_NAME(defaultdefault).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)
- Use
/integrated-brokersto connect and manage OANDA credentials. /profileand/u/<handle>only display broker performance output.- 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:
- Change
API_TOKEN. - Change
chartNameonly if your chart name is notdefault. - Keep wrapper shape exactly as-is for
/api/ingest.
| Endpoint | Body shape |
|---|---|
/api/ingest | Wrapped body: {"chartName":"...","payload":{...}} |
/api/charts/by-id/{id}/ingest | Raw payload body: {...payload...} |
/api/charts/{name}/ingest | Raw payload body + logged-in browser session |
Most common mistake:
- Raw payload sent directly to
/api/ingestreturnsinvalid payload. - Wrapped payload sent to
/api/charts/by-id/{id}/ingestis wrong for that endpoint.
Security note:
- Never paste real tokens in docs/chat/screenshots.
- If exposed, revoke and regenerate immediately.
13) Troubleshooting by Error Message
| Error | Meaning | Action |
|---|---|---|
missing token | /api/ingest request has no token. | Send Authorization: Bearer <API_TOKEN>. |
invalid payload | Payload shape is wrong. | Send full envelope with payload.data[]. |
Invalid JSON body | Body is not valid JSON. | Validate JSON before posting. |
invalid token or Invalid or revoked token | Token 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. |
Forbidden | Chart belongs to another user. | Use your own chart id. |
Chart not found | Chart id is invalid. | Copy chart id again from Chart workspace. |
| Connection/refused/not found | Network/domain issue while calling QuantMaxi. | Check internet access and retry. |