"""Build a reviewed BACKTEST URL locally; never navigate, execute or contact an account. The fixed mapping is a source-reviewed v0.5 ordinary-BACKTEST adapter. Catalogue labels are not payload keys. The bundled validator and strict condition corpus must approve a complete card before either public builder returns a payload. """ # (c) 2026 ITradingBot Limited. All rights reserved. # Published so that a user's own AI assistant can read the ITradingBot backtest contract and build cards and preset links for it. Not for redistribution, and not for use in another product. from __future__ import annotations import argparse import base64 from copy import deepcopy from datetime import datetime import hashlib import json from pathlib import Path import re import sys import zlib from urllib.parse import parse_qs, urlsplit from validate_card import validate_card, preset_datetime HERE = Path(__file__).resolve().parent BASE_URL = "https://app.itradingbot.net/backtest-new" MAX_URL_PAYLOAD = 8192 # Native importer checks the decoded query value length. MAX_JSON_BYTES = 65536 # Additional local generator ceiling, not a native contract. ROLE_KEYS = {"entry": "MAIN", "filter": "PRICE_LIMIT", "exit": "CLOSE_BY"} TYPE_TAGS = {"string": "str", "integer": "int", "number": "float", "boolean": "bool"} STATIC_KEYS = ( "exchange", "symbol", "account_balance_mode", "position_side", "backtest_mode", "start_dt", "end_dt", "initial_quote_qty", "first_order", "maker_commission_perc", "taker_commission_perc", "margin_type", "leverage", "tp_perc", "sl_perc", "in_step_perc", "in_dynamic_step", "in_count", "in_martin_gale", "close_by_signal_only_profit_perc", ) PROFILE_KEYS = ( "dca_enabled", "hedge_enabled", "fast_enabled", "multi_enabled", "optimizer_enabled", "stop_be_enabled", "timezone_intent", ) # Server-preset section split, read from the reviewed form markup. The loader merges the # three groups back into one object, so this split is readability only; the partition is # self-checked below so a future STATIC_KEYS edit cannot silently drop or duplicate a field. MAIN_BACKTEST_KEYS = ( "exchange", "symbol", "account_balance_mode", "position_side", "backtest_mode", "start_dt", "end_dt", ) MONEY_MANAGEMENT_KEYS = ( "initial_quote_qty", "first_order", "maker_commission_perc", "taker_commission_perc", "margin_type", "leverage", ) RISK_MANAGEMENT_KEYS = ( "tp_perc", "sl_perc", "in_step_perc", "in_dynamic_step", "in_count", "in_martin_gale", "close_by_signal_only_profit_perc", ) OPTIONAL_RISK_KEYS = ("stop_be_trigger_pct", "stop_be_val_pct") _SECTIONS = MAIN_BACKTEST_KEYS + MONEY_MANAGEMENT_KEYS + RISK_MANAGEMENT_KEYS if sorted(_SECTIONS) != sorted(STATIC_KEYS) or len(set(_SECTIONS)) != len(_SECTIONS): raise RuntimeError("The three server-preset sections must partition the 20 static fields exactly") SYMBOL_PATTERN = re.compile(r"[A-Z0-9_]{1,40}") # Pair-list ceiling of the reviewed calculation service for a VIP account (source default # MAX_SYMBOLS_PER_VIP). It is a transport ceiling, not a promise that the run is accepted. MAX_PRESET_SYMBOLS = 600 # Tier limit observed in the calculation service: an account without VIP may calculate one pair. SINGLE_SYMBOL_TIER_LIMIT = 1 # Snapshot-derived code allowlist: indicator/role, exact field keys and type tags. # Context enum changes are encoded explicitly; arbitrary catalogue remapping fails. DEFINITION_FINGERPRINT = { "adx-di.entry": "e667fa73ca43be67", "adx-di.exit": "1b4109b40a78f73f", "adx.filter": "969c6999f42cc36f", "bollinger-width.filter": "5ea02364fa054bda", "bollinger.entry": "2661da6d9111c06f", "bollinger.exit": "f4d8c547b34b79e5", "bollinger.filter": "f62d7f8ef4f54652", "chande-momentum.entry": "059c89d6dca01d36", "chande-momentum.exit": "ebefb50a4bb17c12", "chande-momentum.filter": "4b930de085a0df68", "fomo-width.filter": "0bb4170f1d67c700", "fomo.entry": "135cdbd3358cde18", "fomo.exit": "da2af5dde5fdf1a5", "fomo.filter": "b6c173e3a15dcdd3", "impulse.entry": "cd55ec7384bb67f4", "impulse.exit": "64e7518d79d2a539", "ma.entry": "ac07a5c92214bd35", "ma.exit": "cb3146ca3fc408bc", "ma.filter": "05205194a6ade587", "macd.entry": "aebd3100a18bbeec", "macd.exit": "efa3d5256c00ecb2", "macd.filter": "77853515ebe365ac", "mfi-growth-decline.filter": "816f616c030f8458", "mfi.entry": "3216dd9f19e62eb0", "mfi.exit": "e46d4e48c805212a", "mfi.filter": "661c980df31a7f31", "momentum.entry": "9857957aa97f4b41", "momentum.exit": "8271a239c328a8b7", "momentum.filter": "9c8a8e47727ee992", "obv-oscillator.entry": "64f7adc883f37482", "obv-oscillator.exit": "8c10b9eab0885a71", "obv-oscillator.filter": "3ad3d4f8d844d241", "past-impulse.filter": "407e74cebfcbe4e9", "percent-volatility.filter": "a4f3076dc0145331", "price-change.filter": "d6d52c6c1ee58a8c", "price-range.filter": "ceaec0e59123d871", "quote-volume-pump.entry": "d566155bcc7da852", "quote-volume-pump.exit": "c21ec839f3ea9512", "quote-volume-pump.filter": "461cc47f4f1827a5", "quote-volume.filter": "4dcc5066e6650400", "rsi-growth-decline.filter": "f969bdf314cd8a10", "rsi.entry": "69f62207c0aaacd3", "rsi.exit": "522cdb349e1c4b1f", "rsi.filter": "4fbf85455858cd96", "sar.entry": "51b8074f856f899d", "sar.exit": "f2b15d9339c9ea6f", "sar.filter": "764a41331626b7a4", "stochastic.entry": "a6a869634e4fa3b4", "stochastic.exit": "65a2be5205005be3", "stochastic.filter": "85edf33cf9f29845", "supertrend.entry": "0ab568839945f5b9", "supertrend.exit": "be2ad21c8346493a", "supertrend.filter": "320ac7cbd1cd9ea2", "trade-count-pump.entry": "3ebba9c33af1a67d", "trade-count-pump.exit": "45e798555bc50547", "trade-count-pump.filter": "715529eb7b49d103", "trade-count.filter": "75c147be0e7a31d5", "volatility.entry": "a0ea362ff472cdb1", "volatility.exit": "c5874d31544b375a", "vortex.entry": "ce08ebf7e5dc7cbf", "vortex.exit": "330edfb61032c8b6", "vortex.filter": "9548c1a852b2c648", } def _definition_contract(definition_id: str, catalog: dict) -> dict: """Rebuild the reviewed adapter contract for one definition out of the catalogue.""" definition = next((d for d in catalog["definitions"] if d["id"] == definition_id), None) if definition is None: raise PresetLinkError("Unsupported condition definition or role") enum_overrides = {} for context, per_definition in (catalog.get("context_overrides") or {}).items(): changed = {field_id: spec["enum_values"] for field_id, spec in (per_definition.get(definition_id) or {}).items() if "enum_values" in spec} if changed: enum_overrides[context] = changed return { "indicator": definition.get("original_indicator_id"), "role": definition.get("role"), "available_contexts": definition.get("available_contexts", []), "fields": {field["id"]: [field.get("source_key"), field.get("type"), field.get("enum_values")] for field in definition["fields"]}, "enum_overrides": enum_overrides, } def _contract_fingerprint(record: dict) -> str: """Digest of exactly the mapping this adapter was reviewed against.""" canonical = { "indicator": record["indicator"], "role": record["role"], "available_contexts": sorted(record["available_contexts"]), "fields": {fid: list(triple) for fid, triple in sorted(record["fields"].items())}, "enum_overrides": {ctx: {fid: value for fid, value in sorted(over.items())} for ctx, over in sorted(record.get("enum_overrides", {}).items())}, } blob = json.dumps(canonical, ensure_ascii=False, sort_keys=True, separators=(",", ":")) return hashlib.sha256(blob.encode()).hexdigest()[:16] MULTI_GATE = { "id": "multi", "expected": True, "reason": ("Multi-pair link: check that the pair table shows exactly the listed pairs. A pair the " "exchange list does not contain is skipped silently, and an earlier selection left in the " "table is not cleared by the link."), } TIER_GATE = { "id": "tier", "expected": None, "reason": ("A calculation over several pairs is a VIP feature: the calculation service compares the " "number of pairs with the account tier limit (one pair without VIP) and otherwise answers " "400 'Too many symbols'. This generator does not observe the account tier."), } MANUAL_GATES = [ {"id": "fast", "expected": False, "reason": "Accelerated calculation (x200) is not part of what the link restores. A VIP account may switch it on by itself, so turn it off by hand before calculating."}, {"id": "hedge", "expected": False, "reason": "Open the ordinary backtester and verify that classical hedge is disabled; no hedge settings are encoded."}, {"id": "multi", "expected": False, "reason": "Clear multi-selection: a nonempty selection overrides the scalar symbol at calculation time."}, {"id": "optimizer", "expected": False, "reason": "Use the ordinary form. No optimizer ranges or optimizer run command are encoded."}, {"id": "dates", "reason": "Verify both displayed dates, timezone interpretation and available history/warm-up. The date strings contain no UTC offset."}, {"id": "form", "reason": "After metadata loads, compare every restored scalar and card against the payload. Exchange/mode/symbol changes can clear or rewrite cards; leverage may be clamped."}, {"id": "account", "reason": "Opening the normal application may require login. This offline generator provides no session, credentials, authorization bypass or calculation command."}, {"id": "costs", "reason": "Confirm actual capital, order size, fees, leverage and instrument limits before separately authorizing a calculation."}, ] class PresetLinkError(ValueError): """A card or mapping cannot safely produce this specific URL format.""" def _manual_gates(multi: bool, multi_count: int = 0) -> list[dict]: """The same checks in both transfer forms; only the pair-selection gate differs.""" tiered = multi and multi_count > SINGLE_SYMBOL_TIER_LIMIT gates = ([deepcopy(TIER_GATE)] if tiered else []) + ([deepcopy(MULTI_GATE)] if multi else []) for gate in MANUAL_GATES: if multi and gate["id"] == "multi": continue gates.append(deepcopy(gate)) return gates def _validated_symbols(card: dict, settings: dict, symbol: str) -> list[str] | None: """Return the checked pair list, or None for the ordinary single-pair form.""" symbols = card.get("symbols") multi = settings["profile.multi_enabled"] if multi is not True and multi is not False: raise PresetLinkError("profile.multi_enabled must be an explicit boolean") if (symbols is not None) != multi: raise PresetLinkError("profile.multi_enabled must be true exactly when the card carries a pair list") if symbols is None: return None if not isinstance(symbols, list) or not symbols: raise PresetLinkError("A multi-pair card requires a nonempty array of pairs") if len(symbols) > MAX_PRESET_SYMBOLS: raise PresetLinkError( f"At most {MAX_PRESET_SYMBOLS} pairs are transferable; the reviewed calculation service " f"refuses a longer list even for VIP") for item in symbols: if not isinstance(item, str) or not SYMBOL_PATTERN.fullmatch(item): raise PresetLinkError("Each pair must be an ASCII uppercase ticker of at most 40 characters") # The selection matches case-insensitively in the form, deduplicates case-sensitively in the # newer loader, and counts before deduplication on the server: keep one exact spelling each. if len({item.upper() for item in symbols}) != len(symbols): raise PresetLinkError("The pair list must not repeat a pair") if symbols[0] != symbol: raise PresetLinkError("The first listed pair must equal backtest.symbol, the pair shown in the form") return list(symbols) def _server_form(single: dict, symbols: list[str]) -> dict: """Re-pack an already validated payload into the server-preset shape the loader reads. Values are the same checked values; the schema type tag is dropped because in this form the application takes each field's type from its own form metadata, not from the link. """ def flat(cards: list) -> list[dict]: return [{"n": name, **{key: value for key, (_, value) in typed.items()}} for name, typed, _ in cards] fields = single["fields"] return { "symbols": list(symbols), "exchange": single["exchange"], "main_backtest": {"exchange": single["exchange"], **{key: fields[key] for key in MAIN_BACKTEST_KEYS if key != "exchange"}}, "money_management": {key: fields[key] for key in MONEY_MANAGEMENT_KEYS}, "risk_management": {key: fields[key] for key in RISK_MANAGEMENT_KEYS + OPTIONAL_RISK_KEYS if key in fields}, # All three groups must exist even when empty: the loader reduces over each one. "main_inds": flat(single["cards"]["MAIN"]), "price_limit_inds": flat(single["cards"]["PRICE_LIMIT"]), "close_by_inds": flat(single["cards"]["CLOSE_BY"]), } def _validated_payload(card: dict, catalog: dict) -> tuple[dict, dict]: validation = validate_card(card, catalog) if not validation.get("valid"): codes = sorted({item.get("code", "invalid") for item in validation.get("errors", [])}) raise PresetLinkError("Card validation failed: " + ", ".join(codes or ["invalid"])) # Status no longer blocks a link. The URL only fills the form and starts nothing, so it is useful # even when the user did not supply every value: they can correct any field in the panel. The hard # requirement is different and is checked below — no encoded value may be null. Whatever the user # did not supply is listed in the report under "assumed_values". if card.get("status") not in {"complete_example", "complete_proposal", "needs_input"}: raise PresetLinkError("Unknown card status") settings = {field["field_id"]: field["value"] for field in card["settings"]} base_settings = {"backtest." + key for key in STATIC_KEYS} | {"profile." + key for key in PROFILE_KEYS} stop_be = settings.get("profile.stop_be_enabled") if type(stop_be) is not bool: raise PresetLinkError("profile.stop_be_enabled must be an explicit boolean") optional_settings = {"backtest." + key for key in OPTIONAL_RISK_KEYS} if stop_be else set() expected_settings = base_settings | optional_settings if set(settings) != expected_settings or any(value is None for value in settings.values()): raise PresetLinkError("Exactly the 20 required fields, 7 profile annotations, and the enabled BE pair are required") for key in ("hedge_enabled", "fast_enabled", "optimizer_enabled"): if settings["profile." + key] is not False: raise PresetLinkError("This URL adapter supports only the ordinary, non-fast, non-hedge profile") exchange = settings["backtest.exchange"] if exchange not in {"BINANCE", "BYBIT"}: raise PresetLinkError("Unsupported exchange") symbol = settings["backtest.symbol"] if not isinstance(symbol, str) or not SYMBOL_PATTERN.fullmatch(symbol): raise PresetLinkError("The reviewed adapter requires an ASCII uppercase ticker") symbols = _validated_symbols(card, settings, symbol) mode = settings["backtest.backtest_mode"] if mode not in {"AVERAGE_BY_LIMIT", "AVERAGE_BY_SIGNAL"}: raise PresetLinkError("Unsupported backtest mode") dca = mode == "AVERAGE_BY_SIGNAL" if settings["profile.dca_enabled"] is not dca: raise PresetLinkError("DCA annotation conflicts with the backtest mode") context = f"backtest-{exchange.lower()}-dca-{str(dca).lower()}" if card["context_id"] != context: raise PresetLinkError("Context must match exchange and backtest mode") fields = {key: settings["backtest." + key] for key in STATIC_KEYS if key != "exchange"} if stop_be: fields.update({key: settings["backtest." + key] for key in OPTIONAL_RISK_KEYS}) for key in ("start_dt", "end_dt"): value = fields[key] try: parsed = preset_datetime(value) except ValueError as exc: raise PresetLinkError("Dates must be complete local wall-clock minutes without an offset") from exc fields[key] = parsed.strftime("%Y-%m-%dT%H:%M") definitions = {item["id"]: item for item in catalog["definitions"]} contextual = catalog.get("context_overrides", {}).get(context, {}) cards = {key: [] for key in ROLE_KEYS.values()} for index, condition in enumerate(card["conditions"], start=1): definition_id = condition["definition_id"] definition = definitions.get(definition_id) if not definition or definition_id not in DEFINITION_FINGERPRINT: raise PresetLinkError("Unsupported condition definition or role") fixed = _definition_contract(definition_id, catalog) # The reviewed mapping is checked by digest instead of a second copy of the catalogue: # any change to a source key, type or enum list moves the digest and stops encoding here. if _contract_fingerprint(fixed) != DEFINITION_FINGERPRINT[definition_id]: raise PresetLinkError("Catalogue field mapping differs from the reviewed adapter") if condition["role"] != fixed["role"]: raise PresetLinkError("Unsupported condition definition or role") if context not in fixed["available_contexts"]: raise PresetLinkError("Condition is unavailable for this exchange and backtest mode") specs = {field["id"]: field for field in definition["fields"]} values = {field["field_id"]: field["value"] for field in condition["fields"]} if set(specs) != set(fixed["fields"]) or set(values) != set(fixed["fields"]): raise PresetLinkError("Every exact reviewed condition field must be supplied") typed_fields = {} for field_id, (source_key, field_type, base_enum) in fixed["fields"].items(): spec = {**specs[field_id], **contextual.get(definition_id, {}).get(field_id, {})} allowed = fixed["enum_overrides"].get(context, {}).get(field_id, base_enum) value = values[field_id] if value is None: raise PresetLinkError("Null condition values cannot be restored") if source_key == "ss" and value != symbol: raise PresetLinkError("Native import rewrites every card asset to the main selected symbol") if allowed is not None and value not in allowed: raise PresetLinkError("Condition value is outside the exact context enum") typed_fields[source_key] = [deepcopy(allowed) if allowed is not None else TYPE_TAGS[field_type], value] cards[ROLE_KEYS[fixed["role"]]].append([fixed["indicator"], typed_fields, index]) if len(cards["MAIN"]) != 1 or len(cards["PRICE_LIMIT"]) > 10 or len(cards["CLOSE_BY"]) > 3: raise PresetLinkError("The reviewed profile allows one entry, up to ten filters and three signal exits") single = {"exchange": exchange, "fields": fields, "cards": cards} if symbols is None: return single, validation return _server_form(single, symbols), validation def build_payload(card: dict, catalog: dict) -> dict: """Validate and return only the allowlisted native custom-preset object.""" return _validated_payload(card, catalog)[0] def _encode(payload: dict) -> tuple[bytes, str]: raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8") return raw, "1~" + base64.urlsafe_b64encode(zlib.compress(raw)).decode("ascii").rstrip("=") def _pairs_that_fit(payload: dict) -> int: """Largest prefix of this exact pair list whose encoded parameter still fits the ceiling.""" symbols = payload["symbols"] low, high = 0, len(symbols) while low < high: middle = (low + high + 1) // 2 trial = {**payload, "symbols": symbols[:middle]} raw, encoded = _encode(trial) if len(raw) <= MAX_JSON_BYTES and len(encoded) <= MAX_URL_PAYLOAD: low = middle else: high = middle - 1 return low def build_preset_link(card: dict, catalog: dict) -> dict: """Validate and return a URL plus explicit restoration limits; does not execute.""" payload, validation = _validated_payload(card, catalog) symbols = payload.get("symbols") break_even_encoded = ( "stop_be_trigger_pct" in payload.get("fields", {}) or "stop_be_trigger_pct" in payload.get("risk_management", {}) ) raw, encoded = _encode(payload) if len(raw) > MAX_JSON_BYTES or len(encoded) > MAX_URL_PAYLOAD: detail = "Payload exceeds the local uncompressed JSON ceiling" if len(raw) > MAX_JSON_BYTES else \ "Encoded customPreset exceeds the native 8192-character limit" if symbols is not None: fits = _pairs_that_fit(payload) raise PresetLinkError( f"{detail}: {len(symbols)} pairs do not fit. With these settings and conditions at most " f"{fits} of them fit in one link; send the rest as a separate link.") raise PresetLinkError(detail) # Every character in encoded is URL-unreserved; no second URL escaping is needed. url = BASE_URL + "?customPreset=" + encoded # Decode the actual returned URL before it can leave this function. This checks # local codec integrity only; it does not certify browser widgets or execution. try: query_value = parse_qs(urlsplit(url).query, strict_parsing=True)["customPreset"][0] base64_value = query_value[2:] decoded = json.loads(zlib.decompress(base64.urlsafe_b64decode( base64_value + "=" * (-len(base64_value) % 4))).decode("utf-8")) decoded_bytes = json.dumps(decoded, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8") if not query_value.startswith("1~") or decoded_bytes != raw: raise PresetLinkError("Local URL roundtrip does not reproduce every payload value exactly") except (ValueError, KeyError, UnicodeError, zlib.error) as exc: raise PresetLinkError("Local URL roundtrip verification failed") from exc return { "format": "itb.preset-link/0.5.0", "url": url, "roundtrip_verified": True, "roundtrip_scope": "local Python codec only; not native UI or backend execution", "payload": payload, "encoded_length": len(encoded), "json_bytes": len(raw), "preset_form": "multi" if symbols is not None else "single", "symbols": list(symbols) if symbols is not None else [payload["fields"]["symbol"]], "symbol_count": len(symbols) if symbols is not None else 1, "requires_vip": symbols is not None and len(symbols) > SINGLE_SYMBOL_TIER_LIMIT, "restored_static_fields": ["backtest." + key for key in STATIC_KEYS + (OPTIONAL_RISK_KEYS if break_even_encoded else ())], "restored_condition_fields": sum(len(condition["fields"]) for condition in card["conditions"]), # In the multi-pair form the multitest annotation is materially carried by the pair list, # so it is no longer listed among the annotations that stay behind in the card. "profile_annotations_not_encoded": ["profile." + key for key in PROFILE_KEYS if key != "stop_be_enabled" and (symbols is None or key != "multi_enabled")], "manual_gates": _manual_gates(symbols is not None, len(symbols or [])) + ([{ "id": "break_even_stop", "expected": True, "reason": "Verify BYBIT is still selected and hedge is disabled. Live transfer is supported only by a Bybit Exchange Account robot; legacy API-broker and Binance robots do not accept this pair." }] if break_even_encoded else []), "validation_warnings": validation.get("warnings", []), "card_status": card["status"], # Every setting the user did not supply. Show this beside the link so the user can see what was # filled in for them and correct it in the form. Commission rates appear here like anything else. "assumed_values": [{"field_id": field["field_id"], "value": field["value"], "origin": field["origin"], "note": field.get("note") or ""} for field in card["settings"] if field.get("origin") != "user"], "run_ready": False, "execution": {"remote_requests": False, "backtest_executed": False, "live_trade_executed": False}, } def _read_json(path: Path): def unique(pairs): obj = {} for key, value in pairs: if key in obj: raise PresetLinkError("Duplicate JSON key: " + key) obj[key] = value return obj def invalid_constant(value): raise PresetLinkError("Non-finite JSON number: " + value) return json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique, parse_constant=invalid_constant) def main(argv=None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--card", type=Path, required=True) parser.add_argument("--catalog", type=Path, default=HERE / "field-catalog.json") args = parser.parse_args(argv) try: result = build_preset_link(_read_json(args.card), _read_json(args.catalog)) except (ValueError, TypeError, KeyError, OSError) as exc: print(json.dumps({"valid": False, "error": str(exc)}, ensure_ascii=False), file=sys.stderr) return 2 print(json.dumps(result, ensure_ascii=False, indent=2, allow_nan=False)) return 0 if __name__ == "__main__": raise SystemExit(main())