Skip to main content

Overview

If you run your own strategy, you can connect it to Innova’s copy engine without sharing your code with us. You run the strategy on your own server and send us only the signal it already generated. We validate it, check that it is still fresh, and copy it to the subscribed accounts.
The provider never sends us their code, only signals.
All examples use the production host https://cloud.innova-trading.com. Always develop against the sandbox endpoint (/signals/test) first.

How to get access

Access is granted by Innova. There is no self-service sign-up yet.
  1. Ask your contact at Innova for a provider token and the x-community-slug of your community (for example my-community).
  2. Keep both private. Never paste them in chats, emails or repositories.
  3. Start with the sandbox endpoint (/signals/test) before sending anything real.

Endpoints

Authentication

Two headers on every call:
  • This is not a free-form API key. The token is personal and is handed to you by Innova (see “How to get access”). The x-community-slug header is mandatory on every call.
  • The token is personal: whoever has it can send signals in your name, so do not share it and never paste it in chats or emails.
  • No token, or a revoked token: HTTP 401.
  • Your signals are isolated inside your community.

The JSON you send

We do not check that sl and tps are on the right side of entry. Today the server does not reject a stop loss on the wrong side (for example an sl above entry on a buy). Validate this in your own code before sending: a wrongly placed stop is accepted as sent.
An unknown field makes the call fail with HTTP 422. This is on purpose: if you write stop_loss instead of sl, we prefer to tell you rather than accept a signal without a stop. A generated_at without a time zone is read as UTC. Keep your server clock synchronized with NTP.

Idempotency

The pair (your provider, your signal_id) is unique on our side.
  • If you retry because of a network failure, send the same signal_id (and the same body).
  • The second call returns "status": "duplicate" with the first result and no extra trade is opened.
  • Never reuse a signal_id for a different signal: the second one would be discarded as a repeat.
  • We also answer duplicate with code active_same_direction if you already have an open signal on the same symbol and direction. We do not stack positions.
A good signal_id is deterministic: strategy + symbol + timeframe + candle time. That way, restarting your process on the same candle never duplicates.

Freshness

A late signal cannot be copied at a price that no longer exists. Before publishing, we check: Today these values are global (the same for every provider). If your strategy works on longer timeframes and 120 s is too short, tell us. The 0.2 % drift rule is directional: a move in your favor never rejects the signal. The 1 % absolute rule is not: an entry more than 1 % away from the live market, on either side, is rejected with price_drift_exceeded, because that price does not exist in the market. If there is no live quote to compare against, a signal with a numeric entry is also rejected (no_market_price, or price_drift_exceeded if the price cannot be measured).

Rejection codes

Branch on code, never on the text of detail. Nothing is silently dropped. Every decision, accepted or rejected, is recorded and available at GET /api/v1/provider/signals/decisions.

The response

(Abridged response: it carries more fields.)
  • status: published (copied), simulated (sandbox), duplicate or rejected.
  • published is true only if the signal reached the copy engine. If it is false, read code.
  • signal_source is the identifier accounts use to subscribe to your strategy. Send it to your community owner.
  • received_at - generated_at is your delivery latency. Log it.
HTTP codes: 200 whenever the call was well formed (even if the signal was rejected: the reason is in code), 401 without a valid token, 409 when the signal could not be saved because of a rare uniqueness conflict (retry with a different signal_id), 422 for a bad JSON, 429 above 120 signals per minute.

Sandbox

POST /api/v1/provider/signals/test is where you should develop. It does the same as the real endpoint (validates the JSON, runs the freshness filter, resolves the symbol against subscribed accounts, computes lot size and partial-close plan) and returns exactly what would happen, including rejection reasons. It never publishes or opens anything. The visible difference: "status": "simulated", "dry_run": true, "published": false, and an extra execution_preview block. If targets_matched is 0, your signal is valid but no account is subscribed to your signal_source yet (the community owner sets that up).

Examples

To send for real, use the same call and replace /signals/test with /signals.
Replace <YOUR_TOKEN> and <YOUR_COMMUNITY> with the values we give you. Never commit them to a repository.

