When a promising candidate produces no order, the first useful question is where its evidence stops. Discovery means the system noticed an opportunity. Evaluation means it recorded a decision. Dispatch means it recorded handing work to another component. These are different events. None establishes a completed trade.

This edition builds a small candidate-trace report that separates explicit blocks from missing evidence. It uses six invented candidates and no market connection. You will finish with a reproducible report, including delayed collection, duplicate messages and invalid histories.

Reader problem and inspected evidence

The owner's missed-candidate questions informed the October 10 editorial audit. A separate public example is Freqtrade issue 11806, opened May 28, 2025 and closed June 1. Its author reported a machine-learning strategy that opened no trades. In the discussion, maintainers identified a stake configuration incompatible with the exchange minimum. That historical diagnosis shows why inspecting admission conditions matters. It does not establish the frequency of this problem or diagnose our system.

For this investigation, the repository's trading branch was main at ddf22b363245ebf351a411d3ce09c0dc77bd3377. We inspected app/funded_nomination.py and app/market_feed.py at commit 48a72d3b66e0bfa82a66bcc18481bdf0658c74b8, which adds funded-discovery blocker and safety-source diagnostics. Railway showed a successful market-feed deployment of that commit.

The inspected code limits detailed candidate diagnostics to four candidates. It records confirmation blockers and evidence failures and labels nomination evidence MARKET_CONFIRMATION_NOT_A_FILL. These are useful clues, but a bounded sample cannot establish a complete candidate funnel. A targeted runtime-log query for ROBINHOOD_FUNDED_DISCOVERY, covering October 12, 2026, 09:55 through 10:04 UTC, returned no matches. That is a bounded absence of evidence, not proof that discovery failed. We verified no funded outcome for this article.

Prerequisites and research question

Use Python 3.12, an empty directory and about 35 minutes. The program needs only the standard library. It reads no credentials and makes no network calls.

Our question is: At a fixed collection cutoff, what is the last evidenced stage for each discovered candidate? Before running, predict six discoveries, five evaluations, three passes and two recorded dispatches. Predict two explicit blocks and two unknown cases. The empty cohort must remain empty rather than produce a percentage.

1. Define one case and an immutable event

A candidate identifier describes one evaluation case, not every appearance of a coin forever. A new evaluation belongs to a new case under this deliberately narrow schema. Each event has a unique identity, a sequence number and a stage. Repeated delivery of the same event must not create another opportunity. Reusing an identity with different content is an error.

The permitted histories are discovery alone, discovery followed by evaluation, or discovery followed by evaluation and dispatch. A dispatch requires a recorded pass. This checks the exported history; it does not enforce permissions on an actual executor.

DISPATCH_RECORDED means exactly that. A worker may fail after dispatch, and an external action may occur without its response arriving. Edition 7 explains why an uncertain external outcome requires reconciliation before retrying.

2. Keep occurrence and collection time separate

happened_at records occurrence in this fixture. observed_at records when the collector received it. OpenTelemetry's log data model makes a corresponding distinction between event and observation timestamps.

Candidate D's dispatch happens at second two but reaches the collector at second nine. Our cutoff is second six. Its evaluation is available; its dispatch is not. The report must say UNKNOWN:dispatch_missing, even though the completed fixture contains a later receipt.

Filter unavailable observations before checking duplicate identities. Otherwise a future conflicting message could alter a report about an earlier evidence set. The lab assumes a common, trustworthy clock. Aware timestamps solve timezone ambiguity, not clock synchronization. Real distributed records need explicit clock and causal-order contracts.

3. Run the complete offline example

Save this as trace_lab.py, then run python3 -I trace_lab.py. The isolation flag prevents Python from adding the current directory and user site packages to its import path. The code prints its report and checks both valid and invalid histories.

from collections import Counter, defaultdict
from datetime import datetime, timedelta, timezone

START = datetime(2026, 10, 1, 12, tzinfo=timezone.utc)
CUTOFF = START + timedelta(seconds=6)


def event(ident, candidate, seq, stage, second, decision=None,
          reason=None, received=None):
    return dict(event_id=ident, candidate=candidate, seq=seq, stage=stage,
                decision=decision, reason=reason,
                happened_at=(START + timedelta(seconds=second)).isoformat(),
                observed_at=(START + timedelta(seconds=second if received is None
                                               else received)).isoformat())


def clock(value):
    try:
        result = datetime.fromisoformat(value)
    except (TypeError, ValueError):
        raise ValueError('INVALID_CLOCK') from None
    if result.tzinfo is None:
        raise ValueError('INVALID_CLOCK')
    return result.astimezone(timezone.utc)


