Example bots
Two complete, small moving-average crossover bots, one in Python and one in TypeScript. They show the patterns every trading program needs. They are examples to learn from, not trading advice.
Downloads
Pythonma_cross_bot.pyhttpx and websockets. With
requirements.txt below.
TypeScriptma-cross-bot.tsNode.js 20+, fetch and ws. With package.json below.
Also: requirements.txt · package.json · openapi.yaml (to generate a client in your own language)
What they do
- Read
/meand refuse to run unless the key is for a demo account. - Every 15 seconds, read candles and compare a fast and a slow moving average of the closed bars.
- When the signal flips, close the bot's opposite positions and open one in the new direction, tagged with the bot's
client_tag. - Print order events from the stream as they happen.
Run them
Python
pip install -r requirements.txt
export JAFX_API_KEY='jafx_demo_…'
python ma_cross_bot.py --symbol EURUSD --timeframe M5 --volume 0.01TypeScript
npm install
export JAFX_API_KEY='jafx_demo_…'
npx tsx ma-cross-bot.ts EURUSD M5 0.01Patterns worth copying
- One idempotency key per intended action. The key is generated when the bot decides to trade, and every retry of that call reuses it. A dropped connection can never open a second position.
- Retry only what is safe to retry.
429,503and409 request_in_progresswait forRetry-Afterand try again; other errors are reported, not retried. - Follow, never resend. A
202answer (pendingoroutcome_unknown) is followed by polling the request until it is decided. - Respect the platform's brakes.
trading_haltedpauses the bot;daily_loss_limit_reachedstops it for the day. - Streams reconnect with backoff and stop on
4401(bad key) and4409(another copy running). - Tags keep strategies apart. The bot only ever touches positions carrying its own
client_tag, so you can run it next to manual trading.
Python
ma_cross_bot.py
"""
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())TypeScript
ma-cross-bot.ts
/**
* Moving-average crossover bot for the JAFX Trading API (demo accounts).
*
* npm install
* JAFX_API_KEY=jafx_demo_… npx tsx ma-cross-bot.ts EURUSD M5 0.01
*
* Shows: authentication, /me limits, candles, idempotent orders, 202 handling,
* trading_halted, and the orders stream. An example, not trading advice.
*/
import { randomUUID } from 'node:crypto'
import WebSocket from 'ws'
const BASE = process.env.JAFX_API_BASE ?? 'https://terminal.jafx.com/trading-api/v1'
const KEY = process.env.JAFX_API_KEY ?? ''
const TAG = 'ma-cross-ts'
const [symbol = 'EURUSD', timeframe = 'M5', volume = '0.01'] = process.argv.slice(2)
class ApiError extends Error {
constructor(readonly status: number, readonly code: string, detail: string) {
super(`${status} ${code}: ${detail}`)
}
}
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms))
async function call<T = any>(method: string, path: string, body?: unknown, idempotencyKey?: string): Promise<{ status: number; data: T }> {
const headers: Record<string, string> = { Authorization: `Bearer ${KEY}` }
if (body !== undefined) headers['Content-Type'] = 'application/json'
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey
for (let attempt = 0; attempt < 4; attempt++) {
let res: Response
try {
res = await fetch(`${BASE}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) })
} catch {
await sleep(1000 * (attempt + 1)) // safe to retry: writes reuse the same Idempotency-Key
continue
}
const data = res.status === 204 ? null : await res.json()
const retryable = res.status === 429 || res.status === 503 || (res.status === 409 && ['request_in_progress', 'order_busy'].includes(data?.code))
if (retryable) {
await sleep(Number(res.headers.get('retry-after') ?? '1') * 1000)
continue
}
if (!res.ok) throw new ApiError(res.status, data?.code ?? 'unknown', data?.detail ?? data?.title ?? '')
return { status: res.status, data }
}
throw new Error(`${method} ${path} kept failing`)
}
/** 202 = still deciding (or answer lost): poll the request, never resend under a new key. */
async function follow(account: number, result: any): Promise<any> {
while (result.status === 'pending' || result.status === 'outcome_unknown') {
await sleep(1000)
result = (await call(`GET`, `/accounts/${account}/requests/${result.request_id}`)).data
}
return result
}
function sma(values: number[], n: number): number | null {
return values.length >= n ? values.slice(-n).reduce((a, b) => a + b, 0) / n : null
}
function streamOrders(delayMs = 1000): void {
const ws = new WebSocket(`${BASE.replace(/^http/, 'ws')}/stream`, { headers: { Authorization: `Bearer ${KEY}` } })
ws.on('open', () => {
delayMs = 1000
ws.send(JSON.stringify({ op: 'subscribe', channels: ['orders'] }))
})
ws.on('message', raw => {
const f = JSON.parse(raw.toString())
if (f.ch === 'orders') console.log(`[stream] ${f.event}`, f.data)
})
ws.on('error', e => console.error('[stream] error:', e.message))
ws.on('close', code => {
if (code === 4401 || code === 4409) return console.error(`[stream] closed with ${code}; not reconnecting`)
setTimeout(() => streamOrders(Math.min(delayMs * 2, 30_000)), delayMs)
})
}
async function main(): Promise<void> {
if (!KEY) throw new Error('Set JAFX_API_KEY')
const me = (await call('GET', '/me')).data
if (me.mode !== 'demo') throw new Error('This example only runs on demo accounts')
const account: number = me.account
console.log(`key ${me.name} on account ${account}; limits`, me.constraints)
streamOrders()
let last: 'buy' | 'sell' | null = null
for (;;) {
try {
const candles = (await call('GET', `/candles/${symbol}?timeframe=${timeframe}&limit=40`)).data
const closes: number[] = candles.data.filter((c: any) => c.complete).map((c: any) => c.close)
const fast = sma(closes, 10)
const slow = sma(closes, 30)
if (fast !== null && slow !== null) {
const signal = fast > slow ? 'buy' : 'sell'
if (signal !== last) {
const positions = (await call('GET', `/accounts/${account}/positions?client_tag=${TAG}`)).data.data
for (const p of positions.filter((p: any) => p.side !== signal)) {
const r = await follow(account, (await call('POST', `/accounts/${account}/positions/${p.ticket}/close`, {}, randomUUID())).data)
console.log('closed', p.ticket, r.status)
}
if (!positions.some((p: any) => p.side === signal)) {
const order = { symbol, side: signal, type: 'market', volume, client_tag: TAG }
const r = await follow(account, (await call('POST', `/accounts/${account}/orders`, order, randomUUID())).data)
console.log(`${signal} ${volume} ${symbol}: ${r.status} ticket=${r.ticket}`)
}
last = signal
}
}
} catch (e) {
if (e instanceof ApiError && e.code === 'trading_halted') { console.log('Trading halted by JAFX; waiting'); await sleep(60_000) }
else if (e instanceof ApiError && e.code === 'daily_loss_limit_reached') { console.log('Daily loss limit reached; stopping'); return }
else console.error(e)
}
await sleep(15_000)
}
}
main().catch(e => { console.error(e); process.exit(1) })