Guides
Guides
Dwellir GuidesAgent CookbookWhen to use IP whitelistingeth_getLogs LimitsBackfill the Full Hyperliquid OHLCV ArchiveExport Hyperliquid OHLCV Candles to CSV or ParquetBuild a Hyperliquid Trading App with Builder CodesStream Order Book Data with the Dwellir WebSocketBuild a Real-Time Base Flashblocks Visualizer with Next.jsHow to Build a Polymarket Copy-Trading BotHyperliquid Funding Rates: Hourly Formula and APIHow to Track Hyperliquid Whales in Real TimeHow to Find the Wallet Behind a Hyperliquid OrderTrade post-mortemPolymarket API: Endpoints, API Keys and Trading GuideKalshi API: Keys, Endpoints, WebSocket and PythonBuild a Real-Time Robinhood Chain Whale Tracker in PythonHow to Build a Copy Trading Bot on Arcus (Robinhood Chain)grpcurl
Hyperliquid Trade Post-Mortem: Find Your Trading Mistakes in Python cover

Hyperliquid Trade Post-Mortem: Find Your Trading Mistakes in Python

Line up every closed Hyperliquid trade against 1s and 1m candles, then test stops, targets and no-adding rules on your own fills. Notebook and CLI in Python.

Published November 4, 2026

hyperliquid logohyperliquid logo
LanguagePython 3.10+
NetworkHyperliquid
DataHyperliquid Index fills and candles

Hyperliquid puts every fill you have ever made on-chain, with the position you held before it, the realized PnL and the fee. Pair that with second-by-second prices and you can replay any trade you closed: where it went for you, where it went against you, what happened after you left, and what a stop or a target would have done.

In this guide we build a trade post-mortem in Python. It rebuilds a wallet's closed perp trades from indexed fills and lines each one up against 1s and 1m candles. It then asks six questions of every trade: should you have skipped it, stopped out, taken profit, held, closed and reopened, or not added? Finally it replays simple rules across all trades. We ran it on an active wallet picked at random from BTC fills. In a 30-day lookback, with trades from 18 October to 3 November 2026, that wallet lost $8,899 across 64 trades. The notebook pins $11,900 of losses on 32 trades that never moved more than 0.3% in its favour.

The code is in dwellir-examples/hyperliquid-trade-postmortem: a notebook, a command-line report and four small modules.

What You Will Learn

  • Page a wallet's fills from userFillsByTime and rebuild round-trip trades from startPosition
  • Load 1s candles for the entry and exit minutes and 1m candles for the rest of each trade
  • Measure maximum favorable and adverse excursion (MFE and MAE), capture and post-exit moves per trade
  • Replay your own entries and exits under stop, target, trailing, time and no-adding rules
  • Check whether a rule that wins on your earlier trades still helps on your later ones

Prerequisites

  • Python 3.10 or newer. Run python3 --version to check.
  • A Dwellir API key. Create a free account and copy the key from the dashboard. The Hyperliquid Index needs no add-on.
  • A Hyperliquid wallet address with closed perp trades since 27 July 2025, which is where the Index history starts.
  • Comfort with pandas DataFrames.

Clone the example and install the four dependencies:

Bash
git clone https://github.com/dwellir-public/dwellir-examples
cd dwellir-examples/examples/hyperliquid-trade-postmortem
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
export DWELLIR_API_KEY=your-api-key

Both endpoints we use are billed per object returned: one API credit per fill and one per candle. Our 30-day run on a wallet with 1,118 fills used about 44,000 credits and took 54 seconds. Candles are cached on disk, so a second run of the same window costs only the fills.

Step 1: Fetch the wallet's fills

userFillsByTime returns up to 2,000 fills per request in time order. The /info route returns a bare array, so we build the next page's cursor from the last fill as time_txIndex. From hl_index.py:

