HookForge

Hooks / Time

TradingCalendar

Gives a pool a trading session, and closes it by raising the price of immediacy rather than by reverting.

TradingCalendar implements 2 of the fourteen Uniswap v4 callbacks: afterInitialize, beforeSwap.

How it works

Some assets should not be quoted around the clock at the same spread. A pool whose reference market is open for part of the day is being priced blind the rest of the time, and the liquidity sitting in it overnight is providing a free option to anyone with better information about where the asset will open. The obvious hook for this reverts outside session hours, and several published ones do exactly that.

Reverting is the wrong instrument. A pool that reverts is a pool that every router, aggregator and quoting service must special case; it fails after the user has signed; it strands liquidity providers who wanted to exit; and it converts a pricing problem into a liveness problem. Worse, it does not stop informed flow at all, it just moves it to the first second after the open, where it hits the same liquidity at the same stale price.

This hook keeps the pool open and quotable at every instant and expresses the session in the fee instead: - Inside the session, swaps pay `sessionFee`. - Outside it, swaps pay `closedFee`, which is meant to be punitive rather than prohibitive. - Across `rampSeconds` on either side of each boundary the fee moves linearly between the two, so the open and the close are gradients rather than cliffs and there is no single block worth racing to.

The ramp is the part that matters. A cliff at the open creates a race: the first swap after the boundary captures the whole overnight gap at the session spread. A ramp means the trader who wants that gap must choose between paying for it early and waiting for a lower fee while the price moves against them, which is precisely the tradeoff that makes the gap get closed gradually and by more than one participant.

Sessions are expressed in UTC seconds-of-day and may wrap midnight (`open > close` describes an overnight session). `daysMask` selects the days of the week the session runs, bit 0 being Monday. A pool with `daysMask` covering all seven days and a 24-hour session is always in session, which is a valid way to disable the calendar.

Prior art: "New York Trading Hours" and "Trading Hours" hooks revert outside a window. Continuous fee ramps around scheduled events appear in the `UniCast` design for known catalysts. Expressing a *recurring weekly calendar* as a continuous fee surface, with no revert path and no oracle, is the contribution here.

Prior art

The published calendar hooks ("New York Trading Hours", "Trading Hours") revert outside a window. Continuous fee ramps around a single scheduled event appear in the UniCast design. Expressing a recurring weekly calendar as a continuous fee surface, with no revert path and no oracle, is the contribution here.

Where it does not help

The calendar is a fixed weekly pattern in UTC. It does not know about holidays, half days, or daylight-saving shifts in the reference market, so a pool tracking an asset with an irregular schedule has to pick a session that is correct most weeks and accept that it is wrong on the exceptions.

Configuring a pool

Uniswap v4 removed hookData from initialize, so a hook that needs per-pool parameters has to receive them out of band. TradingCalendar takes them through configure, which anyone may call for a pool key whose pool does not exist yet, and which nobody may call afterwards. The parameters are part of what the pool is, so they are fixed for its lifetime.

// 1. Fix the terms, before the pool exists.
hook.configure(
    key,
    TradingCalendarHook.Config({
        openSecond: /* uint32 */ 0,
        closeSecond: /* uint32 */ 0,
        rampSeconds: /* uint32 */ 0,
        sessionFee: /* uint24 */ 0,
        closedFee: /* uint24 */ 0,
        daysMask: /* uint8 */ 0
    })
);

// 2. Initialize the pool. The hook rejects a pool it was never configured for.
poolManager.initialize(key, startingSqrtPriceX96);
ParameterTypeUnits
openSeconduint32
closeSeconduint32
rampSecondsuint32seconds
sessionFeeuint24hundredths of a bip (3000 = 0.30%)
closedFeeuint24hundredths of a bip (3000 = 0.30%)
daysMaskuint8

From TypeScript

The SDK ships the catalogue, the address book and the pool-key helpers, so a client never hardcodes an address or recomputes a pool id by hand.

npm i @hookforge/sdk

import {getHook, hookAddress, poolKeyFor, poolId} from "@hookforge/sdk";

const hook = getHook("trading-calendar");
const address = hookAddress("trading-calendar", 1);
const key = poolKeyFor({hook: address, currencyA: USDC, currencyB: WETH, tickSpacing: 60, dynamicFee: true});
console.log(poolId(key));

What it reverts with

ErrorMeaning
ClosedFeeBelowSessionFee()`closedFee` must be at least `sessionFee`; a session that is cheaper when closed is a configuration error.
FeeTooLarge(uint24)A fee was configured above the protocol maximum of 100%.
InvalidSecondOfDay()A seconds-of-day field was at or above 86400.
NoTradingDays()`daysMask` selected no days, which would close the pool permanently.
NotDynamicFee()The hook was attempted to be initialized with a non-dynamic fee.
PoolAlreadyInitialized()The pool already exists, so its configuration is final.
PoolNotConfigured()The pool was initialized without a configuration for this hook.
RampTooLong()The ramp cannot be longer than the session or the gap between sessions it has to fit inside.

Addresses

ChainChain IDAddress
unichain1300x55cE2fB1636B6E4E70C51689156d0088D48d5080
robinhood46630x66341c30B3b860f0255bF51EEb431C2e92Bdd080
base84530x58b9359042e81aA137972AF2Ff082476dDE09080
arbitrum421610x0bB9D56c24767E5250C5BacE6100C11cb78FD080

These addresses are deterministic, not live. They are the CREATE2 addresses the deploy script mines so that the low fourteen bits encode this hook's permissions. Until the deploy runs on a chain, there is no code at them. Check before you send anything anywhere.

Source and verification

The contract is contracts/src/hooks/TradingCalendarHook.sol, and everything on this page is generated from it: the prose is its NatSpec, the parameters are its configure ABI, the callbacks above are the flags it declares, and the tags are the strings its own hookTags() returns. A hook cannot be documented here as something it is not.

Ask the deployed contract what it is and it answers directly, with no registry in the loop:

cast call $HOOK "hookName()(string)"    # TradingCalendar
cast call $HOOK "specURI()(string)"     # https://hookforge.pages.dev/schema/hooks/trading-calendar.json
cast call $HOOK "hookTags()(string[])"  # dynamic-fee, schedule, rwa, oracle-free