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.0.0 · Response generated:
89 days · 1405/02/27, 15:13 UTC to 1405/05/23, 18:45 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. A win is a trade whose recorded ROI is strictly greater than zero. Nothing else is filtered — every loss is included.
Definition of a win
A win is a closed gradeable trade whose recorded realised ROI (positions.pnl_percent_on_close) 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.
Definition of a loss
Any closed gradeable trade with recorded ROI <= 0. All of them are published.
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
- ADAPTIVE:* (any adaptive throttle)
339 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,495 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)|
- 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,495 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 759 of the 1,083 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. The default basis is 'recorded' — the conservative one, and the one that matches what was announced on Telegram at the time. Live count: 86 of the 1,155 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 2026-08-15 across the whole gradeable universe: 14,568 events, none before the position opened, three after it closed and all three within 125 seconds (one position, TYR-260520-IQZ3E1S0). 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.
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,495 |
| closed_gradeable_trades | 1,155 |
| excluded_housekeeping_rows | 339 |
| trades_with_telegram_message | 905 |
| trades_with_observed_exit_price | 1,083 |
| trades_with_nonreconciling_exit_price | 759 |
| trades_with_partial_fills | 86 |
| trades_without_recorded_leverage | 0 |
| tp_hit_events | 816 |
| distinct_symbols | 93 |
Data range: – · 93 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. 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.