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_historyresponse were not verified against a live call while writing this. Print the raw payload once and adjustc["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:
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.