Python
def user_fills(user, start_ms, end_ms, coin=None):
    """All fills for `user` in [start_ms, end_ms), oldest first, as a DataFrame."""
    body = {"type": "userFillsByTime", "user": user.lower(), "startTime": int(start_ms), "endTime": int(end_ms), "limit": FILL_PAGE}
    if coin:
        body["coin"] = coin
    rows = []
    while True:
        page = _request(_url("/info"), body)
        rows += page
        if len(page) < FILL_PAGE:
            break
        body.pop("startTime", None)
        body["cursor"] = f"{page[-1]['time']}_{page[-1]['txIndex']}"
    df = pd.DataFrame(rows)
    if df.empty:
        return df
    for col in ["px", "sz", "startPosition", "closedPnl", "fee"]:
        df[col] = pd.to_numeric(df[col])
    # tid is a random trade id. Fills in one block are ordered by txIndex.
    return df.sort_values(["time", "txIndex"], kind="stable").reset_index(drop=True)

_request() posts JSON to https://api-hyperliquid-index.n.dwellir.com/$DWELLIR_API_KEY/info and retries on 429, 500, 502, 503 and 504 responses and on network timeouts.

The sort on the last line matters. A market order that sweeps the book produces many fills with the same timestamp. Our first version sorted them by tid, which is a random trade id, and the running position fell out of step on 608 of 702 BTC fills. Sorting by txIndex, the fill's position inside the block, brought the mismatches to zero.

Try it in a Python shell:

Python
import time, hl_index
end = int(time.time() * 1000)
fills = hl_index.user_fills("0xYourWallet", end - 30 * 86_400_000, end)
print(len(fills))
fills[["coin", "side", "px", "sz", "startPosition", "dir", "closedPnl", "fee", "txIndex"]].head(4)

You should see output like this:

Text
1118
  coin side      px      sz  startPosition        dir  closedPnl       fee  txIndex
0  ETH    B  2402.5  0.0100         0.0000  Open Long        0.0  0.010811       18
1  ETH    B  2402.5  0.0293         0.0100  Open Long        0.0  0.031676       20
2  ETH    B  2402.5  1.0047         0.0393  Open Long        0.0  1.086206       22
3  ETH    B  2402.5  0.9247         1.0440  Open Long        0.0  0.999716       24

One ETH market buy produced those four fills in the same block, and each fill's startPosition is the previous fill's position plus its size.

Step 2: Rebuild round-trip trades

A trade starts when a position leaves zero and ends when it returns to zero. Fills that grow the position are entries and fills that shrink it are exits. A fill that crosses zero, which Hyperliquid labels Long > Short or Short > Long, closes one trade and opens the next at the same price. The core loop in trades.py, abridged (the full version also flags liquidation fills when the API marks them):

Python
for f in rows.itertuples(index=False):
    start = f.startPosition
    if pos is None or abs(pos - start) > EPS * max(1.0, abs(start)):
        # First fill for this coin, or our running position drifted.
        # Resync on the exchange's number and drop the trade we cannot account for.
        pos, trade = start, None
    qty = f.sz if f.side == "B" else -f.sz

    if abs(pos) < EPS:                                  # flat: a new trade opens
        trade = Trade(coin, 1 if qty > 0 else -1)
        trade.entries.append((f.time, f.px, abs(qty), f.fee))
    elif (pos > 0) == (qty > 0):                        # same direction: an entry
        if trade:
            trade.entries.append((f.time, f.px, abs(qty), f.fee))
    else:                                               # opposite direction: an exit
        closing = min(abs(qty), abs(pos))
        share = closing / abs(qty)
        if trade:
            trade.exits.append((f.time, f.px, closing, f.fee * share, f.closedPnl))
        if abs(qty) > abs(pos) + EPS:                   # flip: the rest opens a new trade
            if trade:
                trades.append(trade)
            trade = Trade(coin, 1 if qty > 0 else -1)
            trade.entries.append((f.time, f.px, abs(qty) - closing, f.fee * (1 - share)))
        elif abs(pos + qty) < EPS * max(1.0, abs(pos)):  # back to flat: the trade is done
            if trade:
                trades.append(trade)
            trade = None
    pos += qty

Because startPosition is the exchange's own number, any trade that was already open when the window started gets dropped, as does any trade we cannot follow end to end. Spot (@123) and HIP-4 outcome (#123) markets are skipped. HIP-3 markets such as xyz:SP500 are perps and stay in.

