Performance Metrics Beyond Sharpe for HFT Strategies
Calmar, Sortino, profit factor, fill ratio, and adverse-selection rate give you a far more honest picture of HFT strategy health than Sharpe alone. Here is how each metric is calculated, what it actually tells you, and a Python dashboard template that derives all of them directly from exchange trade logs.
The Sharpe ratio made sense when the limiting factor was monthly return data and a spreadsheet. High-frequency strategies generate thousands of fills per day, and the distribution of those P&L ticks is almost never Gaussian — it is fat-tailed, autocorrelated, and heavily contaminated by market-microstructure effects. Applying Sharpe to that data is not wrong so much as it is uninformative. The five metrics below are the ones we actually use when deciding whether a strategy running on our bot infrastructure earns more capital allocation or gets shut down.
Calmar Ratio: Sizing Risk by Drawdown, Not Variance
Calmar = annualised return / maximum drawdown (absolute value).
For an HFT book, max drawdown is the number that matters to the risk desk. A strategy with annualised return of 180 % and a 6 % max drawdown has a Calmar of 30 — excellent. The same return with a 40 % drawdown gives you 4.5, which means you are one bad session away from a margin call. Sharpe can look identical in both cases if the variance of the losing ticks is low.
One practical note: measure drawdown on equity-curve timestamps, not on daily closes. An HFT book that recovers intraday will show a flattering daily drawdown but a brutal intraday one. Use tick-level or at minimum minute-level P&L.
Sortino Ratio: Penalising Only the Bad Volatility
Sortino = (annualised return − target return) / downside deviation.
Downside deviation is the standard deviation of returns that fall below your target (often zero or the risk-free rate). The logic is simple: a strategy that has a lot of upside variance is not risky, it is just profitable in an uneven way. Sharpe penalises both tails equally. Sortino does not.
In practice, a Sortino above 3 on daily P&L is the threshold we use to feel comfortable running a strategy through a volatile regime. Below 2, we investigate whether the downside tails are structural (adverse selection, stale quotes getting hit) or random noise. This distinction drives the next two metrics.
Profit Factor: The Clearest Signal for Execution Quality
Profit factor = gross profit / gross loss.
This one is embarrassingly simple and almost universally underused. A profit factor of 1.5 means you make $1.50 for every $1.00 you lose. Below 1.0 and the strategy is net negative by definition. The useful range for a sustainable HFT strategy is 1.3 to 2.5 — above 2.5 on live data you should be suspicious that you are overfitting or that the sample is too short.
What profit factor catches that Sortino misses: strategies that have many small winners and rare catastrophic losers can look fine on Sortino until the catastrophe arrives. Profit factor degrades earlier because each large loser moves the denominator hard.
Fill Ratio: Your Window Into Queue Position
Fill ratio = filled quantity / submitted quantity, measured per order or per session.
On Solana DEX markets and Hyperliquid's order book, queue position is everything. A fill ratio below 70 % on limit orders usually means one of three things: you are posting too aggressively and cancelling before the market comes to you, your latency is putting you at the back of the queue, or the spread is wider than your model assumes at the time of submission. All three diagnoses point to different fixes.
We track fill ratio broken down by side (bid vs. ask) and by time-of-session bucket. A fill ratio that degrades in the last hour of a trading window on Hyperliquid perps is a reliable signal of end-of-funding-period microstructure distortion — the kind of pattern you only see when you instrument at this level of granularity.
Adverse-Selection Rate: The Metric That Kills Strategies Slowly
Adverse-selection rate = fraction of fills followed by immediate price movement against the fill direction, typically measured over a 500ms to 5s window post-fill.
If you are a maker and 60 % of your fills are immediately followed by the market moving against you, informed order flow is picking you off. Your strategy may show positive P&L for weeks while the adverse-selection rate quietly climbs, then gap down when a large informed trader begins targeting your quotes systematically.
We compute this per venue and per asset. On Solana AMM pools the equivalent metric is price impact ratio — how much your fill moved the pool price relative to your expected slippage model. Above a threshold (we use 1.8x expected), the fill is flagged as adversely selected.
Python Dashboard Template
The snippet below assumes a trade-log DataFrame with columns ts, side, qty, price, pnl. Drop it into a Jupyter notebook or a FastAPI endpoint and you get all five metrics in under 50 lines.
import pandas as pd
import numpy as np
def compute_metrics(df: pd.DataFrame, target_return: float = 0.0, lookback_s: int = 2) -> dict:
df = df.sort_values("ts").reset_index(drop=True)
# Calmar
equity = df["pnl"].cumsum()
rolling_max = equity.cummax()
drawdown = (equity - rolling_max)
max_dd = drawdown.min()
ann_return = df["pnl"].sum() / (df["ts"].iloc[-1] - df["ts"].iloc[0]).total_seconds() * 365 * 86400
calmar = ann_return / abs(max_dd) if max_dd != 0 else np.nan
# Sortino
daily_pnl = df.set_index("ts")["pnl"].resample("1D").sum()
downside = daily_pnl[daily_pnl < target_return] - target_return
downside_std = np.sqrt((downside ** 2).mean())
sortino = (daily_pnl.mean() * 252 - target_return) / (downside_std * np.sqrt(252) + 1e-10)
# Profit Factor
gross_profit = df.loc[df["pnl"] > 0, "pnl"].sum()
gross_loss = abs(df.loc[df["pnl"] < 0, "pnl"].sum())
profit_factor = gross_profit / gross_loss if gross_loss > 0 else np.nan
# Fill Ratio (requires submitted_qty column)
if "submitted_qty" in df.columns:
fill_ratio = (df["qty"] / df["submitted_qty"]).mean()
else:
fill_ratio = np.nan
# Adverse Selection Rate
adv_sel_count = 0
for i, row in df[df["side"] == "buy"].iterrows():
window = df[(df["ts"] > row["ts"]) & (df["ts"] <= row["ts"] + pd.Timedelta(seconds=lookback_s))]
if not window.empty and window["price"].iloc[-1] < row["price"]:
adv_sel_count += 1
buy_fills = (df["side"] == "buy").sum()
adv_sel_rate = adv_sel_count / buy_fills if buy_fills > 0 else np.nan
return {
"calmar": round(calmar, 3),
"sortino": round(sortino, 3),
"profit_factor": round(profit_factor, 3),
"fill_ratio": round(fill_ratio, 3) if not np.isnan(fill_ratio) else None,
"adverse_selection_rate": round(adv_sel_rate, 3),
}
Wire this to a cron that pulls logs from your exchange connector every 15 minutes and you have a live scorecard. We alert on Calmar dropping below 5, profit factor below 1.2, or adverse-selection rate above 55 % — any single trigger is enough to pause the strategy and review.
The right scorecard forces you to be honest about what is working and why. Sharpe gives you a single number to hide behind. These five give you nowhere to hide.
If you want to see how this scorecard integrates with a production bot deployment across Solana, Hyperliquid, and Polymarket, get in touch — we can walk through a live session with real log data.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article