
My PC clock ran 16 seconds ahead of the exchange. Signature validation failed, a "helpful" fallback computed equity from a paper balance, and the bot declared a 58% drawdown. No orders were sent. Here is the chain of events and the two fixes.
I run a small, deliberately boring trading bot: daily bars, spot only, no leverage, about ¥24,000 of my own money on a Japanese exchange (GMO Coin). It wakes up once a day at 06:05, looks at yesterday's candle, and usually does nothing.
On day six it halted itself and reported a 58% drawdown. My balance had not moved by a single yen.
(The free lite backtester for this system, public API only, no key: GMO Coin Trend Lab, source on GitHub.)
Nothing was lost. No order was sent. But the bug is a good one, because every link in the chain looked reasonable on its own.
ERR-5009 Timestamp for this request is too fast.Because it was in dry-run, "flatten" sent nothing. The halt flag, however, was written to the real state file. The bot would have sat there, halted, until a human noticed.
"Fall back to dry-run if the key does not validate." Friendly on day one, when the likely failure is a user pasting the FX key into the crypto slot. Dangerous on day six, when the key is fine and the failure is transient.
"One state file." Simple. Until two modes with different balances write the same high-water mark.
"Halt at 30% drawdown." Correct, and it worked. The kill switch is not the bug. The bug is that a number from one world was compared against a number from another.
The exchange checks that your timestamp is close to its own. So ask it what time it is. Every HTTP response carries a Date header, and that is accurate enough for a tolerance measured in seconds.
import email.utils, time, requests
PUBLIC = "https://api.coin.z.com/public"
class GmoClient:
def __init__(self):
self.s = requests.Session()
self._offset = None # local clock minus server clock, in seconds
def clock_offset(self, refresh=False):
if self._offset is None or refresh:
try:
t0 = time.time()
r = self.s.get(PUBLIC + "/v1/status", timeout=15)
t1 = time.time()
srv = email.utils.parsedate_to_datetime(r.headers["Date"]).timestamp()
self._offset = (t0 + t1) / 2 - srv
except Exception:
self._offset = 0.0
return self._offset
def _timestamp_ms(self):
# the Date header has one-second resolution, so lean half a second early:
# "slightly slow" is tolerated, "too fast" is rejected
return str(int((time.time() - self.clock_offset() - 0.5) * 1000))
Two details matter. Take the midpoint of the request so latency does not bias the estimate. And lean slightly early, because this API rejects timestamps from the future more strictly than ones from the recent past.
After this change the bot does not care whether the OS clock is right.
c = GmoClient()
if not c.dry_run: # keys are present, so we intend to be live
try:
c.assets()
except GmoError as e:
log(f"key validation failed, aborting this run without touching state "
f"(clock offset {c.clock_offset():+.1f}s): {e}")
return # try again tomorrow
if c.dry_run:
STATE = ROOT / "state_dry.json" # paper trading lives in its own file
The rule I took away: if the system intended to be live and cannot prove it is live, it should do nothing, loudly. Not "degrade gracefully" into a different mode that shares storage with the real one.
The strategy itself is a 20-day breakout with a 50-day filter and a 2×ATR stop, risking 1% per trade. My backtest on the exchange's own daily data from 2018 to 2026 gives about the same return as volatility-targeted buy-and-hold with roughly half the drawdown, and close to zero in ranging years. Walk-forward out-of-sample Sharpe is 0.85. Its first real trade happened on day eight: 0.0076 ETH, with about ¥240 at risk.
Operational holes scare me more than strategy losses. This one cost nothing, which is the best price to learn at.
If you want to poke at the backtest, the lite version is a free download (public API only, no key needed): https://wataflow1.gumroad.com/l/trend-lab-free
Not investment advice.
eventsinyourcityOn Wednesday, September 30, Anthropic and Google Cloud are co-hosting a hands-on developer workshop...
opensourceFor decades, platforms like Claris FileMaker, Microsoft Access, and 4D enabled businesses to build...
aiThis video (the script, the voice timings, the source code, the storyboard, the briefs the subagents...
devchallengeWe are thrilled to announce the winners of DEV's Big Summer Bug Smash powered by Sentry! This...
gemmaGemma 4 E2B, E4B, 12B and a 26B-A4B fp8 build read by their label probabilities with vLLM on one TPU v6e chip, checked against the same read on an NVIDIA L4 and against Jev 1.13.0's published results. What fits one chip, how to read labels when vLLM on TPU returns only the top 32 log-probabilities, speed, cost, and why no 31B loads today.
aiCoding agents in your terminal can scaffold microservices in seconds. But what happens when your...
Workflows from the Neura Market marketplace related to this Perplexity resource