Each Trade exposes avg_entry, avg_exit, gross_pnl (the sum of closedPnl), fees, net_pnl and adds, which are entries placed more than a minute after the first. As a check, we recomputed every trade's PnL from its average entry and exit prices. It matched Hyperliquid's closedPnl to within $0.46 per trade.

Python
import trades as tr
T = tr.build_trades(fills)
tr.trades_frame(T)[["coin", "side", "open", "hours", "notional", "entries", "adds", "exits", "net_pnl"]].head(3)
Text
  coin   side                       open  hours   notional  entries  adds  exits   net_pnl
0  ETH   long  2026-10-18 00:45:03+00:00  13.51  123333.28       11     6     15  -1323.44
1  BTC  short  2026-10-18 14:16:05+00:00   0.15  160856.52       16     0     11   -543.18
2  BTC   long  2026-10-18 16:21:03+00:00   0.58   79571.91        2     0      9   -119.52

Step 3: Load the price path around each trade

The candle range endpoint returns up to 5,000 candles per page for one market. It requires start and end aligned to the interval and rejects ranges that include the bucket still in progress, so candles() rounds the window outward, clips it to the last finished bucket and caches the result:

Python
def candles(market, interval, start_ms, end_ms):
    step = INTERVAL_MS[interval]
    start = start_ms // step * step
    last_done = (int(time.time() * 1000) // step - 1) * step
    end = min(-(-end_ms // step) * step, last_done)
    if end <= start:
        return pd.DataFrame(columns=CANDLE_COLS)
    path = CACHE / f"{market.replace(':', '_').replace('@', 'at')}-{interval}-{start}-{end}.csv.gz"
    if path.exists():
        return pd.read_csv(path)
    df = _fetch_candles(market, interval, start, end)
    CACHE.mkdir(parents=True, exist_ok=True)
    df.to_csv(path, index=False)
    return df

Candles are sparse: a second or minute with no trades has no candle, not a zero candle.

A trade that opened at 00:45:03 should not see the low printed at 00:45:01. So Prices.path() in analysis.py uses 1s candles for the entry minute and the exit minute, and 1m candles everywhere else, out to 24 hours after the exit:

Python
def path(self, trade, until=None):
    until = until or trade.close_time + self.after
    m = self.minute[trade.coin]
    entry_min = trade.open_time // MIN * MIN
    exit_min = trade.close_time // MIN * MIN
    open_sec = trade.open_time // 1000 * 1000
    edge = [self.seconds(trade.coin, entry_min)]
    if exit_min != entry_min:
        edge.append(self.seconds(trade.coin, exit_min))
    sec = pd.concat(edge)
    sec = sec[(sec.index >= open_sec) & (sec.index < until)]
    mins = m[(m.index > entry_min) & (m.index != exit_min) & (m.index < until)]
    bars = pd.concat([sec.assign(t_end=sec.index + 999), mins.assign(t_end=mins.index + MIN - 1)])
    return bars[["o", "h", "l", "c", "t_end"]].sort_index()

Prices.__init__ merges overlapping trade windows per coin before fetching 1m candles, so 20 short BTC trades in one afternoon share one candle range instead of fetching 20 overlapping ones. For our 64 trades that came to 37,931 1m candles and 4,834 1s candles.

Check the path for the first trade:

Python
import analysis as an
P = an.Prices(T)
len(P.path(T[0]))
Text
2290

That is 13.5 hours of 1m bars plus 24 hours after the exit, with 1s bars in the entry and exit minutes. Minutes with no trades have no bar.

Step 4: Measure what each trade lived through

From the path, excursions() measures four things per trade, signed so that positive is good for the trade's direction:

  • MFE, the best move in your favour while the trade was open, in % of the average entry
  • MAE, the worst move against you while it was open, in % of the average entry
  • capture, the realized move as a fraction of the MFE
  • post-exit moves, where price was 1, 4 and 24 hours after the exit, in % of the average exit

The core of excursions(), abridged:

Python
held = bars[bars.index <= trade.close_time]
after = bars[bars.index > trade.close_time]
s, entry, exit_ = trade.side, trade.avg_entry, trade.avg_exit
best = held["h"].max() if s > 0 else held["l"].min()
worst = held["l"].min() if s > 0 else held["h"].max()
mfe = s * (best - entry) / entry
mae = s * (worst - entry) / entry
realized = s * (exit_ - entry) / entry

For the 13.5-hour ETH long from step 2:

Python
an.excursions(T[0], P)
Text
{'mfe_pct': 0.635, 'mae_pct': -1.296, 'realized_pct': -0.984, 'capture': -1.55,
 'mins_to_mfe': 182.9, 'after_1h_close_pct': -0.499, 'after_4h_close_pct': -1.156, ...}

Its best price, three hours in, was 0.64% above the final average entry. Its worst was 1.30% below, and it closed 0.98% down. Four hours after the exit, ETH was another 1.16% lower, so holding would have lost more.

Plotting MFE against MAE for every trade shows the wallet's main problem straight away:

Scatter of 64 Hyperliquid trades, best move in favour (MFE) on the y axis against worst move against (MAE) on the x axis. A cluster of red trades labelled 'better not to open it' sits along the bottom with MFE under 0.3%. Trades labelled fine or take-profit sit higher, up to 5.6% MFE, with MAE mostly under 0.7%. Four orange 'should have stopped out' trades sit to the right with MAE from 1.4% to 5%.

Winners had a median MFE of 0.78%. Losers had a median MFE of 0.13%, so most losing trades never went the wallet's way at all.

Step 5: Replay the trade under a rule

simulate() walks a trade bar by bar. Your real entries and exits happen as they did, and the rule only steps in when it fires. Real exits close the same fraction of the simulated position as they did of the real one, so a rule that never fires returns your real PnL. When a rule fires, it closes the rest of the position at the rule's price. A rule is a small dataclass:

Python
@dataclass
class Rule:
    name: str
    stop: float = None        # fraction against the current average entry, e.g. 0.02
    take: float = None        # fraction in favour of the current average entry
    trail: float = None       # fraction back from the best price since entry
    max_hours: float = None   # close everything this long after the first entry
    no_adds: bool = False     # ignore entries placed more than a minute after the first
    reenter: float = None     # after a stop, buy back once price bounces this far off the low

Abridged from simulate(), the part that decides whether a stop or target fires inside a bar:

Python
stops = []
if rule.stop:
    stops.append(avg * (1 - s * rule.stop))
if rule.trail:
    stops.append(best * (1 - s * rule.trail))
stops = [px for px in stops if (s > 0 and lo <= px) or (s < 0 and hi >= px)]
stop_px = (max(stops) if s > 0 else min(stops)) if stops else None
tp = avg * (1 + s * rule.take) if rule.take else None
hit_tp = tp is not None and ((s > 0 and hi >= tp) or (s < 0 and lo <= tp))
if stop_px is not None and hit_tp:
    if _stop_first(prices, trade.coin, t, bar, stop_px, tp, s):
        hit_tp = False
    else:
        stop_px = None

if stop_px is not None:
    # a bar that opens through the stop fills at the open
    px = (min(stop_px, bar["o"]) if s > 0 else max(stop_px, bar["o"])) * (1 - s * slippage)
    pnl += s * (px - avg) * pos - fee_rate * px * pos

Four details keep the replay honest:

  • Stops and targets follow the running average entry, which moves every time you add.
  • Synthetic fills pay the wallet's own average fee rate (3.92 bps here), and stops and time exits pay another 5 bps of slippage.
  • If a stop and a target both fall inside one 1m bar, _stop_first() loads that minute's 1s candles to see which traded first.
  • We checked three stopped-out trades by hand against the raw candles. The simulated PnL matched to the cent.
Python
rate = an.taker_rate(T)
for rule in [an.Rule("stop 0.46%", stop=0.0046), an.Rule("take 0.78%", take=0.0078), an.Rule("no adds", no_adds=True)]:
    print(rule.name, round(an.simulate(T[0], P, rule, rate), 2))
Text
stop 0.46% -652.65
take 0.78% 496.85
no adds -292.35

The ETH long lost $1,323. A 0.46% stop would have halved the loss, and a 0.78% target would have turned it into a $497 win. The chart shows why:

ETH long on 18 October 2026 with 11 entries, six of them adds, between 00:45 and 03:50, and 15 exits, net -$1,323. The target line at 0.78% above the running average entry is touched near 03:50, just as the last and highest entry is placed at about 2,425. The position is then held while ETH falls to about 2,385 before the final exit at 14:15.

Price touched the target just before 03:50, and that is when the wallet made its largest add, at the top. The average entry jumped, the target moved out of reach, and the position rode ETH down for the next ten hours.

Step 6: Ask the six questions of every trade

diagnose() prices each alternative in dollars against what really happened:

Python
def diagnose(trade, prices, fee_rate, stop=0.02, take=0.03, hold_hours=4, bounce=0.01):
    actual = trade.net_pnl
    out = {"skip": -actual}
    out["stop"] = simulate(trade, prices, Rule("s", stop=stop), fee_rate) - actual
    out["take"] = simulate(trade, prices, Rule("t", take=take), fee_rate) - actual
    out["reenter"] = simulate(trade, prices, Rule("r", stop=stop, reenter=bounce), fee_rate) - actual
    out["no_adds"] = (simulate(trade, prices, Rule("n", no_adds=True), fee_rate) - actual) if trade.adds else 0.0
    # hold: exit `hold_hours` after the real exit, at that bar's close
    ...
    return out

Fixed levels such as a 2% stop rarely fire for a wallet whose trades move a fraction of a percent, so levels() sizes the stop and target from the wallet itself. The stop goes where 80% of winning trades never went further against you. The target is the median best move on winners. Both are clamped to 0.2% to 5%.

For this wallet, 80% of winners never went more than 0.46% against the entry, so the stop is 0.46%. The target came out at 0.78%.

label() then gives each trade the one lesson that would have saved the most. analyse() runs excursions(), levels(), diagnose() and label() over every trade, and lessons() sums the result:

Python
df = an.analyse(T, P)
print(round(df.attrs["stop"], 4), round(df.attrs["take"], 4))
an.lessons(df)[["trades", "net_pnl", "saving"]].round(0)
Text
0.0046 0.0078
        trades  net_pnl   saving
lesson
skip        32 -11900.0  11900.0
take         5  -1254.0   3934.0
stop         4  -3653.0   2232.0
fine        23   7908.0      NaN

The lessons:

LessonWhen it applies
Better not to open itThe trade lost, and its MFE was smaller than its fees plus 0.2%
Should have stopped outA stop at the wallet's stop size saves the most
Should have taken profitA target at the wallet's target size saves the most
Should have kept holdingPrice 4 hours after the exit was more than 0.5% further your way, and further than the trade itself made
Better to close and reopenStopping out, then buying back after a half-stop bounce off the low, saves the most
Better not to addThe same exits without the later entries save the most
FineNo alternative is worth more than 0.1% of the trade's notional

The "kept holding" test is strict on purpose. For a single trade, where price is four hours after the exit is mostly noise, so the label needs a large move. exit_timing() tests early exits across all trades instead (step 8).

Bar chart of what each fix would have added for the sample wallet: better not to open it, 32 trades, $11,900; should have taken profit, 5 trades, $3,934; should have stopped out, 4 trades, $2,232.

32 of the 64 trades lost money without ever moving 0.3% the wallet's way. They lost $11,900 between them, $2,542 of it in fees. A 0.46% stop would have cut those losses by $2,150 in total, but it fires on only 7 of the 32 trades. The rest lose a little at a time, inside any reasonable stop. The fix for these trades is at the entry. The 23 trades labelled "fine" made $7,908.

Here is one of them, an ETH long that peaked 0.02% above its entry, went as far as 1.79% against it, and closed 1.57% down:

ETH long opened on 22 October 2026 that never trades meaningfully above its entry: best move 0.02%, worst move -1.79%, net -$1,208. Labelled 'better not to open it'.

The opposite case, a ZEC long held 3.9 hours while it went almost 5% against the entry, is the clearest stop-loss lesson in the set. A 0.46% stop would have saved $966 of its $1,152 loss:

ZEC long on 29 October 2026, a single entry near 1,355 and a single exit near 1,305 after 3.9 hours, net -$1,152, MFE 0.43%, MAE -4.96%. The 0.46% stop line sits just under the entry and is crossed within the first ten minutes.

Step 7: Test rules on earlier and later trades

A rule that looks best on the trades you just reviewed is fitted to them. test_rules() replays every rule on every trade and scores the first and second half of the trades separately:

Python
def test_rules(trades, prices, rules, split=0.5):
    rate = taker_rate(trades)
    cut = trades[int(len(trades) * split)].open_time if trades else 0
    early = np.array([t.open_time < cut for t in trades])
    rows = []
    for rule in [ACTUAL] + list(rules):
        pnl = np.array([simulate(t, prices, rule, rate) for t in trades])
        rows.append({
            "rule": rule.name,
            "net_pnl": pnl.sum(),
            "first_half": pnl[early].sum(),
            "second_half": pnl[~early].sum(),
            "win_rate": (pnl > 0).mean(),
            "worst_trade": pnl.min(),
        })
    out = pd.DataFrame(rows).set_index("rule")
    out["vs_actual"] = out["net_pnl"] - out.loc["actual", "net_pnl"]
    return out

rules_for(stop, take) builds the default set around the wallet's own levels: half, one and two stop sizes (0.23%, 0.46% and 0.92% here), one and two target sizes, stop-and-target pairs, a trailing stop, 1-hour and 4-hour time stops, no adds, no adds with a stop, and stop-then-re-enter.

Paired bars for 13 rules showing the change against actual PnL on the earlier half and the later half of trades. Most rules add $1,700 to $3,400 on the earlier half and lose $800 to $2,600 on the later half. Stop 0.92% is the only rule that helps on both halves, adding $592 early and $1,232 late. Max 1h adds $3,444 early and loses $2,493 late. Stop 0.46% with re-entry on a 0.23% bounce loses on both halves.

Almost every rule helps on the earlier trades and hurts on the later ones. The 0.46% stop adds $2,192 on the earlier half and loses $1,615 on the later half. The one rule that helps on both is the wide 0.92% stop: +$592 early, +$1,232 late, +$1,825 overall. It rarely fires, and when it does it cuts the few large losers such as the ZEC trade. No adds stays close to flat on the later half (-$295). The stop-and-re-enter rule churns in and out of chop, paying fees each time, and loses $2,962.

pick_and_check() does the honest version for you: choose on the first half, read the second:

Text
Best rule on the earlier half: max 1h ($3,444). On the later half it did not help (-$2,493).

Picking on the earlier trades chose a 1-hour time stop, and it gave back $2,493 on the later ones. The wide stop that held up on both halves is worth trying, but it recovers $1,825 of an $8,899 loss. The bigger lesson for this wallet is the one from step 6: 32 trades were wrong at the entry, and no exit rule recovers most of that.

Step 8: Putting it all together

postmortem.py runs every step and prints the report:

Bash
python postmortem.py 0xYourWallet --days 30

The output for our sample wallet, trimmed to the summary, the lessons and the exit timing:

Text
64 closed trades from 1118 fills, 2026-10-18 to 2026-11-03
Net -$8,899 = gross -$4,037 - fees $4,862 (funding not included)
Won 19 of 64 (30%); average win $527, average loss -$420
Best move in your favour, median: winners 0.78%, losers 0.13%
Stop and target sized from your trades: stop 0.46%, take 0.78%

Where the money went (one lesson per trade, saving = what that fix would have added):
        trades   net_pnl   saving                        meaning
lesson
skip        32  -$11,900  $11,900          Better not to open it
take         5   -$1,254   $3,934       Should have taken profit
stop         4   -$3,653   $2,232        Should have stopped out
fine        23    $7,908           No fix worth 0.1% of notional

Do you exit early? Price move after your exit, in your direction:
       trades  mean_pct  median_pct  share_in_your_favour  t_stat
after
1h         64    -0.104      -0.006                 0.469  -1.519
4h         64    -0.451      -0.112                 0.438  -2.637
24h        64    -0.145      -0.038                 0.484  -0.474

The full report goes on to print the 14-row rules table from step 7 and the pick-and-check line.

The exit-timing table answers "should I have held longer?" across all trades at once. Four hours after an exit, price had moved 0.45% against this wallet's direction on average, with a t-stat of -2.6. This wallet does not exit too early. Its trades tend to point the wrong way.

Fees are the other line to read: $4,862, more than the gross loss of $4,037. At 3.9 bps a side, a round trip costs 0.078% of notional. That is a tenth of this wallet's median winning move (0.78%), and more than half of its median losing trade's best move (0.13%).

For charts, open the notebook. It calls the same run() and adds the scatter, the lessons bar, close-ups of the three worst trades and the rules chart:

Bash
WALLET=0xYourWallet jupyter notebook postmortem.ipynb

Pass --start and --end for a fixed window, --stop and --take to use your own levels, and --csv trades.csv to keep the per-trade table.

Going to production

Funding. closedPnl excludes funding, and so does every counterfactual, so comparisons are like for like. On positions held for days, add funding before you draw conclusions. The funding rates guide shows how to price it.

Bar resolution. Between the entry and exit minutes the replay uses 1m bars, so it assumes your real fills inside a minute happened before that minute's high or low. For scalps measured in seconds, switch the middle of the path to 1s candles. That costs up to 60 times the candle credits, so cache aggressively.

Liquidations. The fills we pulled from userFillsByTime carried no liquidation field, so a liquidation is scored as an ordinary exit. build_trades() flags a trade as liquidated when the field is present. To be sure, join liquidationFillsByTime on tid.

Sample size. Splitting 64 trades in half leaves 32 trades per side. Treat any rule's gain on the later half as a hint until it holds over a few months of trades. Use --start to review more history.

API key handling. The key travels in the URL path, so keep it out of notebooks you share. Read it from DWELLIR_API_KEY as the example does, and clear notebook outputs before committing.

Next steps

  • Feed the per-trade CSV into your journal and track how the share of "better not to open it" trades changes month by month.
  • Test entry filters, not just exits. The trades labelled "better not to open it" are the ones to study: the time of day, the coin, the funding rate or the order book state when you opened them.
  • Replay the market rather than your own fills. The Hyperliquid historical data for backtesting post covers which datasets suit a full strategy backtest, and machine learning on Hyperliquid data shows how to score forecasts without lookahead.

Everything in this guide ran on Dwellir's Hyperliquid Index, with fills and 1s/1m candles back to 27 July 2025 on one API key. To run it on your own wallet, create a free account.

Frequently Asked Questions

How do I analyze my closed Hyperliquid trades?

Fetch your fills with userFillsByTime from Dwellir's Hyperliquid Index, group them into round trips using each fill's startPosition, then load 1s and 1m candles from the candle range endpoint for each trade's lifetime. From that price path you can measure the best and worst move while the trade was open (MFE and MAE), what price did after you exited, and how a stop, target or no-adding rule would have changed the result.

What API gives Hyperliquid fills and minute-by-minute prices?

Dwellir's Hyperliquid Index serves both on one host with one API key. userFillsByTime returns a wallet's fills (2,000 per page, back to 27 July 2025), and /v1/candles/range returns 1s, 1m or 5m OHLCV candles for any market, including HIP-3 markets. Both are billed per object returned.

How do I find the right stop loss for my trading?

Measure how far your winning trades went against you before they worked. If 80% of your winners never moved more than 0.46% against you, a stop near 0.46% cuts losers while touching only one winner in five. Then replay that stop on your earlier trades and check it on your later ones, because a level picked on the same trades it is scored on looks better than it is.

Does the analysis include funding payments?

No. Hyperliquid's closedPnl excludes funding, and so does every counterfactual in the notebook, so the comparisons are like for like. A trade held through many funding hours had a different real result.

How to Find the Wallet Behind a Hyperliquid Order

Turn a Hyperliquid transaction hash or order ID into the wallet that placed it. A tested Python script using txDetails, orderStatus, and indexed fills.

Polymarket API: Endpoints, API Keys and Trading Guide

The Polymarket API is three HTTP services: Gamma lists markets, CLOB serves order books and takes orders, Data returns positions. Reading needs no API key.

On this page

What You Will LearnPrerequisitesStep 1: Fetch the wallet's fillsStep 2: Rebuild round-trip tradesStep 3: Load the price path around each tradeStep 4: Measure what each trade lived throughStep 5: Replay the trade under a ruleStep 6: Ask the six questions of every tradeStep 7: Test rules on earlier and later tradesStep 8: Putting it all togetherGoing to productionNext steps