Integration checklist

  1. Ask your contact at Innova for the token and the x-community-slug.
  2. Send a signal to /signals/test. Expect "status": "simulated".
  3. Trigger a rejection on purpose (send a generated_at from one hour ago) and confirm your code handles code: "stale".
  4. Send an invented field and confirm your code handles the 422.
  5. Check that your code retries with the same signal_id when a call fails (the Python kit, when available, does it for you). The sandbox always answers simulated: duplicate only shows up on the real /signals, so you will see it there for the first time. That is expected.
  6. Only then switch to /signals.
  7. Send us the signal_source from the response so we can subscribe accounts.

Convert your PineScript indicator

Coming soon. This section and the next one describe the Python kit (innova-signal-kit), which is not downloadable yet. If you want early access, ask your contact at Innova. Until then, use the plain HTTP contract described above, which works today.
If your strategy lives in TradingView as PineScript, you do not rewrite it by hand. Everything comes in the Python kit (a pip package, see “Ready-made Python kit” below). It includes a ready-made prompt (PROMPT-CONVERTIR-PINE.md, copied into your folder by innova-provider init, or printed with innova-provider prompt) that you paste into an AI assistant together with your Pine code, and it returns your strategy.py in the format the kit expects.
Innova does not receive your Pine code. But the AI you paste it into does: a cloud assistant (Claude, ChatGPT or another) is a third party and receives your Pine. If your strategy is sensitive, use a local AI or review that service’s privacy terms first.
  1. Paste the prompt and your Pine code into your assistant, and save the result as strategy.py.
  2. Export your candles and your indicator’s signals from TradingView to two CSV files. The Strategy Tester trade list shows the time of the candle after the signal: in that case add --expected-shift 1 to the command in step 3.
  3. Run innova-provider parity --bars bars.csv --expected expected.csv --symbol EURUSD --timeframe 15 --strategy-name my-strategy.
  4. Acceptance criterion: 99 % of the signals match. --warmup defaults to your REQUIRED_BARS (live, evaluate always starts with that full history): you do not need to pass it. Check convergence: run the tool with two values of REQUIRED_BARS (for example 300 and 600) and keep the larger one if the result improves; the tool also prints an AVISO: when more history raises the match. Indicators with path-dependent state (supertrend, ATR/RMA/EMA, trailing stops) do not converge with “5 times the period”: measured with a supertrend + RSI, REQUIRED_BARS = 70 gave 94 % and 150 gave 100 %; start at 300. The tool answers NO APROBADO: historia insuficiente (even at 100 %) when the CSV has fewer than 4 times REQUIRED_BARS candles (the message gives the exact figure) or when fewer than 10 signals are compared. Your CSV must meet both conditions: at least 4 times REQUIRED_BARS candles AND at least 10 Pine signals in that stretch; if your Pine gives fewer, export more history until it does (on M15 with little activity that can take months); Pine signals before the first evaluated candle are ignored and only reported. If it still falls short, the tool lists every difference. Paste them back to your assistant and ask it to fix the conversion.
  5. Only then move on to the sandbox (/signals/test) and, last, to /signals.
Some things do not convert on their own: indicators that read another timeframe (request.security), logic inside the candle, and strategies that depend on an open position. The prompt flags them as “Doubts” so you can decide.

Ready-made Python kit

Coming soon, by request. The kit is not published: there is no public pip install yet. Ask your contact at Innova for access. Do not rely on it for your integration today; use the HTTP contract (see “Prefer another language?”).
We provide a Python kit (client with retries and latency measurement, a runner that waits for each candle close, and a strategy template where you only edit strategy.py). It is a pip package called innova-signal-kit, with a single command: innova-provider.

Installation

Python 3.9 or newer. Installation instructions will be shared when you get access to the kit. There is no public package index entry yet. Your strategy.py is not part of the package: it lives in your own folder, on your server, and Innova never receives it.

Flow

run accepts --dry-run, --once, --env-file, --margin, --bars, --fetch-attempts and -v; parity accepts the options described above. The README inside the kit explains everything.