def diagnose(records, cutoff):
    unique, duplicates, withheld = {}, 0, 0
    for item in records:
        observed = clock(item['observed_at'])
        if observed > cutoff:
            withheld += 1
            continue
        happened = clock(item['happened_at'])
        if observed < happened:
            raise ValueError('INVALID_CLOCK_ORDER')
        if not isinstance(item.get('event_id'), str) or not item['event_id']:
            raise ValueError('INVALID_ID')
        ident = item['event_id']
        if ident in unique:
            if unique[ident] != item:
                raise ValueError('EVENT_ID_COLLISION')
            duplicates += 1
        else:
            unique[ident] = item
    groups = defaultdict(list)
    for item in unique.values():
        if type(item['seq']) is not int or item['seq'] < 1:
            raise ValueError('INVALID_SEQUENCE')
        if not isinstance(item['candidate'], str) or not item['candidate']:
            raise ValueError('INVALID_ID')
        groups[item['candidate']].append(item)
    counts = dict(discovered=len(groups), evaluated=0, passed=0,
                  dispatch_recorded=0)
    states = {}
    for candidate, items in sorted(groups.items()):
        items.sort(key=lambda item: item['seq'])
        stages = [item['stage'] for item in items]
        if stages not in (['DISCOVERED'], ['DISCOVERED', 'EVALUATED'],
                          ['DISCOVERED', 'EVALUATED', 'DISPATCHED']):
            raise ValueError('BAD_STAGE_CHAIN')
        if [item['seq'] for item in items] != list(range(1, len(items) + 1)):
            raise ValueError('BAD_STAGE_CHAIN')
        times = [clock(item['happened_at']) for item in items]
        if times != sorted(times):
            raise ValueError('INVALID_CLOCK_ORDER')
        if items[0]['decision'] is not None or items[0]['reason'] is not None:
            raise ValueError('INVALID_DECISION')
        if len(items) == 1:
            states[candidate] = 'UNKNOWN:evaluation_missing'
            continue
        evaluated = items[1]
        decision, reason = evaluated['decision'], evaluated['reason']
        if decision not in ('PASS', 'BLOCK'):
            raise ValueError('INVALID_DECISION')
        if decision == 'PASS' and reason is not None:
            raise ValueError('INVALID_DECISION')
        if decision == 'BLOCK' and (not isinstance(reason, str) or not reason):
            raise ValueError('INVALID_DECISION')
        counts['evaluated'] += 1
        if decision == 'BLOCK':
            if len(items) == 3:
                raise ValueError('DISPATCH_WITHOUT_PASS')
            states[candidate] = 'BLOCKED:' + reason
            continue
        counts['passed'] += 1
        if len(items) == 3:
            if items[2]['decision'] is not None or items[2]['reason'] is not None:
                raise ValueError('INVALID_DECISION')
            counts['dispatch_recorded'] += 1
            states[candidate] = 'DISPATCH_RECORDED'
        else:
            states[candidate] = 'UNKNOWN:dispatch_missing'
    return counts, states, (len(records), len(unique), duplicates, withheld)


rows = [
    event('a1', 'A', 1, 'DISCOVERED', 0),
    event('a2', 'A', 2, 'EVALUATED', 1, 'PASS'),
    event('a3', 'A', 3, 'DISPATCHED', 2),
    event('b1', 'B', 1, 'DISCOVERED', 0),
    event('b2', 'B', 2, 'EVALUATED', 1, 'BLOCK', 'route_missing'),
    event('c1', 'C', 1, 'DISCOVERED', 0),
    event('d1', 'D', 1, 'DISCOVERED', 0),
    event('d2', 'D', 2, 'EVALUATED', 1, 'PASS'),
    event('d3', 'D', 3, 'DISPATCHED', 2, received=9),
    event('e1', 'E', 1, 'DISCOVERED', 0),
    event('e2', 'E', 2, 'EVALUATED', 1, 'PASS'),
    event('e3', 'E', 3, 'DISPATCHED', 2),
    event('f1', 'F', 1, 'DISCOVERED', 0),
    event('f2', 'F', 2, 'EVALUATED', 1, 'BLOCK', 'cost_missing'),
]
rows.append(dict(rows[1]))
counts, states, meta = diagnose(rows, CUTOFF)
print('records=%d included_unique=%d duplicates=%d withheld=%d' % meta)
print('funnel ' + ' '.join(f'{key}={value}' for key, value in counts.items()))
for candidate, state in states.items():
    print(f'{candidate}: {state}')
totals = Counter(state.split(':')[0] for state in states.values())
print('states blocked=%d unknown=%d dispatch_recorded=%d' %
      (totals['BLOCKED'], totals['UNKNOWN'], totals['DISPATCH_RECORDED']))
empty = diagnose([], CUTOFF)
print(f'empty discovered={empty[0]["discovered"]} rate=undefined')

checks = 0


def check(condition):
    global checks
    if not condition:
        raise AssertionError('CHECK_FAILED')
    checks += 1


check(meta == (15, 13, 1, 1))
check(list(counts.values()) == [6, 5, 3, 2])
check(states == dict(A='DISPATCH_RECORDED', B='BLOCKED:route_missing',
                     C='UNKNOWN:evaluation_missing', D='UNKNOWN:dispatch_missing',
                     E='DISPATCH_RECORDED', F='BLOCKED:cost_missing'))
check(sum(totals.values()) == counts['discovered'])
check(empty == (dict(discovered=0, evaluated=0, passed=0,
                     dispatch_recorded=0), {}, (0, 0, 0, 0)))
