-
Notifications
You must be signed in to change notification settings - Fork 7
Feature: EOD time management module - di.eodtime #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # di.eodtime | ||
|
|
||
| End-of-day time management for TorQ-based tickerplant processes. Resolves the current trading date in a configurable roll timezone, calculates the next EOD roll timestamp in UTC, and computes a daily UTC offset used to timestamp incoming data. | ||
|
|
||
| --- | ||
|
|
||
| ## Dependencies | ||
|
|
||
| **Hard dependency:** `di.tz` - loaded automatically when the module is imported. | ||
|
|
||
| --- | ||
|
|
||
| ## Initialisation | ||
|
|
||
| ```q | ||
| eodtime:use`di.eodtime | ||
| eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000)] | ||
| ``` | ||
|
|
||
| Config keys are all optional. Passing `(::)` or an empty dict `()!()` loads the module with defaults that match TorQ's original `eodtime.q` behaviour. | ||
|
|
||
| | Key | Type | Default | Description | | ||
| |------------------|----------|------------------|-----------------------------------------------------------------------------| | ||
| | `rolltimezone` | symbol | `` `$"GMT" `` | Timezone used for EOD roll scheduling | | ||
| | `datatimezone` | symbol | `` `$"GMT" `` | Timezone used for stamping incoming data | | ||
| | `rolltimeoffset` | timespan | `0D00:00:00.000` | Offset from midnight for the EOD roll (e.g. `0D17:00:00.000` for a 5pm roll) | | ||
|
|
||
| `init` must be called before any other function is used. It computes the initial values of `d`, `nextroll`, and `dailyadj`. | ||
|
|
||
| Timezone values should be standard timezone identifiers in the format `"Region/City"` (e.g. `"Europe/London"`, `"America/New_York"`). The values `"GMT"`, `"UTC"` and `"Etc/GMT"` are handled as special cases returning zero offset directly, without a timezone lookup. `"GMT"` is not recognised by `di.tz` but is the default timezone in TorQ, so this ensures the module works out of the box with existing TorQ products (i.e. backwards compatible). Note: `"Etc/UTC"` is a valid argument in `di.tz`. | ||
|
|
||
| --- | ||
|
|
||
| ## Exported functions | ||
|
|
||
| ### `init` | ||
| Initialises the module with the provided config. Computes initial values of `d`, `nextroll`, and `dailyadj`. | ||
| ```q | ||
| eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000)] | ||
| / or with defaults: | ||
| eodtime.init[::] | ||
| ``` | ||
|
|
||
| ### `getd` | ||
| Returns the current trading date in the roll timezone. | ||
| ```q | ||
| eodtime.getd[] | ||
| / 2025.06.01 | ||
| ``` | ||
|
|
||
| ### `getnextroll` | ||
| Returns the UTC timestamp of the next scheduled EOD roll. | ||
| ```q | ||
| eodtime.getnextroll[] | ||
| / 2025.06.02D00:00:00.000000000 | ||
| ``` | ||
|
|
||
| ### `getdailyadj` | ||
| Returns the **cached** UTC offset for the data timezone - the value stored at the last `init` or `setdailyadj` call. Used by upstream processes to stamp incoming data: | ||
| ```q | ||
| .z.p + eodtime.getdailyadj[] | ||
| / adds the stored offset to the current UTC time | ||
| ``` | ||
|
|
||
| ### `getdailyadjustment` | ||
| **Recomputes** the UTC offset for the data timezone at the current time. Call this after an EOD roll to get a fresh offset - important when the data timezone observes daylight saving time, as the offset changes at the transition. | ||
| ```q | ||
| eodtime.getdailyadjustment[] | ||
| / 0D01:00:00.000000000 | ||
| ``` | ||
|
|
||
| ### `getroll` | ||
| Computes the UTC timestamp of the next EOD roll after UTC timestamp `p`. Returns today's roll time if the roll has not yet passed; returns tomorrow's if it has. | ||
| ```q | ||
| eodtime.getroll[.z.p] | ||
| / 2025.06.02D00:00:00.000000000 | ||
| ``` | ||
|
|
||
| ### `setnextroll` | ||
| Updates the stored next roll timestamp. Called by the tickerplant and segmented tickerplant after completing an EOD roll. | ||
| ```q | ||
| eodtime.setnextroll eodtime.getroll[.z.p] | ||
| ``` | ||
|
|
||
| ### `setdailyadj` | ||
| Updates the stored daily adjustment offset. Called by the tickerplant after an EOD roll to refresh the offset for the new day. | ||
| ```q | ||
| eodtime.setdailyadj eodtime.getdailyadjustment[] | ||
| ``` | ||
|
|
||
| ### `setd` | ||
| Updates the stored trading date. Called by the segmented tickerplant to advance the date after an EOD roll. | ||
| ```q | ||
| eodtime.setd[1+eodtime.getd[]] | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Usage example | ||
|
|
||
| ```q | ||
| / load and initialise for a London-based system rolling at midnight | ||
| eodtime:use`di.eodtime | ||
| eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000)] | ||
|
|
||
| / check current state | ||
| eodtime.getd[] / today's trading date in London time | ||
| eodtime.getnextroll[] / next midnight UTC (23:00 UTC in summer) | ||
| eodtime.getdailyadj[] / current UTC offset for data timestamping | ||
|
|
||
| / simulate what a tickerplant does at EOD roll | ||
| eodtime.setnextroll eodtime.getroll[.z.p]; | ||
| eodtime.setdailyadj eodtime.getdailyadjustment[]; | ||
|
|
||
| / simulate what the segmented tickerplant does at EOD roll | ||
| eodtime.setd[1+eodtime.getd[]] | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## TorQ migration pattern | ||
|
|
||
| | TorQ pattern | Module equivalent | | ||
| |--------------------------------------------------------|------------------------------------------------------| | ||
| | `.eodtime.d` (read) | `eodtime.getd[]` | | ||
| | `.eodtime.nextroll` (read) | `eodtime.getnextroll[]` | | ||
| | `.eodtime.dailyadj` (read) | `eodtime.getdailyadj[]` | | ||
| | `.eodtime.getroll[p]` | `eodtime.getroll[p]` | | ||
| | `.eodtime.getdailyadjustment[]` | `eodtime.getdailyadjustment[]` | | ||
| | `.eodtime.nextroll:.eodtime.getroll[.z.p]` | `eodtime.setnextroll eodtime.getroll[.z.p]` | | ||
| | `.eodtime.dailyadj:.eodtime.getdailyadjustment[]` | `eodtime.setdailyadj eodtime.getdailyadjustment[]` | | ||
| | `.eodtime.d+:1` | `eodtime.setd[1+eodtime.getd[]]` | | ||
|
|
||
| --- | ||
|
|
||
| ## Notes | ||
|
|
||
| - `rolltimeoffset` adjusts the roll time from midnight e.g. `0D17:00:00.000` produces a 5pm local roll. | ||
| - `getdailyadj` returns the cached offset; `getdailyadjustment` recomputes it fresh. Always call `getdailyadjustment` after an EOD roll rather than relying on the cached value. | ||
| - `"GMT"`, `"UTC"` and `"Etc/GMT"` are not in the timezone database used by `di.tz` and are handled as zero-offset shortcuts directly in this module. `"Etc/UTC"` is valid and passed through to `di.tz` normally. All other timezone values are passed through to `di.tz`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| / end-of-day time management - date resolution, roll scheduling and data timestamp adjustment | ||
|
|
||
| / utc-equivalent timezone names - zero offset from utc so no timezone lookup required | ||
| utczones:`$("GMT";"UTC";"Etc/GMT"); | ||
|
|
||
| / ============================================================ | ||
| / module state and defaults | ||
| / ============================================================ | ||
|
|
||
| / roll timezone for eod scheduling - overwritten by init | ||
| rolltimezone:`$"GMT"; | ||
|
|
||
| / data timezone for incoming data timestamping - overwritten by init | ||
| datatimezone:`$"GMT"; | ||
|
|
||
| / offset from midnight for the eod roll - overwritten by init | ||
| rolltimeoffset:0D00:00:00.000; | ||
|
|
||
| / current trading date in rolltimezone - set by init | ||
| d:0Nd; | ||
|
|
||
| / utc timestamp of next eod roll - set by init | ||
| nextroll:0Wp; | ||
|
|
||
| / utc offset for datatimezone data stamping - set by init | ||
| dailyadj:0D00:00:00.000; | ||
|
|
||
| / ============================================================ | ||
| / internal helpers | ||
| / ============================================================ | ||
|
|
||
| / returns timespan offset from utc for rolltimezone at timestamp p | ||
| adjtime:{[p] | ||
| / utc-equivalent timezones have zero offset | ||
| if[rolltimezone in utczones;:0D]; | ||
| `timespan$tz.gmttolocal[rolltimezone;p]-p | ||
| }; | ||
|
|
||
| / returns timespan offset from utc for datatimezone at current time | ||
| getdailyadjustment:{ | ||
| / utc-equivalent timezones have zero offset | ||
| if[datatimezone in utczones;:0D]; | ||
| `timespan$tz.gmttolocal[datatimezone;.z.p]-.z.p | ||
| }; | ||
|
|
||
| / returns the date in rolltimezone for utc timestamp p | ||
| getday:{[p]"d"$(p+adjtime[p])-rolltimeoffset}; | ||
|
|
||
| / returns utc timestamp of next eod roll after utc timestamp p | ||
| getroll:{[p] | ||
| / mod[z,1D] normalises the roll time to [0D,1D) to handle timezone offsets crossing midnight | ||
| z:rolltimeoffset-adjtime[p]; | ||
| z:`timespan$(mod) . "j"$z,1D; | ||
| / kdb-x 5.0: comparing timespan to timestamp checks against p's time-of-day component - true means roll has already passed | ||
| ("d"$p)+$[z<=p;z+1D;z] | ||
| }; | ||
|
|
||
| / ============================================================ | ||
| / public api | ||
| / ============================================================ | ||
|
|
||
| / state getters | ||
| getd:{d}; | ||
| getnextroll:{nextroll}; | ||
| getdailyadj:{dailyadj}; | ||
|
|
||
| / state setters | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since these variables are dependent on each other would it be safer if there was just one setter function that updated them all together? E.g. if we had then after EOD, a TP would just need to call I know TorQ tickerplant.q sets them separately, but now that we have to do it via explicit setter functions, exposing three different setter functions might give the impression that they can be called independently. |
||
| setnextroll:{.z.m.nextroll:x}; | ||
| setdailyadj:{.z.m.dailyadj:x}; | ||
| setd:{.z.m.d:x}; | ||
|
|
||
| / initialise module with config dictionary | ||
| init:{[config] | ||
| / normalise config to dict and apply keys, falling back to defaults where not provided | ||
| cfg:$[99h=type config;config;()!()]; | ||
| .z.m.rolltimezone:$[`rolltimezone in key cfg;cfg`rolltimezone;`$"GMT"]; | ||
| .z.m.datatimezone:$[`datatimezone in key cfg;cfg`datatimezone;`$"GMT"]; | ||
| .z.m.rolltimeoffset:$[`rolltimeoffset in key cfg;cfg`rolltimeoffset;0D00:00:00.000]; | ||
| .z.m.dailyadj:getdailyadjustment[]; | ||
| .z.m.d:getday[.z.p]; | ||
| .z.m.nextroll:getroll[.z.p]; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| / end-of-day time management - date resolution, roll scheduling and data timestamp adjustment | ||
|
|
||
| tz:use`di.tz | ||
|
|
||
| \l ::eodtime.q | ||
|
|
||
| export:([init;getd;getnextroll;getdailyadj;getroll;getdailyadjustment;setnextroll;setdailyadj;setd]) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| action,ms,bytes,lang,code,repeat,minver,comment | ||
| / Pre-test setup: load module and initialise with GMT defaults | ||
| before,0,0,q,eodtime:use`di.eodtime,1,1,load di.eodtime module | ||
| before,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"GMT";`$"GMT";0D00:00:00.000)],1,1,init with gmt defaults | ||
|
|
||
| / Test 1: getd | ||
| true,0,0,q,-14h=type eodtime.getd[],1,1,getd returns date type | ||
|
|
||
| / Test 2: getnextroll | ||
| true,0,0,q,-12h=type eodtime.getnextroll[],1,1,getnextroll returns timestamp type | ||
| true,0,0,q,eodtime.getnextroll[]>.z.p,1,1,getnextroll returns future timestamp | ||
|
|
||
| / Test 3: getdailyadj | ||
| true,0,0,q,-16h=type eodtime.getdailyadj[],1,1,getdailyadj returns timespan type | ||
| true,0,0,q,0D=eodtime.getdailyadj[],1,1,getdailyadj returns 0D for gmt datatimezone | ||
|
|
||
| / Test 4: getdailyadjustment | ||
| true,0,0,q,-16h=type eodtime.getdailyadjustment[],1,1,getdailyadjustment returns timespan type | ||
| true,0,0,q,0D=eodtime.getdailyadjustment[],1,1,getdailyadjustment returns 0D for gmt datatimezone | ||
|
|
||
| / Test 5: getroll - gmt zero offset | ||
| true,0,0,q,-12h=type eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,getroll returns timestamp type | ||
| true,0,0,q,2025.01.02D00:00:00.000000000=eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,getroll returns next midnight for gmt zero offset | ||
|
|
||
| / Test 6: getroll - gmt with rolltimeoffset | ||
| run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"GMT";`$"GMT";0D17:00:00.000)],1,1,reinit with 5pm gmt roll time | ||
| true,0,0,q,2025.01.01D17:00:00.000000000=eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,getroll returns todays 5pm when before roll time | ||
| true,0,0,q,2025.01.02D17:00:00.000000000=eodtime.getroll[2025.01.01D18:00:00.000000000],1,1,getroll returns next day 5pm when past roll time | ||
|
|
||
| / Test 7: setnextroll | ||
| run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"GMT";`$"GMT";0D00:00:00.000)],1,1,reinit with gmt defaults | ||
| run,0,0,q,eodtime.setnextroll[2025.06.01D00:00:00.000000000],1,1,setnextroll does not error | ||
| true,0,0,q,2025.06.01D00:00:00.000000000=eodtime.getnextroll[],1,1,getnextroll reflects setnextroll | ||
|
|
||
| / Test 8: setdailyadj | ||
| run,0,0,q,eodtime.setdailyadj[0D01:00:00.000000000],1,1,setdailyadj does not error | ||
| true,0,0,q,0D01:00:00.000000000=eodtime.getdailyadj[],1,1,getdailyadj reflects setdailyadj | ||
|
|
||
| / Test 9: setd | ||
| run,0,0,q,eodtime.setd[2025.06.01],1,1,setd does not error | ||
| true,0,0,q,2025.06.01=eodtime.getd[],1,1,getd reflects setd | ||
|
|
||
| / Test 10: utc-equivalent timezone shortcuts (GMT/UTC/Etc/GMT) | ||
| run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"UTC";`$"Etc/GMT";0D00:00:00.000)],1,1,reinit with UTC/Etc/GMT shortcuts | ||
| true,0,0,q,0D=eodtime.getdailyadjustment[],1,1,Etc/GMT datatimezone returns 0D without lookup | ||
| true,0,0,q,2025.01.02D00:00:00.000000000=eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,UTC rolltimezone returns correct roll | ||
|
|
||
| / Test 11: non-gmt rolltimezone (Europe/London winter - UTC+0) | ||
| run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000)],1,1,reinit with London rolltimezone | ||
| true,0,0,q,-12h=type eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,getroll returns timestamp type for non-gmt rolltimezone | ||
| true,0,0,q,2025.01.02D00:00:00.000000000=eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,getroll correct for london winter (utc+0) | ||
| true,0,0,q,2025.07.01D23:00:00.000000000=eodtime.getroll[2025.07.01D12:00:00.000000000],1,1,getroll correct for london summer (utc+1 - midnight local is 23:00 utc) | ||
|
|
||
| / Test 12: non-gmt datatimezone (Europe/London - DST aware) | ||
| run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"GMT";`$"Europe/London";0D00:00:00.000)],1,1,reinit with London datatimezone | ||
| true,0,0,q,-16h=type eodtime.getdailyadjustment[],1,1,Europe/London datatimezone returns timespan type | ||
| true,0,0,q,0D<=eodtime.getdailyadjustment[],1,1,Europe/London datatimezone returns non-negative offset | ||
|
|
||
| / Test 13: init with empty dict applies all defaults | ||
| run,0,0,q,eodtime.init[()!()],1,1,init with empty config applies all defaults | ||
| true,0,0,q,0D=eodtime.getdailyadj[],1,1,default datatimezone (GMT) produces zero daily adjustment | ||
| true,0,0,q,0D=eodtime.getdailyadjustment[],1,1,default datatimezone (GMT) produces zero computed adjustment | ||
| true,0,0,q,2025.01.02D00:00:00.000000000=eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,default rolltimeoffset (0D) gives midnight roll | ||
|
|
||
| / Test 14: init with :: applies all defaults | ||
| run,0,0,q,eodtime.init[::],1,1,init with :: applies defaults without error | ||
| true,0,0,q,0D=eodtime.getdailyadj[],1,1,:: init produces zero daily adjustment | ||
| true,0,0,q,2025.01.02D00:00:00.000000000=eodtime.getroll[2025.01.01D12:00:00.000000000],1,1,:: init default rolltimeoffset gives midnight roll | ||
|
|
||
| / Test 15: error cases | ||
| fail,0,0,q,eodtime.getroll[`notvalid],1,1,getroll throws on non-timestamp input |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of these are in the export dict so should be moved to the "public api" section