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);
| Parameter | Type | Units |
|---|---|---|
openSecond | uint32 | |
closeSecond | uint32 | |
rampSeconds | uint32 | seconds |
sessionFee | uint24 | hundredths of a bip (3000 = 0.30%) |
closedFee | uint24 | hundredths of a bip (3000 = 0.30%) |
daysMask | uint8 |
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
| Error | Meaning |
|---|---|
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
| Chain | Chain ID | Address |
|---|---|---|
| unichain | 130 | 0x55cE2fB1636B6E4E70C51689156d0088D48d5080 |
| robinhood | 4663 | 0x66341c30B3b860f0255bF51EEb431C2e92Bdd080 |
| base | 8453 | 0x58b9359042e81aA137972AF2Ff082476dDE09080 |
| arbitrum | 42161 | 0x0bB9D56c24767E5250C5BacE6100C11cb78FD080 |
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