Methodology: how these numbers are counted
Everything on this page is read straight from the same engine that produces the numbers on the site. If a definition changes, this page changes with it — none of these statements is typed in here.
Methodology version 1.2.0 · Response generated:
131 days · 1405/02/27, 15:13 UTC to 1405/07/03, 14:35 UTC
Which trades enter the record
A trade enters the public record when it has closed (closed_at IS NOT NULL), has a recorded realised ROI (pnl_percent_on_close IS NOT NULL AND <> 0), and was not closed by a risk-engine housekeeping action. Nothing else is filtered — every loss is included. A WIN IS THE SAME TEST ON EVERY BASIS — a trade whose ROI on that basis is strictly greater than zero — and every metric says which basis it used. What differs between the bases is the ROI, not the verdict: basis=ladder (the default) measures a trade at the highest take-profit rung its price reached, on the full position, while recorded and blended measure it at its final exit. How often price reached a rung is published separately, as success_rate_pct and reach_pct, and is a different and smaller number than the win rate. The universe itself — which rows are graded at all — is identical on all three bases, so the three answers are three readings of one set of trades and not three different samples.
Definition of a win
WHICH RULE APPLIES DEPENDS ON THE BASIS, and every metric envelope states the one it used. basis=recorded and basis=blended: a win is a closed gradeable trade whose ROI on that basis is strictly greater than zero. This deliberately counts a trailing stop that closed in profit as a win and a break-even stop as a loss, because the outcome, not the status token, is what a follower experienced. basis=ladder (the default): A trade WINS when its ROI is positive, on every basis. Until 2026-08-24 basis=ladder answered positions.tp_hit_count >= 1 instead, which put the counts and the money on different partitions of the same rows: 301 of 1288 trades closed green without ever tagging a target and were published as losses beside their own positive returns. Under the peak figure the ROI test is a SUPERSET of the rung test, not a rival to it — a target can only be tagged in the profit direction, so every trade that reached a rung still wins. Rung reach is still published in reach_pct and success_rate_pct (SPEC section 5.1); it is no longer a second verdict on the same trade. REPORTING only — every live trading control loop still grades a trade by the sign of positions.pnl_percent_on_close. Live count: 659 of the 1,574 closed gradeable trades reached at least TP1 and are successes under this basis.
Definition of a loss
basis=recorded/blended: any closed gradeable trade with ROI <= 0 on that basis. basis=ladder: any closed gradeable trade with tp_hit_count = 0 — it never reached a published target, whatever its ROI. All of them are published either way; nothing is filtered out of the ledger on any basis.
What is excluded, and why
Excluded rows are positions the risk engine flattened for reasons unrelated to the trade thesis (exposure caps, correlation guards, circuit breakers, news blackouts, adaptive throttles). They close at exactly pnl_percent_on_close = 0 because no market outcome was realised. Counting them as wins would inflate the win rate; counting them as losses would deflate it. They are reported as a count so the exclusion is auditable.
Excluded close reasons
- EXPOSURE_CAP
- ADMIN_BULK_CLOSE_P10
- SIGNAL_GUARD
- CORR_GUARD
- CIRCUIT_BREAKER
- CLAUDE_VETO
- CLAUDE_L5_ABORT
- TF_HIERARCHY
- NEWS_BLACKOUT
- LIQUIDITY
- MAX_HOLD_MANUAL_EXPOSURE_UNBLOCK
- OI_PUMP
- ADAPTIVE:* (any adaptive throttle)
333 closed rows are set aside for these reasons, and the count is published here.
How the return is computed
- Source column
- positions.pnl_percent_on_close
- Written by
- app/trading/monitor.py via app.trading.utils.roi_percent()
- Formula
- ((exit - entry)/entry for LONG, (entry - exit)/entry for SHORT) * leverage * 100
- Unit
- percent of margin (leveraged ROI)
Important warning
This number ALREADY includes leverage. Multiplying it by leverage again is the double-count that produced the -192 USD display for a -19.21% trade.
Unleveraged price move
The unleveraged price move. Derived algebraically from the authoritative recorded ROI rather than recomputed from stored exit prices — see known_limitations.
leveraged_roi_pct / leverage
The equity curve — a model, not an account balance
This curve is a model, not an account balance. Assumption: each trade allocates 2% of current equity as margin, trades are taken sequentially with no overlap, and fees and funding are not modelled. The starting value of 100 is an index, not dollars.
equity_n = equity_(n-1) * (1 + allocation_pct/100 * leveraged_roi_pct/100)
allocation_pct is an assumption chosen by whoever called this endpoint, not a fact measured from the data. Every metric listed in sizing_dependent_metrics moves with it and each one restates the value used in its own method string. At a small allocation the compounded curve approaches the arithmetic sum of per-trade ROI and can be positive while a larger allocation is negative, because losses compound. The per-trade record — win rate, profit factor, expectancy, the ledger — does NOT depend on it.
Why no dollar figure is published
positions.quantity, positions.pnl_usdt and positions.exit_price are 100% NULL across all 1,912 rows of positions. Position size was never recorded, so absolute profit in USDT is not computable from this dataset and is never published.
Risk metrics
- profit_factor
- sum(positive ROI-points) / |sum(negative ROI-points)|. Partitioned by the SIGN of the ROI on the requested basis — on basis=ladder that is not the same partition as wins and losses, because a trade that banked a rung and gave the remainder back is a success with a negative ROI.
- expectancy
- mean leveraged ROI per trade
- max_drawdown
- peak-to-trough of the compounded equity curve, expressed in percent
- sharpe
- annualised on calendar-daily returns of the compounded curve, sqrt(365), risk-free rate 0, days with no closed trade count as 0%. Requires >= 30 days.
- calmar
- annualised compounded return / |max drawdown|. Requires >= 30 days of history.
- sizing_dependence
- profit_factor, expectancy and the win rate are properties of the trade record and do not move with any assumption. max_drawdown, sharpe and calmar are computed on the compounded curve and therefore DO move with allocation_pct, which the caller chooses. Every one of those envelopes restates the allocation_pct it used in its own method string and carries sizing_dependent=true.
Known limitations
We do not hide this list and we do not shorten it. Each entry has its own id, severity and technical explanation.
L1 — No money figure exists anywhere in the dataset Critical
positions.pnl_usdt, positions.exit_price, positions.quantity and positions.order_id are 100% NULL across all 1,912 rows. Position size was never recorded, so absolute profit in USDT is not computable. No endpoint on this surface returns a USDT figure, and any page that shows one is wrong.
L2 — Stored exit prices do not reproduce the recorded ROI High
signal_quality_metrics.exit_price is the market price observed when the exit condition was detected. On stop exits the engine deliberately books the ROI at a modelled fill of sl +/- 0.10% instead (app/trading/monitor.py, P7 2026-05-30). Measured live from the current dataset: the two disagree by more than 0.5 ROI-points on 1,037 of the 1,434 trades that have both, and the disagreement is systematically flattering to the observed series. Aggregates therefore use the recorded ROI only; the observed exit price is published per-trade with a reconciliation flag.
L3 — Recorded ROI ignores partial take-profit fills High
pnl_percent_on_close records only the final exit leg. Since 2026-07-30 the TP ladder closes positions in parts (partial_close_log). For those trades the recorded number UNDER-states the real result. basis=blended reconstructs sum(close_pct * roi_at_close) + (1 - sum(close_pct)) * recorded, but only for the trades that have those rows. basis=ladder — the default since methodology 1.2.0 — prices every rung the price reached from the ladder published with the signal and covers the whole history rather than one era of it; basis=recorded still returns the conservative figure that was announced on Telegram at the time, and is the audit trail. Live count: 105 of the 1,574 closed gradeable trades have at least one recorded partial fill.
L4 — ROI is not comparable across time High
Stop distance was tightened from roughly 5-8% to roughly 1.2-1.5% during 2026-08 and average leverage fell from 15.2x to 11.8x. A May trade and an August trade are measured with different rulers. avg_leverage is published per month so the break is visible rather than hidden.
L5 — Signal-record performance, not audited account statements Medium
These are the outcomes of published signals as recorded by our own monitor. There is no exchange order id, no fill, no fee and no funding cost in the dataset. A follower's real result will differ. What IS verifiable is publication: see row_counts.trades_with_telegram_message — each of those trades carries the id of the message that announced it in the channel before the outcome was known.
L6 — Timestamps are stored as text in three different shapes Medium
Every timestamp column in this dataset is TEXT, not a timestamptz. Measured 2026-08-15: positions.created_at (1495/1495), activated_at (1382/1382) and closed_at (1494/1494) always carry an explicit UTC offset — '+03:30' from app.trading.utils.now_iso(), or '+00' on the 70 rows written by the 2026-05-30 admin bulk close. partial_close_log.executed_at (125/125) always carries '+03:30'. position_events.occurred_at is the exception: 14,935 of 14,937 rows are NAIVE and are Tehran local time, because db_positions.py writes them with now_sql_naive(). Reading those as UTC shifted the whole event log 3.5 hours forward and produced per-trade pages whose entry came after their own exit; that is fixed, and there is now exactly one conversion function. Everything published is UTC and month buckets are computed in UTC, so the calendar-month boundary sits at 03:30 Tehran, not midnight.
L7 — Concurrency is not modelled in the equity curve Medium
The compounded curve applies trades sequentially in close order. In reality several positions were open at once, so the real path of a follower's equity would differ. The curve is a model of the trade sequence, not a reconstruction of an account.
L8 — A closing event can be logged a few seconds after the close Low
position_events rows are written by the monitor loop after it has already stamped positions.closed_at, so a closing event can trail the close slightly. Measured live across the whole gradeable universe: 143,258 timestamped events, 1 before the position opened, 41 after it closed across 39 position(s), the worst by 125s. The published allowance is 300 seconds; /trade/{code} ships a timeline_consistency block that lists any event outside it instead of hiding it.
L9 — Two endpoints can be served from snapshots up to one TTL apart Low
All arithmetic comes from this single module, so no two surfaces can ever apply different DEFINITIONS. The snapshot is a different matter: the trade set is cached per worker process for 60 seconds and the API runs several workers, so /summary and /ledger can be answered from snapshots up to one TTL apart and differ by whatever closed in between. Every payload publishes provenance.data_loaded_at and provenance.cache_ttl_seconds, so the skew is always measurable from the responses themselves; compare those two fields before comparing two numbers.
L10 — The ladder basis counts a touched target as a success, and can call a money-losing trade a win High
Under the default basis a trade is a success when it reached at least one take-profit rung, because reaching TP1 banked the fraction of the position the published ladder assigned to TP1. A trade that tagged TP1 and then gave the remainder back on a stop is therefore counted as a win AND carries a negative realised ROI: the same row can read result=win with leveraged_roi_pct below zero. That is not a contradiction and it is not hidden — the money metrics on this basis are computed on those same rows, outcome_class says which of them gave profit back (TP_THEN_SL, TP_THEN_BREAKEVEN), and basis=recorded reproduces the ROI-sign win rate exactly. Read the success rate and the profit factor together; either one alone describes half of this record.
L11 — Ladder figures cover only the rows the accounting has stamped Medium
realized_pnl_pct, tp_hit_count, success_weight and outcome_class are written row by row by app/services/ladder_accounting_store.py — for history by the backfill, for new closes by the monitor, through the same function so the two cannot diverge. A row it has not reached yet falls back to the blended and then the recorded ROI, which UNDER-states it, and is flagged per trade as ladder_accounted=false with ladder_roi_source naming the column actually used. Every ladder aggregate publishes its coverage in ladder_accounting.coverage. Live count: 1,574 of the 1,574 closed gradeable trades carry a stored realized_pnl_pct. The weighted success rate is the one figure withheld entirely until coverage is complete, because the rows still missing are exactly the ones whose success weight is above zero and any interim value would be biased downward.
L12 — The take-profit rungs were announced, never executed on an exchange Medium
The take-profit rungs priced by this basis were ANNOUNCED, not executed. partial_close_log carries volume_closed = 0.0 and order_id = NULL on 100% of its rows, order_fills is empty and positions.user_id is NULL everywhere: there is no exchange fill behind any of it. TradeYar is a signal service, so the ladder that was published to subscribers is the correct thing to account for — but it is a record of what was published, not of money that moved. A subscriber's own result depends on whether they took the rungs, at what size, with what fees.
Timestamp policy
Every timestamp published by this module is ISO-8601 in UTC (+00:00). Source columns are TEXT in mixed formats: positions.created_at/activated_at/closed_at and partial_close_log.executed_at always carry an explicit UTC offset; position_events.occurred_at is written naive in Asia/Tehran local time by app.trading.utils.now_sql_naive(). A naive value is therefore interpreted as Asia/Tehran and converted to UTC. There is exactly one conversion function, so no two timestamps in one payload can be in different zones.
Live row counts
| Counter | Value |
|---|---|
| positions_table_rows | 1,912 |
| closed_gradeable_trades | 1,574 |
| excluded_housekeeping_rows | 333 |
| trades_with_telegram_message | 1,277 |
| trades_with_observed_exit_price | 1,434 |
| trades_with_nonreconciling_exit_price | 1,037 |
| trades_with_partial_fills | 105 |
| trades_without_recorded_leverage | 0 |
| tp_hit_events | 1,688 |
| distinct_symbols | 94 |
| trades_with_ladder_accounting | 1,574 |
| trades_without_ladder_accounting | 0 |
| trades_that_reached_tp1 | 659 |
| event_log_rows | 143,258 |
| event_log_rows_after_position_close | 41 |
| event_log_rows_before_position_start | 1 |
| positions_with_event_log_drift | 39 |
Data range: – · 94 symbols
The same data, machine-readable
All of these endpoints are public and unauthenticated. You can reproduce every number on this site yourself.
- summary: /api/v1/public/performance/summary?window=7d|30d|90d|all
- equity: /api/v1/public/performance/equity?window=&basis=&allocation_pct=
- monthly: /api/v1/public/performance/monthly
- ledger: /api/v1/public/performance/ledger?limit=&offset=&symbol=&direction=&timeframe=&result=&from=&to=&sort=&order=
- ledger_csv: /api/v1/public/performance/ledger?format=csv
- trade: /api/v1/public/performance/trade/{unique_code}
- by_symbol: /api/v1/public/performance/by-symbol
- by_timeframe: /api/v1/public/performance/by-timeframe
- by_direction: /api/v1/public/performance/by-direction
- tp_ladder: /api/v1/public/performance/tp-ladder?window=
- execution_quality: /api/v1/public/performance/execution-quality?window=
- proof_of_publication: /api/v1/public/performance/proof-of-publication?window=
Every number on this surface is computed by one module, so no two endpoints can apply different definitions of a win, a window or a universe FOR THE SAME BASIS. The basis itself is a parameter — recorded, blended, ladder, default ladder — and it changes what a win measures: the final exit on recorded, the partial-fill correction on blended, the highest rung reached on ladder. A win is the sign of that basis's ROI on all three. Every payload names the basis it used and every metric restates the rule in its own method string, so two figures that differ can always be traced to the question each was answering. The data SNAPSHOT is cached per worker process for 60 seconds and the API runs several workers, so two endpoints can be answered from snapshots up to one TTL apart and differ by whatever closed in between. Compare provenance.data_loaded_at on the two responses before comparing their numbers — that field exists so the skew is measurable rather than deniable.