Skip to content

Schedules and timeframes

A Python strategy can check each bar, gate a rebalance to a period, or read completed bars at another timeframe.

Gate a rebalance

should_rebalance("monthly") is a daily/weekly/monthly gate for on_tick. It uses bar timestamps and fires on the first observed bar of a new period. Weekends and holidays can move that first observation.

Schedule a target portfolio

from atom.library.clock_decorators import recompute
from atom.library.book import target

universe = ["TCS", "INFY"]

@recompute("monthly")
def book():
    return target(weights={"TCS": 0.25, "INFY": 0.25})

Supported period forms include daily, weekly, monthly, twice-monthly, and every-N-bar scheduling. A period clock also fires on the first bar. It describes observed calendar boundaries, not a guarantee of an exchange opening-time fill.

Read another timeframe

def on_tick():
    daily_close = security("TCS", "1d", field="close", n=30)
    if len(daily_close) >= 20:
        daily_average = mean(daily_close, 20)
        if position.is_flat and close[0] > daily_average:
            long(10)

security exposes completed higher-timeframe bars. A forming daily candle is not visible in an intraday decision. Use the timeframe identifiers accepted by your data source, such as 1m, 1h, 1d, 1w or 1mo. Call market-data vocabulary inside strategy functions, not in module-level calculations.

This example requires real 1d observations from the environment. In the local CLI, use a daily CSV carrying timeframe=1d as described in price data; the default synthetic minute stream cannot satisfy it.

Event-driven rules require an event feed explicitly supplied by the environment. A function decorator alone does not create earnings or other event data.

Pick the clock that matches your intent

Form Intended entry style When it is due
on_tick() Per-bar conditions. Each dispatched bar.
should_rebalance("monthly") A gate inside on_tick(). First observed bar in a new month.
@recompute("weekly") A scheduled book() or supported entry point. First observed bar of a new week.
@recompute("twice-monthly") A target book. First observed monthly bar and first observed bar on/after the 15th.
@recompute("every_n_bars=5") A target book. First bar, then every fifth dispatched bar.

A count of five bars means five observations at the configured interval, not five calendar days. Gates need not fire on an exchange opening auction; missing weekends/holidays shift the first observed bar.

should_rebalance is stateful. Calling the same gate twice in one bar can consume the event in the first call; calculate it once if several actions should share that decision.

Read the security arguments

security(symbol, timeframe, field="close", n=500) requests a series for the exact instrument and recognised timeframe. field can be open, high, low, close or volume. n is a positive history limit, not an indicator period. Choose enough history for the intended calculation, and check the returned length.

The newest completed requested-timeframe value is [0]. During an intraday decision, today's forming daily candle is omitted. This prevents treating information from an unfinished higher-timeframe period as already known. Supported examples include 1m, 1h, 1d, 1w and 1mo. The original identifier is passed to the provider unchanged; completion understands daily/weekly/monthly aliases, but this does not make a provider accept TradingView aliases. A bare 60 is unsupported; use 60m.

Positive integer minute/hour intervals use start labels and become visible only after their full duration. A 90m observation starting at 09:00 is hidden at 10:00 and visible at 10:30. A 1h observation starting at 09:30 is also hidden until 10:30; the wall-clock hour boundary at 10:00 does not close it. Daily, weekly and monthly reads retain the calendar-boundary convention: current-day, current-week and current-month observations are withheld. Exchange-specific publication delays remain the data provider's responsibility.

Use positive minute/hour forms or 1d, 1w, 1mo. Unsupported completion rules such as 2d, and zero intervals, raise an error rather than exposing an unfinished value. The provider must supply the requested resolution. Minute observations cannot stand in for daily data.

Supply timing before reading

Every security read requires a current decision timestamp, including len, [N], .df and indicator dataframe access. Source timestamps must be datetime values without nulls. Use timezone-aware UTC instants for both sources and decisions; other explicit timezones compare by instant. Existing pairs of naive datetimes remain comparable, but mixing naive and aware timestamps is rejected. Strings, dates without times and numeric epoch values must be converted to datetimes by the data source before reading.

Missing or invalid timing raises TS-TF-NO-TIMESTAMP with the message “Cannot read SPY@1h: usable source timestamps and decision time are required.” The symbol and timeframe identify the failing read. This is an essential error: even a non-strict backtest fails. Supply the decision clock and repair source timestamps instead of treating the error as warmup. A host managing existing positions must block new decisions and continue its position-management duties. For an AsyncLiveEngine host, supply ongoing stops, cancellation and owned reductions through manage(bar, params, state) separately from the decision callback on_tick. Both hooks may be synchronous or async. Management runs before decisions on each tick and keeps running after a timing failure, as do mark-to-market and risk checks. The host logs the error and retains it as engine.decision_error and in the returned result. Decisions stay blocked for that engine instance even if later history has valid timestamps; repair timing and reconcile holdings before starting a fresh instance. Existing hosts with management inside on_tick must move it to manage so it remains active.

For example, if one unit is already held with a stop at 95, a timing failure at 10:30 must not end management. A low of 94 at 10:32 still triggers the registered protective exit through the host's management hook. No new entry decision runs, and submitting that exit does not itself prove a fill: the position stays held until the host observes and reconciles the fill.

With a valid decision clock, no returned history or an empty datetime-stamped frame gives length zero; indexing raises IndexError. An empty frame without a datetime timestamp column is invalid. Cached history still requires a valid clock on every read. Previously, missing timing could expose unfiltered bars; inspection callers must now supply the same timing evidence as strategies.

For example, suppose hourly observations start at 09:00, 10:00 and 11:00 UTC, with closes 100, 110 and 999. At 10:30, security("SPY", "1h")[0] is 100; at 11:00 it is 110. The future 999 cannot affect either decision:

def on_tick():
    hourly = security("SPY", "1h")
    if len(hourly) and position.is_flat and close[0] > hourly[0]:
        long(1)

On a SPY execution stream with a 10:30 close of 105, this rule requests one unit because 105 > 100. With the default next-open fill timing, a next raw open of 106 fills at 106 before any configured costs. Missing timing stops the read before this rule can request an entry. Calendar completion conventions above are unchanged; this timing check does not establish exchange-session completion.

Events require an actual feed

Import on_event from atom.library.clock_decorators before using @on_event("earnings"). It schedules a supported entry point for supplied earnings events and needs the external feed wired to the run; a missing feed can prevent setup. This differs from a timestamp gate. Start with bar/calendar scheduling unless the environment explicitly supplies the event contract.