Skip to content

Building Your Own Harness

For readers going past prompt-and-cron. This page covers the four things that separate a script that works once from one you can leave running: computing your own indicators, keeping state, staying inside the rate budget, and scheduling.


Indicators from candles

There are no indicator tools on this server. e8_trade_history gives you OHLCV; the maths is yours.

That is worth doing in code rather than in a prompt. A model asked to compute a 14-period RSI across 120 candles will usually get close, occasionally get it wrong, and always cost more than the two lines of arithmetic it replaces. Compute in code, hand the result to the model to interpret.

import os, json, requests

ENDPOINT = "https://trade.e8markets.com/api/mcp"
HEADERS = {
    "Authorization": f"Bearer {os.environ['E8_API_KEY']}",
    "Content-Type": "application/json",
    "Accept": "application/json, text/event-stream",
}

def call(tool: str, **args):
    """Call one MCP tool, return its parsed text payload."""
    r = requests.post(ENDPOINT, headers=HEADERS, json={
        "jsonrpc": "2.0", "id": 1, "method": "tools/call",
        "params": {"name": tool, "arguments": args},
    }, timeout=30)
    r.raise_for_status()
    body = r.json()
    if "error" in body:
        raise RuntimeError(f"{tool}: {body['error']['message']}")
    return json.loads(body["result"]["content"][0]["text"])


def rsi(closes, period=14):
    gains = losses = 0.0
    for prev, cur in zip(closes[:period], closes[1:period + 1]):
        delta = cur - prev
        gains += max(delta, 0.0)
        losses += max(-delta, 0.0)
    avg_gain, avg_loss = gains / period, losses / period

    for prev, cur in zip(closes[period:-1], closes[period + 1:]):
        delta = cur - prev
        avg_gain = (avg_gain * (period - 1) + max(delta, 0.0)) / period
        avg_loss = (avg_loss * (period - 1) + max(-delta, 0.0)) / period

    if avg_loss == 0:
        return 100.0
    return 100.0 - 100.0 / (1.0 + avg_gain / avg_loss)


candles = call("e8_trade_history", symbol="EURUSD", resolution="60", countBack=120)
closes = [c["close"] for c in candles["candles"]]
print(f"EURUSD 1H RSI(14): {rsi(closes):.1f}")

ATR and moving averages follow the same shape. Once you have them, the interesting call is the one that hands a model computed values and asks for a reading — that plays to what it is good at.

The exact key names inside the e8_trade_history response were not verified against a live call while writing this. Print the raw payload once and adjust c["close"] if it differs.

Your numbers will not match the E8 chat assistant's. It runs its own implementation with its own defaults. Both can be right. Do not spend an afternoon reconciling them.


Keeping state

Anything that runs on a schedule needs to remember the last run — otherwise recipe 3 rewrites the same journal entry daily and recipe 6 forgets its trip wire has fired.

A JSON file is enough:

{
  "last_run": "2026-08-04T07:00:00Z",
  "last_order_id": "ord_...",
  "consecutive_losses": 0,
  "disabled": false
}

Two rules that matter more than the format:

Write it after the side effect, not before. If you record "order placed" and then the call fails, your state is lying. If you place the order and then crash before writing, you can reconcile from e8_orders_list — recoverable.

Reconcile on start, don't trust the file. Query e8_orders_list for orders since last_run and compare. The server is the truth; your file is a cache.


Idempotency

clientOrderId is your own identifier attached to an order. Set it deterministically:

auto-20260804-0700-EURUSD

Now a retry after a timeout is checkable — query e8_orders_list, look for that id, and only place if it is absent. Without it, "did my order go through?" has no reliable answer, and the safe assumption after a timeout is that it did.


Rate budgeting

Your ceiling is 60 calls per minute and 10,000 per day on an OAuth connection; API keys carry their own per-key limits.

Pattern Cost
Risk watch, every 15 min, market hours ~100/day
Morning brief, once daily, 5 symbols ~10/day
Screener over 40 symbols, hourly ~1,000/day
Screener over 40 symbols, every 5 min ~12,000/day ✗

Three ways to stay under:

Cache candles. Hourly candles do not change within the hour. Fetch once, reuse.

Ask for less. countBack: 120 when you need 120, not 500 because it was there. sections on e8_analytics_account instead of the full 15 KB payload.

Back off properly. A 429 carries Retry-After in seconds. Honour it. Retrying immediately in a loop turns a brief throttle into a sustained one:

import time

def call_with_retry(tool, attempts=4, **args):
    for i in range(attempts):
        try:
            return call(tool, **args)
        except requests.HTTPError as e:
            if e.response.status_code != 429 or i == attempts - 1:
                raise
            time.sleep(int(e.response.headers.get("Retry-After", 2 ** i)))

Scheduling

cron for anything on a clock. Note that cron runs with almost no environment — set your key explicitly and use absolute paths:

E8_API_KEY=e8_...
0 7 * * 1-5   /usr/local/bin/e8-morning-brief.sh >> /var/log/e8.log 2>&1
*/15 9-17 * * 1-5  /usr/local/bin/e8-risk-watch.sh >> /var/log/e8.log 2>&1

Restricting the risk watch to 9-17 on weekdays cuts its call volume by two thirds and loses nothing — the market is closed the rest of the time.

An agent loop when the next step depends on the last. Claude Code's /loop keeps context between iterations, which cron cannot. Better for exploratory monitoring, worse for anything that must run unattended for weeks.

Event-driven if you already have infrastructure. Nothing here needs to be polled from your laptop — the endpoint is plain HTTPS and works fine from a server, a container, or a workflow tool.


Failure modes to handle

Situation What to do
Market closed Check isMarketOpen before order calls; exit cleanly, do not retry
Multiple accounts Always pass accountId (see the rule)
Rate limited Honour Retry-After
Credential revoked Fail loudly. A silent monitoring loop that stopped working is worse than no loop
Timeout mid-order Reconcile via clientOrderId before retrying

The one worth dwelling on is the fourth. A risk watch that has been silently failing for a week feels exactly like a risk watch reporting good news. Make your scripts alert on failure to run, not just on threshold breaches — a daily heartbeat you would notice missing.


Next: Safety & Limits.