later_collision = {**rows[1], 'reason': 'changed',
                   'observed_at': (START + timedelta(seconds=9)).isoformat()}
check(diagnose(rows + [later_collision], CUTOFF)[1] == states)
bad = {
    'collision': (rows + [{**rows[1], 'reason': 'changed'}], 'EVENT_ID_COLLISION'),
    'orphan': ([rows[1]], 'BAD_STAGE_CHAIN'),
    'after_block': ([rows[3], rows[4], event('b3', 'B', 3, 'DISPATCHED', 2)],
                    'DISPATCH_WITHOUT_PASS'),
    'naive_clock': ([{**rows[0], 'happened_at': '2026-10-01T12:00:00'}],
                    'INVALID_CLOCK'),
    'malformed_clock': ([{**rows[0], 'observed_at': 'invalid'}], 'INVALID_CLOCK'),
    'repeated_stage': ([rows[0], rows[1],
                        event('a4', 'A', 3, 'EVALUATED', 2, 'PASS')],
                       'BAD_STAGE_CHAIN'),
    'clock_reversal': ([{**rows[1], 'observed_at': rows[0]['observed_at']}],
                       'INVALID_CLOCK_ORDER'),
}
for name, (records, expected) in bad.items():
    try:
        diagnose(records, CUTOFF)
    except ValueError as error:
        check(str(error) == expected)
        print(f'{name}: rejected={error}')
    else:
        raise AssertionError('BAD_CASE_ACCEPTED:' + name)
print(f'checks={checks} passed; submitted_orders=0')

Expected complete stdout:

records=15 included_unique=13 duplicates=1 withheld=1
funnel discovered=6 evaluated=5 passed=3 dispatch_recorded=2
A: DISPATCH_RECORDED
B: BLOCKED:route_missing
C: UNKNOWN:evaluation_missing
D: UNKNOWN:dispatch_missing
E: DISPATCH_RECORDED
F: BLOCKED:cost_missing
states blocked=2 unknown=2 dispatch_recorded=2
empty discovered=0 rate=undefined
collision: rejected=EVENT_ID_COLLISION
orphan: rejected=BAD_STAGE_CHAIN
after_block: rejected=DISPATCH_WITHOUT_PASS
naive_clock: rejected=INVALID_CLOCK
malformed_clock: rejected=INVALID_CLOCK
repeated_stage: rejected=BAD_STAGE_CHAIN
clock_reversal: rejected=INVALID_CLOCK_ORDER
checks=13 passed; submitted_orders=0

4. Recount before interpreting the report

There are 15 messages: 13 unique available events, one repeat and one observation withheld by the cutoff. Five candidates reach evaluation. B and F have recorded blocks, with invented reasons route_missing and cost_missing. Three pass, but only A and E have available dispatch records.

C lacks evaluation evidence. D lacks available dispatch evidence. Neither is counted as blocked. The final partition is two blocked, two unknown and two with dispatch recorded, totaling all six discoveries. These are terminal evidence states at this cutoff, not trading outcomes or a conversion forecast.

The seven defective histories are rejected explicitly. In particular, a dispatch after a block is refused, an orphan evaluation cannot manufacture discovery, and malformed or timezone-free clocks cannot silently become current time. Thirteen built-in checks pass. The expected output was also checked against an independent SQL count and separate boundary cases before publication.

5. Apply the investigation to a read-only export

Start with a complete discovery cohort over a declared interval. Retain immutable case and event identities, policy version, stage, decision reason, occurrence time and collection time. Use a durable export rather than assuming a few sampled log lines represent every candidate.

For a discovery-only case, inspect evaluation scheduling and evidence collection. For a passed case without dispatch evidence, inspect the dispatcher and receipt collection. For an explicit block, inspect the recorded admission condition. Missing telemetry does not authorize an order. Keep the investigation read-only and retain the original export so another reviewer can recount it.

Existing guidance already covers parts of this task. Freqtrade's FAQ recommends checking logs and configuration when trades do not occur. Its backtest analysis guide exposes rejected signals for analysis. Our additional artifact is a cutoff-aware case partition that preserves unknown evidence and rejects contradictory histories. A backtest rejection is not an explanation of a live missing receipt.

Lamport's 1978 paper distinguishes causal ordering from a universal physical-time ordering. Our fixed sequence validates an intentionally simple single-case contract. It does not implement Lamport clocks or prove causality across services.

Troubleshooting and completion check

If a clock error occurs, repair the source export under a documented rule. Do not replace missing times with the time of your investigation. If an event identity collides, inspect the producer's identity contract. If a case legitimately has repeated evaluations, split it into distinct cases or design a richer schema before using this report.

Your completion check is exact stdout equality, the hand-counted six-case partition and a written explanation of why D remains unknown at second six. Also explain why two dispatch records cannot establish two fills. If those explanations differ, inspect the inputs before discussing performance.

The lab costs no API usage and changes no operational state. Its fallback is manual inspection of the frozen export. It does not model venue acceptance, clock skew, price movement, fees, model accuracy or funded returns. The next experiment will interrupt a synthetic durable dispatcher between claiming a case and collecting a receipt, then test recovery without turning an unknown external effect into permission to resend.