If the network fails

  • The client makes up to 4 attempts per send (the first plus 3 retries). If it still does not arrive, the log says SEÑAL PERDIDA with the candle time and the signal_id.
  • If the cause was the network or a 5xx, the runner retries it once in the next cycle with the same signal_id (the server does not duplicate), only while the candle is still within the 120 s freshness window (true on M1, not on longer candles; the log says so). A 401 or 422 is not retried.
  • After a long outage the log says Se omitieron N velas sin evaluar (desde X hasta Y): the runner only evaluates the last closed candle, so signals of those candles are not sent and not replayed (they would arrive stale). A restart between a candle close and the next cycle also loses that candle.
  • In --dry-run the 120 s candle-age guard is not applied: a test with the market closed passes and the same candle would be skipped in real mode. The reported generated_at->received_at latency includes the client retries.

Your past signals (history)

What it is. Your strategy runs on your machine over past candles and gives your last N signals (50 by default, --last N). It uses the same window as live (REQUIRED_BARS closed candles, the last one is the candle that just closed). Candles come from --bars-csv (same format as parity) or, if you do not pass it, from /api/external/bars paging backwards with before_time (up to 1000 per request, at most --max-bars, 10 000 by default; if that is not enough for N signals the command says how many it found and why it stopped). The result is history.json in your folder: each signal has only the contract fields plus kind: "backtest" and source: "kit-history"; generated_at is the close of the signal’s candle and the signal_id starts with backtest-. The command also prints a summary (how many signals, date range, buy/sell). What it is not. It is not a backtest with PnL: nothing simulates SL or TP and there is no equity curve; you only get which signals your strategy would have given. It is always marked backtest, so it can never be mistaken for a live signal, and it is never mixed with the live record. Your code and parameters do not leave your machine: only the contract fields travel. Known limit today. The server accepts before_time but does not apply it yet, so downloading more than one page of candles does not work: the command stops with a clear message. Use --bars-csv with your own history. Sending it: automatic sending from the kit is not available yet. The endpoint POST /api/v1/provider/signals/history already exists on the server, but the kit’s --send flag is not available yet (--send fails with an error and sends nothing). If you post it yourself over HTTP, do not send history.json as is: it has the top-level keys kind, source and summary, and the endpoint rejects unknown keys with a 422 extra_forbidden. The body of POST /api/v1/provider/signals/history is {"signals": [ ... ]}: only the array stored under the signals key of history.json, without kind, source or summary. Send at most 50 signals per request (split into batches if you have more; a longer list gets a 422 too_long). Each element of that array carries the contract fields plus kind: "backtest" and source, which the endpoint accepts per signal; in history entry must be a number.

History limits

These limits apply to the history you send to POST /api/v1/provider/signals/history (the endpoint the “Sending it” note above refers to). Each community can have its own values. The Innova platform team can adjust both limits for your community without a release (allowed range: retention 7 to 365 days, cap 50 to 5000 signals). The values in force for you come back in every response of the endpoint, in the retention_days and max_per_provider fields: trust those, not the defaults above. When a signal is not stored, its entry in results has status: "rejected" and one of these codes. The rest of the batch is still processed. Signals you already sent are answered as duplicate even when you are at the cap, so retrying a batch is always safe. Lowering a limit never deletes what is already stored.

Prefer another language?

This is the path that works today. There is no JS or other SDK, and none is promised: the contract is plain HTTP and works from any language. What you have to implement:
  • Endpoints (see “Endpoints” above): read candles from /api/external/bars, send signals to /api/v1/provider/signals/test (sandbox) and then /api/v1/provider/signals.
  • Headers: Authorization: Bearer <token> and x-community-slug.
  • Fields: exactly those in “The JSON you send”; an unknown field is a 422.
  • Stop and targets: check on your side that sl and tps are on the right side of entry; the server does not check it.
  • signal_id: deterministic (strategy + symbol + timeframe + candle time), and retry with the same one. See “Idempotency”.
  • 120 s guard: generate the signal when the candle closes and only send it if that close was at most 120 s ago. generated_at is “now” and the server rejects it as stale past 120 s (see “Freshness”). Do not send the signal of an old candle (weekend, delayed data).
  • Candles: use only closed ones; drop the one still forming.
The sections above (endpoints, JSON, idempotency, freshness, rejection codes) are the contract reference. The Python kit is the reference implementation: you can port its runner and parity module line by line.

Next Steps

Authentication

How API tokens work

Signal Best Practices

Tips for better signals