""" Moving-average crossover bot for the JAFX Trading API (demo accounts). pip install -r requirements.txt export JAFX_API_KEY=jafx_demo_... # from the web terminal: Developers -> Create API key python ma_cross_bot.py --symbol EURUSD --timeframe M5 --volume 0.01 What it shows: authentication, reading /me limits, candles, idempotent orders, handling 202 pending / outcome_unknown, stopping on trading_halted, and the orders stream. It is an example, not trading advice. """ import argparse import asyncio import json import os import sys import uuid import httpx import websockets BASE = os.environ.get("JAFX_API_BASE", "https://terminal.jafx.com/trading-api/v1") KEY = os.environ.get("JAFX_API_KEY", "") TAG = "ma-cross" class ApiError(Exception): def __init__(self, status, problem): super().__init__(f"{status} {problem.get('code')}: {problem.get('detail') or problem.get('title')}") self.status = status self.code = problem.get("code") self.retry_after = problem.get("retry_after") async def call(client, method, path, body=None, idempotency_key=None): headers = {"Authorization": f"Bearer {KEY}"} if idempotency_key: headers["Idempotency-Key"] = idempotency_key for attempt in range(4): try: r = await client.request(method, f"{BASE}{path}", json=body, headers=headers, timeout=20) except httpx.TransportError: # Network blip: retrying is safe because writes carry the same Idempotency-Key. await asyncio.sleep(1 + attempt) continue if r.status_code in (429, 503) or (r.status_code == 409 and r.json().get("code") in ("request_in_progress", "order_busy")): await asyncio.sleep(float(r.headers.get("Retry-After", "1"))) continue data = r.json() if r.content else None if r.status_code >= 400: raise ApiError(r.status_code, data or {}) return r.status_code, data raise RuntimeError(f"{method} {path} kept failing") def sma(values, n): return sum(values[-n:]) / n if len(values) >= n else None async def follow_request(client, account, result): """A 202 means the broker is still deciding (or the answer was lost): poll, never resend.""" while result.get("status") in ("pending", "outcome_unknown"): await asyncio.sleep(1) _, result = await call(client, "GET", f"/accounts/{account}/requests/{result['request_id']}") return result async def stream_orders(key): url = BASE.replace("https://", "wss://").replace("http://", "ws://") + "/stream" delay = 1 while True: try: async with websockets.connect(url, additional_headers={"Authorization": f"Bearer {key}"}) as ws: delay = 1 await ws.send(json.dumps({"op": "subscribe", "channels": ["orders"]})) async for raw in ws: frame = json.loads(raw) if frame.get("ch") == "orders": print(f"[stream] {frame['event']}: {frame['data']}") except websockets.ConnectionClosed as e: code = e.rcvd.code if e.rcvd else None if code in (4401, 4409): print(f"[stream] closed with {code}; not reconnecting") return except OSError as e: print(f"[stream] {e}") await asyncio.sleep(delay) delay = min(delay * 2, 30) async def main(): p = argparse.ArgumentParser() p.add_argument("--symbol", default="EURUSD") p.add_argument("--timeframe", default="M5") p.add_argument("--volume", default="0.01") p.add_argument("--fast", type=int, default=10) p.add_argument("--slow", type=int, default=30) args = p.parse_args() if not KEY: sys.exit("Set JAFX_API_KEY") async with httpx.AsyncClient() as client: _, me = await call(client, "GET", "/me") account = me["account"] print(f"key {me['name']} on account {account} ({me['mode']}); limits: {me['constraints']}") if me["mode"] != "demo": sys.exit("This example only runs on demo accounts") asyncio.create_task(stream_orders(KEY)) last_signal = None while True: try: _, candles = await call(client, "GET", f"/candles/{args.symbol}?timeframe={args.timeframe}&limit={args.slow + 5}") closes = [c["close"] for c in candles["data"] if c["complete"]] fast, slow = sma(closes, args.fast), sma(closes, args.slow) if fast is None or slow is None: await asyncio.sleep(10) continue signal = "buy" if fast > slow else "sell" if signal != last_signal: _, positions = await call(client, "GET", f"/accounts/{account}/positions?client_tag={TAG}") for pos in positions["data"]: if pos["side"] != signal: # One idempotency key per intended action. _, r = await call(client, "POST", f"/accounts/{account}/positions/{pos['ticket']}/close", {}, str(uuid.uuid4())) print("closed", pos["ticket"], (await follow_request(client, account, r))["status"]) if not any(pos["side"] == signal for pos in positions["data"]): order = {"symbol": args.symbol, "side": signal, "type": "market", "volume": args.volume, "client_tag": TAG} status, r = await call(client, "POST", f"/accounts/{account}/orders", order, str(uuid.uuid4())) r = await follow_request(client, account, r) print(f"{signal} {args.volume} {args.symbol}: {r['status']} ticket={r.get('ticket')}") last_signal = signal except ApiError as e: if e.code == "trading_halted": print("Trading halted by JAFX; waiting a minute") await asyncio.sleep(60) elif e.code in ("daily_loss_limit_reached",): print("Daily loss limit reached; stopping for today") return else: print("API error:", e) await asyncio.sleep(15) if __name__ == "__main__": asyncio.run(main())