Clocks

Clocks are the central abstraction in chronopype. They manage time progression, coordinate processors, and emit events at each lifecycle stage.

Clock Modes

Chronopype provides two clock implementations:

Mode Class Use Case
ClockMode.REALTIME RealtimeClock Live systems, real-time data processing
ClockMode.BACKTEST BacktestClock Historical simulation, strategy backtesting

Both share the same BaseClock interface, so processors work identically in either mode.

Configuration

All clocks are configured via ClockConfig:

from chronopype import ClockConfig, ClockMode

config = ClockConfig(
    clock_mode=ClockMode.REALTIME,
    tick_size=1.0,              # seconds between ticks
    start_time=1700000000.0,    # UNIX timestamp
    end_time=0.0,               # 0 = no end (required > 0 for BACKTEST)
    processor_timeout=1.0,      # max seconds per processor tick
    max_retries=3,              # retry count for failed processors
    concurrent_processors=False, # True = run processors in parallel
    stats_window_size=100,      # rolling window for execution stats
)

ClockConfig is a frozen Pydantic model --- it cannot be modified after creation.

Clock Lifecycle

Clocks use the async context manager protocol:

async with RealtimeClock(config) as clock:
    # 1. __aenter__: emits ClockStartEvent, starts all processors
    clock.add_processor(my_processor)

    # 2. Run: executes ticks, emits ClockTickEvent per tick
    await clock.run_til(target_time)

    # 3. __aexit__: stops all processors, emits ClockStopEvent

Events

The clock emits three events via the eventspype publication system:

Event When Data
ClockStartEvent Context entry timestamp, mode, tick_size
ClockTickEvent Each tick timestamp, tick_counter, processors
ClockStopEvent Context exit timestamp, total_ticks, final_states

RealtimeClock

The realtime clock synchronizes with wall-clock time and handles drift:

import time
from chronopype import ClockConfig, ClockMode
from chronopype.clocks import RealtimeClock

config = ClockConfig(
    clock_mode=ClockMode.REALTIME,
    start_time=time.time(),
    tick_size=0.5,  # tick every 500ms
)

async with RealtimeClock(config) as clock:
    clock.add_processor(my_processor)

    # Run until a target time
    await clock.run_til(config.start_time + 60)

    # Or run indefinitely (cancel with asyncio cancellation)
    # await clock.run()

Drift handling: If a tick takes longer than tick_size, the next tick fires immediately to catch up. Processor timestamps are always aligned to tick boundaries regardless of wall-clock drift.

BacktestClock

The backtest clock steps through a defined time range deterministically:

from chronopype import ClockConfig, ClockMode
from chronopype.clocks import BacktestClock

config = ClockConfig(
    clock_mode=ClockMode.BACKTEST,
    start_time=1700000000.0,
    end_time=1700003600.0,  # 1 hour range
    tick_size=1.0,
)

async with BacktestClock(config) as clock:
    clock.add_processor(my_processor)
    await clock.run()  # processes all ticks from start to end

Step-by-Step Execution

The backtest clock supports fine-grained control:

async with BacktestClock(config) as clock:
    clock.add_processor(my_processor)

    # Advance by exactly 5 ticks
    new_time = await clock.step(5)

    # Advance to a specific timestamp
    new_time = await clock.step_to(1700001000.0)

    # Fast forward by N seconds
    await clock.fast_forward(3600.0)

    # Run to a target time (creates an internal task)
    await clock.run_til(1700002000.0)

This is useful for testing strategies where you need to inspect state between ticks.

Processor Management

async with RealtimeClock(config) as clock:
    processor = MyProcessor()

    # Add and remove processors
    clock.add_processor(processor)
    clock.remove_processor(processor)

    # Pause and resume (processor stays registered but skips ticks)
    clock.add_processor(processor)
    clock.pause_processor(processor)
    clock.resume_processor(processor)

    # Query processor state
    active = clock.get_active_processors()
    state = clock.get_processor_state(processor)
    all_states = clock.processor_states  # dict copy

Clock Registry

Use the registry to get a clock class dynamically:

from chronopype.clocks import get_clock_class, ClockMode

clock_class = get_clock_class(ClockMode.BACKTEST)  # returns BacktestClock
clock = clock_class(config)

Shutdown

For graceful shutdown outside the context manager:

await clock.shutdown(timeout=5.0)  # stops all processors with timeout