Signal methodology and Signal Transparency Lab metrics
Code: src/lib/signals/engine.ts (rules + lifecycle), src/lib/signals/store.ts (persistence), src/lib/signals/metrics.ts (metrics).
Classification taxonomy
Every signal row carries exactly one classification (enforced by a CHECK constraint):
| Classification | Meaning |
|---|---|
live | Published in real time and acted on with real orders. Sky Terminal does not currently produce these. |
paper | Published in real time with a defined entry, stop, targets and exit rules; outcomes are derived from exchange candles. No order is executed. |
backtest | Generated after the fact by running rules over historical data. Subject to look-ahead and overfitting risk. |
historical | Real-time signals from an earlier system/version whose records are complete and kept as published. |
imported | Brought in from an external export (importLegacySignals). Timestamps, prices and reported results are preserved as given and not independently verified. |
Metrics can be broken down by classification (byClassification); never mix them without saying so.
Integrity rules
signalsrows are immutable: database triggers rejectUPDATEandDELETE. A signal is written once, at generation time, with its entry, stop, targets, entry conditions, closure criteria, data sources, timeframe and market regime.- Lifecycle is the append-only
signal_eventslog, one row per state per signal (opened,tp1,tp2,stopped,trail_exit,expired,closed,invalidated). The current state is the latest event. - Admin corrections go to the append-only
signal_correctionstable (field, old value, new value, reason, actor, time). The original row is never changed; corrections are shown next to it. Metrics are computed from the original record. - Losing signals are never deleted, hidden or filtered. Imports bring in losses exactly like winners.
Strategy sky-capitulation version 1.0.0 (paper)
Parameters are stored in signal_strategies (engine, version, params JSON). The engine refuses to run if the stored parameters for a version differ from the code: any parameter change requires a new version.
Universe: Binance USD-M perpetuals, the top 120 by 24h quote volume with at least $20M volume, from the latest market snapshot (must be < 15 min old).
Trigger — evaluated once per hour on the most recently closed 1h candle:
- Close is at least 15 % below the highest high of the prior 24 candles.
- Candle quote volume is at least 3× the median quote volume of the prior 24 candles.
- Lower wick (
min(open, close) − low) is at least 50 % of the candle range (high − low). - No open signal from this strategy on the symbol.
- No new signal on a symbol within 72 h of a
stoppedevent on that symbol (any stop, including a breakeven one).
The scan runs only within 15 minutes after the candle closes; a missed hour is not backfilled (no retroactive signals). Signal ids are deterministic (engine|version|symbol|candle time), so re-running is idempotent.
Market regime at generation: BTCUSDT 24h change > +3 % → risk_on, < −3 % → risk_off, otherwise neutral (unknown if unavailable). Timeframe is 1h.
Plan (long only):
| Level | Action | |
|---|---|---|
| Entry / reference | 1h candle close | full position |
| Stop | entry × 0.88 (−12 %) | exit everything remaining |
| TP1 | entry × 1.10 (+10 %) | close 1/3 |
| TP2 | entry × 1.20 (+20 %) | close 1/3, move stop to entry |
| Trail | max(entry, highest high since TP2 × 0.92) | exit last 1/3 (trail_exit) |
| Max hold | 7 days from the candle close | exit remaining at the next 5m open (expired) |
Note: once TP2 is reached the trail level is at least 1.20 × 0.92 = 1.104 × entry, so the trail always dominates the breakeven stop in this version.
Lifecycle resolution (advanceOpenSignals): 5m Binance candles from the last state change onward are walked in time order. Only closed candles are used. Rules inside one candle:
- The stop is checked first. If a candle touches both the stop and a target, the stop is assumed to have hit first (conservative).
- A candle that opens through the stop fills at its open (gap), not at the stop.
- Targets fill at the target price. TP1 and TP2 may hit in the same candle.
- The trailing stop is enforced from the candle after the TP2 candle; the TP2 candle's high seeds the trail.
- If no market data arrives for 24 h after expiry, the signal is marked
invalidatedand excluded from metrics with that reason.
Event timestamps of tp1/tp2/exits are the open time of the 5m candle where the level was touched.
Costs: 0.05 % fee + 0.05 % slippage per side on every fill (entry, each partial exit, final exit), as a percentage of that fill's notional relative to the entry notional:
gross% = Σ fraction_i × (exit_i / entry − 1) × 100 sides = 1 + Σ fraction_i × exit_i / entry fees% = 0.05 × sides slippage% = 0.05 × sides net% = gross% − fees% − slippage%
The result is stored on the closed event with gross, fees, slippage, fills, exit reason and holding time.
Caveat on entry: the reference entry is the 1h close. The signal becomes visible a few minutes later (at generation). A reader acting on it would usually get a different price.
Opens and closes are published to Sky Feed (category = signal, source = signals, entitlement = premium, dedupe key signal:{id}:{state}). The engine is disabled by the signals.engine flag.
Metric definitions (computeMetrics)
All P&L metrics are equal-weighted (each signal = one unit of notional), unleveraged (percent of entry notional, not of margin) and net of fees and slippage as recorded in each signal's result.
| Metric | Definition | ||
|---|---|---|---|
| total | all signals passed in, including excluded ones | ||
| open | included signals without a realized result | ||
| closed | included signals with a realized net result and close time | ||
| wins | closed with net result > 0 | ||
| losses | closed with net result ≤ 0 — breakeven counts as a non-win/loss | ||
| win rate | wins / closed (closed only; null if none) | ||
| average gain / loss | mean net % of wins / of losses | ||
| average holding period | mean of closed.at − opened.at | ||
| profit factor | Σ net % of wins / | Σ net % of losses | ; null with a note when there are no losses (or nothing closed) rather than ∞ |
| max drawdown | largest peak-to-trough fall of the cumulative sum of per-signal net %, ordered by close time (ties by id), with the peak starting at 0. Units: percentage points | ||
| R distribution | realized net % ÷ planned stop distance % (` | entry − stop | / entry), bucketed < −1R, −1R–0R, 0R–1R, 1R–2R, 2R–3R, ≥ 3R. Signals without a stop are counted in rUnavailable` |
| breakdowns | by symbol, strategy+version, timeframe, market regime and classification |
Exclusions are surfaced in excluded: [{ id, reason }], never dropped silently:
- alert-only records (
signal_typealert) — no defined entry/exit methodology; - missing entry price;
invalidatedsignals (with their reason);- closed without a realized result, or without a close time;
- terminal exit state without a
closedresult.
What is NOT claimed
- No independent verification. Results are computed by Sky Terminal from public exchange candles and its own records. No third party has audited them.
- Not executed trades.
papersignals are not orders. Real fills, partial fills, queue position, funding payments, liquidation risk, outages and latency are not modelled beyond the flat fee/slippage assumption. - Imported results are as reported by the source and may be gross of fees.
- Past results do not predict future results. A small sample can look very good or very bad by chance.
- Nothing here is investment advice or a recommendation to trade.