Auto-ingest data
Connect your AI agent
Start hereQuantMaxi 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.
claude mcp add quantmaxi \ --env API_TOKEN=qmx_YOUR_TOKEN \ -- npx -y quantmaxi-mcp
codex mcp add quantmaxi \ --env API_TOKEN=qmx_YOUR_TOKEN \ -- npx -y quantmaxi-mcp
{
"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.
publish backtest.parquet to QuantMaxi as "SMA cross", BTC-USD daily
| Tool | What it does |
|---|---|
inspect_dataset | Reads the column names and a few sample rows, then proposes the plot layout. The dataset itself never enters the conversation. |
validate_payload | Converts locally and returns the exact rejection reason, in milliseconds, instead of a 400 after the upload. |
upload_dataset | Publishes to your account. Rows stream from disk to the API, not through the model. |
generate_adapter_script | Writes a standalone script for the mapping you just confirmed, so the next run needs no AI at all. |
contract_reference | The full contract, for the edge cases below. |
inspect_datasetWhat it doesReads the column names and a few sample rows, then proposes the plot layout. The dataset itself never enters the conversation.validate_payloadWhat it doesConverts locally and returns the exact rejection reason, in milliseconds, instead of a 400 after the upload.upload_datasetWhat it doesPublishes to your account. Rows stream from disk to the API, not through the model.generate_adapter_scriptWhat it doesWrites a standalone script for the mapping you just confirmed, so the next run needs no AI at all.contract_referenceWhat it doesThe full contract, for the edge cases below.
Show the same entry as Codex TOML, for ~/.codex/config.toml
[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.
curl -fsSL -O https://www.quantmaxi.com/quantmaxi_adapter.py
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
pip install yfinance pandas numpy requests
#!/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)API_TOKEN=<YOUR_TOKEN> CHART_NAME=default python3 strategy.py
| Env var | Default | Description |
|---|---|---|
API_TOKEN | — | Required for upload. The script skips uploading if it is missing. |
CHART_NAME | default | Name of the chart to create or update. |
TICKER | ETH-USD | Asset symbol for yfinance. |
API_TOKENDefault—DescriptionRequired for upload. The script skips uploading if it is missing.CHART_NAMEDefaultdefaultDescriptionName of the chart to create or update.TICKERDefaultETH-USDDescriptionAsset 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.
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
{
ticker: string,
interval: string,
columns: string[],
data: Array<Record<string, number | string | null>>,
metadata: Record<string, any>
}timeis 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
NaNandInfinityfirst - Candlestick requires all four OHLC components in metadata
- At most 200,000 rows and 512 columns per payload
chart: N is mapped to paneIndex: N-1 automatically. Use paneIndex directly in new code.{
"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
| Type | Required fields | Style keys | Notes |
|---|---|---|---|
candlestick | 4 entries: component: open|high|low|close | built-in | Always pane 0 |
line | numeric column | color | paneIndex |
area | numeric column | color | paneIndex |
histogram | numeric column | upColor, downColor | paneIndex |
markers | numeric column | color, shape, position | paneIndex |
candlestickRequired fields4 entries:component: open|high|low|closeStyle keysbuilt-inNotesAlways pane 0lineRequired fieldsnumeric columnStyle keyscolorNotespaneIndexareaRequired fieldsnumeric columnStyle keyscolorNotespaneIndexhistogramRequired fieldsnumeric columnStyle keysupColor,downColorNotespaneIndexmarkersRequired fieldsnumeric columnStyle keyscolor,shape,positionNotespaneIndex
API endpoints
| Endpoint | Auth | Body shape |
|---|---|---|
/api/ingest | Bearer token | Wrapped: {"chartName":"...","payload":{...}} |
/api/charts/by-id/{id}/ingest | Bearer token | Raw payload |
/api/charts/{name}/ingest | Browser session | Raw payload |
/api/ingestAuthBearer tokenBody shapeWrapped:{"chartName":"...","payload":{...}}/api/charts/by-id/{id}/ingestAuthBearer tokenBody shapeRaw payload/api/charts/{name}/ingestAuthBrowser sessionBody shapeRaw payload
Show 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...}'/api/ingest wants the wrapper, /api/charts/by-id/ wants the raw payload. Sending one to the other is the most common failure. Never put a real token in a doc or a screenshot.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
missing token | No auth header on /api/ingest | Add Authorization: Bearer <TOKEN> |
invalid payload | Wrong payload shape | Use the full envelope with payload.data[] |
Invalid JSON body | Body is not valid JSON | Validate the JSON before sending |
invalid token | Token hash lookup failed | Create a new token and retry |
Unauthorized | Missing auth context | Sign in, or send a bearer token |
Forbidden | Chart belongs to another user | Use your own chart ID |
Chart not found | Invalid chart ID | Copy the chart ID from the workspace |
Connection refused | Network or domain issue | Check the URL and internet access |
time is milliseconds | Timestamps too large | Divide by 1000 for seconds |
Non-numeric plotted field | A string or NaN in a data column | Ensure every plotted value is a number |
Incomplete candlestick | Missing OHLC metadata entries | Include all four component entries |
missing tokenCauseNo auth header on /api/ingestFixAdd Authorization: Bearer <TOKEN>invalid payloadCauseWrong payload shapeFixUse the full envelope with payload.data[]Invalid JSON bodyCauseBody is not valid JSONFixValidate the JSON before sendinginvalid tokenCauseToken hash lookup failedFixCreate a new token and retryUnauthorizedCauseMissing auth contextFixSign in, or send a bearer tokenForbiddenCauseChart belongs to another userFixUse your own chart IDChart not foundCauseInvalid chart IDFixCopy the chart ID from the workspaceConnection refusedCauseNetwork or domain issueFixCheck the URL and internet accesstime is millisecondsCauseTimestamps too largeFixDivide by 1000 for secondsNon-numeric plotted fieldCauseA string or NaN in a data columnFixEnsure every plotted value is a numberIncomplete candlestickCauseMissing OHLC metadata entriesFixInclude all four component entries