From 65516d1e530dbc9b87ae5ced9256226668d3d660 Mon Sep 17 00:00:00 2001 From: alowrydi Date: Tue, 2 Jun 2026 15:00:44 +0100 Subject: [PATCH 1/7] di.eodtime module - code complete, docs and tests in progress --- di/eodtime/eodtime.q | 81 ++++++++++++++++++++++++++++++++++++++++++++ di/eodtime/init.q | 7 ++++ 2 files changed, 88 insertions(+) create mode 100644 di/eodtime/eodtime.q create mode 100644 di/eodtime/init.q diff --git a/di/eodtime/eodtime.q b/di/eodtime/eodtime.q new file mode 100644 index 00000000..2c5512a2 --- /dev/null +++ b/di/eodtime/eodtime.q @@ -0,0 +1,81 @@ +/ 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";"Etc/UTC"); + +/ ============================================================ +/ 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; + ("d"$p)+$[z<=p;z+1D;z] + }; + +/ ============================================================ +/ public api +/ ============================================================ + +/ state getters +getd:{d}; +getnextroll:{nextroll}; +getdailyadj:{dailyadj}; + +/ state setters +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;rolltimezone]; + .z.m.datatimezone:$[`datatimezone in key cfg;cfg`datatimezone;datatimezone]; + .z.m.rolltimeoffset:$[`rolltimeoffset in key cfg;cfg`rolltimeoffset;rolltimeoffset]; + .z.m.dailyadj:getdailyadjustment[]; + .z.m.d:getday[.z.p]; + .z.m.nextroll:getroll[.z.p]; + }; diff --git a/di/eodtime/init.q b/di/eodtime/init.q new file mode 100644 index 00000000..e60c20bd --- /dev/null +++ b/di/eodtime/init.q @@ -0,0 +1,7 @@ +/ end-of-day time management - date resolution, roll scheduling and data timestamp adjustment + +tz:use`di.timezone + +\l ::eodtime.q + +export:([init;getd;getnextroll;getdailyadj;getroll;getdailyadjustment;setnextroll;setdailyadj;setd]) From 3538ed646c3a599bc813c1bb2326a8277dbc771f Mon Sep 17 00:00:00 2001 From: alowrydi Date: Tue, 2 Jun 2026 16:30:23 +0100 Subject: [PATCH 2/7] add docs, update init to di.tz, fix utczones --- di/eodtime/eodtime.md | 140 ++++++++++++++++++++++++++++++++++++++++++ di/eodtime/eodtime.q | 2 +- di/eodtime/init.q | 2 +- 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 di/eodtime/eodtime.md diff --git a/di/eodtime/eodtime.md b/di/eodtime/eodtime.md new file mode 100644 index 00000000..f03c12a4 --- /dev/null +++ b/di/eodtime/eodtime.md @@ -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`. diff --git a/di/eodtime/eodtime.q b/di/eodtime/eodtime.q index 2c5512a2..3d4baff3 100644 --- a/di/eodtime/eodtime.q +++ b/di/eodtime/eodtime.q @@ -1,7 +1,7 @@ / 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";"Etc/UTC"); +utczones:`$("GMT";"UTC";"Etc/GMT"); / ============================================================ / module state and defaults diff --git a/di/eodtime/init.q b/di/eodtime/init.q index e60c20bd..027c0e12 100644 --- a/di/eodtime/init.q +++ b/di/eodtime/init.q @@ -1,6 +1,6 @@ / end-of-day time management - date resolution, roll scheduling and data timestamp adjustment -tz:use`di.timezone +tz:use`di.tz \l ::eodtime.q From 79b5400ecb520b04a154c884e360d72d2511ff86 Mon Sep 17 00:00:00 2001 From: alowrydi Date: Tue, 2 Jun 2026 17:38:33 +0100 Subject: [PATCH 3/7] add test.csv - 37 tests passing, fix init defaults bug --- di/eodtime/eodtime.q | 7 +++-- di/eodtime/test.csv | 71 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 di/eodtime/test.csv diff --git a/di/eodtime/eodtime.q b/di/eodtime/eodtime.q index 3d4baff3..f29fc391 100644 --- a/di/eodtime/eodtime.q +++ b/di/eodtime/eodtime.q @@ -51,6 +51,7 @@ 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] }; @@ -72,9 +73,9 @@ setd:{.z.m.d:x}; 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;rolltimezone]; - .z.m.datatimezone:$[`datatimezone in key cfg;cfg`datatimezone;datatimezone]; - .z.m.rolltimeoffset:$[`rolltimeoffset in key cfg;cfg`rolltimeoffset;rolltimeoffset]; + .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]; diff --git a/di/eodtime/test.csv b/di/eodtime/test.csv new file mode 100644 index 00000000..f4b7fb49 --- /dev/null +++ b/di/eodtime/test.csv @@ -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 From d5874bd5b1b4f48cd00c3422ad3e5d5c9de5eb63 Mon Sep 17 00:00:00 2001 From: alowrydi Date: Wed, 24 Jun 2026 08:12:06 +0100 Subject: [PATCH 4/7] refactoring to discussed standards from review call: optional kx log dep, init confirmation message, feedback alignment --- di/eodtime/eodtime.md | 39 ++++++++++++++++++---- di/eodtime/eodtime.q | 41 ++++++++++++++++------- di/eodtime/test.csv | 77 +++++++++++++++++++++---------------------- 3 files changed, 100 insertions(+), 57 deletions(-) diff --git a/di/eodtime/eodtime.md b/di/eodtime/eodtime.md index f03c12a4..7aa30d4c 100644 --- a/di/eodtime/eodtime.md +++ b/di/eodtime/eodtime.md @@ -6,7 +6,31 @@ End-of-day time management for TorQ-based tickerplant processes. Resolves the cu ## Dependencies -**Hard dependency:** `di.tz` - loaded automatically when the module is imported. +| Dependency | Key | Required | Description | +|---|---|---|---| +| logger | `log` | no | `info`, `warn`, `error` — each monadic `{[msg] ...}`. Falls back to a silent no-op if absent or malformed | + +**Hard dependency:** `di.tz` — loaded automatically when the module is imported. + +A `kx.log` instance can be passed directly as the second argument to `init` — no manual wrapping required: + +```q +kxlog:use`kx.log +eodtime:use`di.eodtime +eodtime.init[config;kxlog.createLog[]] +``` + +Alternatively, pass a deps dict with a `log` key: + +```q +eodtime.init[config;enlist[`log]!enlist kxlog.createLog[]] +``` + +If no logger is needed, pass an empty dict or omit it entirely — the module will run silently: + +```q +eodtime.init[config;()!()] +``` --- @@ -14,7 +38,7 @@ End-of-day time management for TorQ-based tickerplant processes. Resolves the cu ```q eodtime:use`di.eodtime -eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000)] +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. @@ -34,11 +58,11 @@ Timezone values should be standard timezone identifiers in the format `"Region/C ## Exported functions ### `init` -Initialises the module with the provided config. Computes initial values of `d`, `nextroll`, and `dailyadj`. +Initialises the module with the provided config and optional log dependency. 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[::] +eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000);kxlog.createLog[]] +/ or with defaults and no logger: +eodtime.init[::;()!()] ``` ### `getd` @@ -100,8 +124,9 @@ eodtime.setd[1+eodtime.getd[]] ```q / load and initialise for a London-based system rolling at midnight +kxlog:use`kx.log eodtime:use`di.eodtime -eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000)] +eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000);kxlog.createLog[]] / check current state eodtime.getd[] / today's trading date in London time diff --git a/di/eodtime/eodtime.q b/di/eodtime/eodtime.q index f29fc391..cb0f0eca 100644 --- a/di/eodtime/eodtime.q +++ b/di/eodtime/eodtime.q @@ -3,6 +3,9 @@ / utc-equivalent timezone names - zero offset from utc so no timezone lookup required utczones:`$("GMT";"UTC";"Etc/GMT"); +/ default no-op logger - used when no log dep is injected +defaultlog:`info`warn`error!({[m]};{[m]};{[m]}); + / ============================================================ / module state and defaults / ============================================================ @@ -29,6 +32,14 @@ dailyadj:0D00:00:00.000; / internal helpers / ============================================================ +setdeps:{[deps] + / accept a bare kx.log instance (info/warn/error at top level) or a full deps dict keyed on `log + / if absent or malformed falls back to no-op logger silently - log dep is optional for this module + dv:$[99h=type deps;$[not `log in key deps;enlist[`log]!enlist deps;deps];deps]; + logval:$[99h=type dv;$[`log in key dv;dv`log;defaultlog];defaultlog]; + .z.m.log:$[99h=type logval;$[all `info`warn`error in key logval;logval;defaultlog];defaultlog]; + }; + / returns timespan offset from utc for rolltimezone at timestamp p adjtime:{[p] / utc-equivalent timezones have zero offset @@ -36,16 +47,21 @@ adjtime:{[p] `timespan$tz.gmttolocal[rolltimezone;p]-p }; -/ returns timespan offset from utc for datatimezone at current time +/ returns the date in rolltimezone for utc timestamp p +getday:{[p]"d"$(p+adjtime[p])-rolltimeoffset}; + +/ ============================================================ +/ public api +/ ============================================================ + +/ recomputes utc offset for datatimezone at current time +/ call after an eod roll to get a fresh offset - important when datatimezone observes dst 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 @@ -55,10 +71,6 @@ getroll:{[p] ("d"$p)+$[z<=p;z+1D;z] }; -/ ============================================================ -/ public api -/ ============================================================ - / state getters getd:{d}; getnextroll:{nextroll}; @@ -69,9 +81,15 @@ 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 +/ initialise module with config dictionary and optional log dependency +init:{[config;deps] + / config: dict with optional keys `rolltimezone`datatimezone`rolltimeoffset + / pass (::) or ()!() to use all defaults matching torq eodtime.q behaviour + / deps: optional - kx.log instance or dict with `log -> `info`warn`error!(infofn;warnfn;errfn) + / if absent or malformed falls back to no-op logging silently + / example: + / eodtime.init[`rolltimezone!enlist`$"Europe/London";kxlog.createLog[]] + setdeps deps; 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"]; @@ -79,4 +97,5 @@ init:{[config] .z.m.dailyadj:getdailyadjustment[]; .z.m.d:getday[.z.p]; .z.m.nextroll:getroll[.z.p]; + .z.m.log[`info]["eodtime: initialised with rolltimezone=",string[rolltimezone]," datatimezone=",string[datatimezone]," rolltimeoffset=",string rolltimeoffset]; }; diff --git a/di/eodtime/test.csv b/di/eodtime/test.csv index f4b7fb49..dd9545b8 100644 --- a/di/eodtime/test.csv +++ b/di/eodtime/test.csv @@ -1,71 +1,70 @@ action,ms,bytes,lang,code,repeat,minver,comment -/ Pre-test setup: load module and initialise with GMT defaults +comment,,,,,,,setup - load module and initialise with gmt defaults and no log dep 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 +before,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"GMT";`$"GMT";0D00:00:00.000);()!()],1,1,init with gmt defaults and no log dep +comment,,,,,,,getd true,0,0,q,-14h=type eodtime.getd[],1,1,getd returns date type - -/ Test 2: getnextroll +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,non-gmt rolltimezone (Europe/London) +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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,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 +comment,,,,,,,error cases fail,0,0,q,eodtime.getroll[`notvalid],1,1,getroll throws on non-timestamp input +comment,,,,,,,log dep - optional no-op fallback +run,0,0,q,eodtime.init[()!();()!()],1,1,init with no log dep does not error +run,0,0,q,eodtime.init[()!();42],1,1,non-dict deps falls back gracefully without error +run,0,0,q,eodtime.init[()!();enlist[`notlog]!enlist 42],1,1,missing log key in deps falls back gracefully +comment,,,,,,,log dep - bare kx.log instance accepted directly +run,0,0,q,kxlogger:use`kx.log,1,1,load kx.log +run,0,0,q,kxinst:kxlogger.createLog[],1,1,create kx.log instance +run,0,0,q,eodtime.init[()!();kxinst],1,1,bare kx.log instance accepted directly as deps +comment,,,,,,,log dep - init message captured when logger provided +run,0,0,q,caplog:`info`warn`error!({[m] `.test.cap upsert(`info;m)};{[m] `.test.cap upsert(`warn;m)};{[m] `.test.cap upsert(`error;m)}),1,1,capturing log mock +run,0,0,q,.test.cap:([] fn:`symbol$();msg:()),1,1,initialise log capture table +run,0,0,q,eodtime.init[()!();enlist[`log]!enlist caplog],1,1,init with capturing logger +true,0,0,q,1=count select from .test.cap where fn=`info,1,1,one info entry captured on init +true,0,0,q,any (.test.cap`msg) like "eodtime: *",1,1,log message carries eodtime context prefix From bb637dec66c1ef71560995fb0d91622951ea2585 Mon Sep 17 00:00:00 2001 From: alowrydi Date: Wed, 24 Jun 2026 14:10:12 +0100 Subject: [PATCH 5/7] apply TorQ-modularisation skill to polish up binary logger, single-dict init, required log dep, kx.log normalisation --- di/eodtime/eodtime.md | 60 ++++++++++++++++++++----------------------- di/eodtime/eodtime.q | 53 +++++++++++++++++++++----------------- di/eodtime/test.csv | 48 +++++++++++++++++----------------- 3 files changed, 83 insertions(+), 78 deletions(-) diff --git a/di/eodtime/eodtime.md b/di/eodtime/eodtime.md index 7aa30d4c..c2b1f207 100644 --- a/di/eodtime/eodtime.md +++ b/di/eodtime/eodtime.md @@ -8,61 +8,56 @@ End-of-day time management for TorQ-based tickerplant processes. Resolves the cu | Dependency | Key | Required | Description | |---|---|---|---| -| logger | `log` | no | `info`, `warn`, `error` — each monadic `{[msg] ...}`. Falls back to a silent no-op if absent or malformed | +| logger | `log` | yes | `info`, `warn`, `error` — each binary `{[c;m]}` where `c` is a symbol context and `m` is a string | **Hard dependency:** `di.tz` — loaded automatically when the module is imported. -A `kx.log` instance can be passed directly as the second argument to `init` — no manual wrapping required: +The `log` dependency must be passed to `init` inside the `configs` dict. The module +throws immediately if it is absent or missing any of the three required keys. + +A `kx.log` instance can be passed directly — the module normalises monadic functions +to the binary `{[c;m]}` contract automatically: ```q kxlog:use`kx.log eodtime:use`di.eodtime -eodtime.init[config;kxlog.createLog[]] +eodtime.init[`log`rolltimezone!(kxlog.createLog[];`$"Europe/London")] ``` -Alternatively, pass a deps dict with a `log` key: - -```q -eodtime.init[config;enlist[`log]!enlist kxlog.createLog[]] -``` +--- -If no logger is needed, pass an empty dict or omit it entirely — the module will run silently: +## Initialisation -```q -eodtime.init[config;()!()] -``` +`init[configs]` takes a single dictionary combining the `log` dependency with +any configuration overrides. ---- +| Key | Required | Description | +|---|---|---| +| `log` | yes | Binary log dep — `info`, `warn`, `error` functions each `{[c;m]}` | +| `rolltimezone` | no | Timezone used for EOD roll scheduling. Default: `` `$"GMT" `` | +| `datatimezone` | no | Timezone used for stamping incoming data. Default: `` `$"GMT" `` | +| `rolltimeoffset` | no | Offset from midnight for the EOD roll (e.g. `0D17:00:00.000` for a 5pm roll). Default: `0D` | -## Initialisation +Passing only the required `log` key loads the module with defaults that match TorQ's original `eodtime.q` behaviour: ```q -eodtime:use`di.eodtime -eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000);()!()] +eodtime.init[enlist[`log]!enlist logdep] ``` -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`. Calling getters before `init` returns uninitialised defaults (`0Nd`, `0Wp`, `0D`) which will silently produce wrong results in downstream processes. -`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`. +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 zero-offset shortcuts directly, without a timezone lookup. `"GMT"` is not recognised by `di.tz` but is the TorQ default, so this ensures the module works out of the box with existing TorQ products. Note: `"Etc/UTC"` is a valid argument in `di.tz`. --- ## Exported functions ### `init` -Initialises the module with the provided config and optional log dependency. Computes initial values of `d`, `nextroll`, and `dailyadj`. +Initialises the module. Validates the log dependency, applies config, and computes initial values of `d`, `nextroll`, and `dailyadj`. ```q -eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000);kxlog.createLog[]] -/ or with defaults and no logger: -eodtime.init[::;()!()] +eodtime.init[`log`rolltimezone`datatimezone`rolltimeoffset!(logdep;`$"Europe/London";`$"GMT";0D)] +/ or with defaults only: +eodtime.init[enlist[`log]!enlist logdep] ``` ### `getd` @@ -123,10 +118,11 @@ eodtime.setd[1+eodtime.getd[]] ## Usage example ```q -/ load and initialise for a London-based system rolling at midnight kxlog:use`kx.log + +/ 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);kxlog.createLog[]] +eodtime.init[`log`rolltimezone`datatimezone`rolltimeoffset!(kxlog.createLog[];`$"Europe/London";`$"GMT";0D)] / check current state eodtime.getd[] / today's trading date in London time diff --git a/di/eodtime/eodtime.q b/di/eodtime/eodtime.q index cb0f0eca..c9ab78ff 100644 --- a/di/eodtime/eodtime.q +++ b/di/eodtime/eodtime.q @@ -3,9 +3,6 @@ / utc-equivalent timezone names - zero offset from utc so no timezone lookup required utczones:`$("GMT";"UTC";"Etc/GMT"); -/ default no-op logger - used when no log dep is injected -defaultlog:`info`warn`error!({[m]};{[m]};{[m]}); - / ============================================================ / module state and defaults / ============================================================ @@ -32,12 +29,16 @@ dailyadj:0D00:00:00.000; / internal helpers / ============================================================ -setdeps:{[deps] - / accept a bare kx.log instance (info/warn/error at top level) or a full deps dict keyed on `log - / if absent or malformed falls back to no-op logger silently - log dep is optional for this module - dv:$[99h=type deps;$[not `log in key deps;enlist[`log]!enlist deps;deps];deps]; - logval:$[99h=type dv;$[`log in key dv;dv`log;defaultlog];defaultlog]; - .z.m.log:$[99h=type logval;$[all `info`warn`error in key logval;logval;defaultlog];defaultlog]; +normlog:{[logdict] + / detect kx.log instance by presence of kx.log-specific keys (getlvl, sinks, fmts) + / kx.log functions are monadic - wrap each into binary {[c;m]} and embed context in the message + / plain {[c;m]} log dicts (info`warn`error only) pass through unchanged + $[any `getlvl`sinks`fmts in key logdict; + `info`warn`error!( + {[fn;c;m] fn[string[c],": ",m]}[logdict`info;]; + {[fn;c;m] fn[string[c],": ",m]}[logdict`warn;]; + {[fn;c;m] fn[string[c],": ",m]}[logdict`error;]); + logdict] }; / returns timespan offset from utc for rolltimezone at timestamp p @@ -81,21 +82,27 @@ setnextroll:{.z.m.nextroll:x}; setdailyadj:{.z.m.dailyadj:x}; setd:{.z.m.d:x}; -/ initialise module with config dictionary and optional log dependency -init:{[config;deps] - / config: dict with optional keys `rolltimezone`datatimezone`rolltimeoffset - / pass (::) or ()!() to use all defaults matching torq eodtime.q behaviour - / deps: optional - kx.log instance or dict with `log -> `info`warn`error!(infofn;warnfn;errfn) - / if absent or malformed falls back to no-op logging silently - / example: - / eodtime.init[`rolltimezone!enlist`$"Europe/London";kxlog.createLog[]] - setdeps deps; - 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]; +init:{[configs] + / initialise the eodtime module - validate deps, apply config, compute initial state + / configs: dict containing `log (required) plus optional `rolltimezone, `datatimezone, `rolltimeoffset + / log dep: `info`warn`error!({[c;m]};{[c;m]};{[c;m]}) - binary, c=context symbol, m=string + / examples: + / eodtime.init[enlist[`log]!enlist logdep] + / eodtime.init[`log`rolltimezone!(logdep;`$"Europe/London")] + if[99h<>type configs; + '"di.eodtime: configs must be a dict with `log key"]; + if[not `log in key configs; + '"di.eodtime: log dependency is required; pass `info`warn`error!(infofn;warnfn;errfn) keyed on `log"]; + if[99h<>type configs`log; + '"di.eodtime: log value must be a dict; pass `info`warn`error functions"]; + if[not all `info`warn`error in key configs`log; + '"di.eodtime: log dict must have `info`warn`error keys; got: ",(", " sv string key configs`log)]; + .z.m.log:normlog configs`log; + .z.m.rolltimezone:$[`rolltimezone in key configs;configs`rolltimezone;`$"GMT"]; + .z.m.datatimezone:$[`datatimezone in key configs;configs`datatimezone;`$"GMT"]; + .z.m.rolltimeoffset:$[`rolltimeoffset in key configs;configs`rolltimeoffset;0D00:00:00.000]; .z.m.dailyadj:getdailyadjustment[]; .z.m.d:getday[.z.p]; .z.m.nextroll:getroll[.z.p]; - .z.m.log[`info]["eodtime: initialised with rolltimezone=",string[rolltimezone]," datatimezone=",string[datatimezone]," rolltimeoffset=",string rolltimeoffset]; + .z.m.log[`info][`eodtime;"initialised with rolltimezone=",string[rolltimezone]," datatimezone=",string[datatimezone]," rolltimeoffset=",string rolltimeoffset]; }; diff --git a/di/eodtime/test.csv b/di/eodtime/test.csv index dd9545b8..94bcb899 100644 --- a/di/eodtime/test.csv +++ b/di/eodtime/test.csv @@ -1,7 +1,8 @@ action,ms,bytes,lang,code,repeat,minver,comment -comment,,,,,,,setup - load module and initialise with gmt defaults and no log dep +comment,,,,,,,setup - load module and initialise with gmt defaults and log dep 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 and no log dep +before,0,0,q,logdep:`info`warn`error!({[c;m]};{[c;m]};{[c;m]}),1,1,silent no-op log mock - binary {[c;m]} matches the log contract +before,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"GMT";0D00:00:00.000;logdep)],1,1,init with gmt defaults and log dep comment,,,,,,,getd true,0,0,q,-14h=type eodtime.getd[],1,1,getd returns date type comment,,,,,,,getnextroll @@ -17,11 +18,11 @@ comment,,,,,,,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 comment,,,,,,,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 +run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"GMT";0D17:00:00.000;logdep)],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 comment,,,,,,,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.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"GMT";0D00:00:00.000;logdep)],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 comment,,,,,,,setdailyadj @@ -31,40 +32,41 @@ comment,,,,,,,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 comment,,,,,,,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 +run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"UTC";`$"Etc/GMT";0D00:00:00.000;logdep)],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 comment,,,,,,,non-gmt rolltimezone (Europe/London) -run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset!(`$"Europe/London";`$"GMT";0D00:00:00.000);()!()],1,1,reinit with London rolltimezone +run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"Europe/London";`$"GMT";0D00:00:00.000;logdep)],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) comment,,,,,,,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 +run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"Europe/London";0D00:00:00.000;logdep)],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 -comment,,,,,,,init with empty dict applies all defaults -run,0,0,q,eodtime.init[()!();()!()],1,1,init with empty config applies all defaults +comment,,,,,,,init - empty config (just log dep) applies all defaults +run,0,0,q,eodtime.init[enlist[`log]!enlist logdep],1,1,init with just log dep 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 -comment,,,,,,,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 +comment,,,,,,,init - non-dict configs throws +fail,0,0,q,eodtime.init[(::)],1,1,errors when configs is not a dictionary comment,,,,,,,error cases fail,0,0,q,eodtime.getroll[`notvalid],1,1,getroll throws on non-timestamp input -comment,,,,,,,log dep - optional no-op fallback -run,0,0,q,eodtime.init[()!();()!()],1,1,init with no log dep does not error -run,0,0,q,eodtime.init[()!();42],1,1,non-dict deps falls back gracefully without error -run,0,0,q,eodtime.init[()!();enlist[`notlog]!enlist 42],1,1,missing log key in deps falls back gracefully -comment,,,,,,,log dep - bare kx.log instance accepted directly +comment,,,,,,,init - dep validation +fail,0,0,q,eodtime.init[()!()],1,1,errors when log dependency is missing +fail,0,0,q,eodtime.init[enlist[`log]!enlist 42],1,1,errors when log value is not a dictionary +fail,0,0,q,eodtime.init[enlist[`log]!enlist `info`warn!(logdep`info;logdep`warn)],1,1,errors when log dict is missing the error key +run,0,0,q,.test.err:@[{eodtime.init[()!()]};(::);{x}],1,1,capture error string from init with missing log dep +true,0,0,q,.test.err like "di.eodtime:*",1,1,error is prefixed di.eodtime: +comment,,,,,,,init - kx.log instance normalised automatically (no manual wrapping required) run,0,0,q,kxlogger:use`kx.log,1,1,load kx.log run,0,0,q,kxinst:kxlogger.createLog[],1,1,create kx.log instance -run,0,0,q,eodtime.init[()!();kxinst],1,1,bare kx.log instance accepted directly as deps -comment,,,,,,,log dep - init message captured when logger provided -run,0,0,q,caplog:`info`warn`error!({[m] `.test.cap upsert(`info;m)};{[m] `.test.cap upsert(`warn;m)};{[m] `.test.cap upsert(`error;m)}),1,1,capturing log mock +run,0,0,q,eodtime.init[`log`rolltimezone!(kxinst;`$"GMT")],1,1,bare kx.log instance accepted without wrapping +run,0,0,q,.m.di.0eodtime.log[`info][`eodtime;"normalisation test"],1,1,normalised logger accepts binary {[c;m]} call without error +comment,,,,,,,log call verification - capturing logger sees init message with binary {[c;m]} args +run,0,0,q,caplog:`info`warn`error!({[c;m] `.test.cap upsert(`info;m)};{[c;m] `.test.cap upsert(`warn;m)};{[c;m] `.test.cap upsert(`error;m)}),1,1,capturing log mock - binary {[c;m]} run,0,0,q,.test.cap:([] fn:`symbol$();msg:()),1,1,initialise log capture table -run,0,0,q,eodtime.init[()!();enlist[`log]!enlist caplog],1,1,init with capturing logger +run,0,0,q,eodtime.init[enlist[`log]!enlist caplog],1,1,init with capturing logger true,0,0,q,1=count select from .test.cap where fn=`info,1,1,one info entry captured on init -true,0,0,q,any (.test.cap`msg) like "eodtime: *",1,1,log message carries eodtime context prefix +true,0,0,q,any (.test.cap`msg) like "initialised*",1,1,log message confirms initialisation From 5f09606059efbbacdca94fef69b374fb34d3b666 Mon Sep 17 00:00:00 2001 From: alowrydi Date: Thu, 25 Jun 2026 12:13:12 +0100 Subject: [PATCH 6/7] Team consistency update: info-only log validation, deps parameter naming, updated docs and tests --- di/eodtime/eodtime.md | 127 ++++++++++++++++++++++-------------------- di/eodtime/eodtime.q | 32 +++++------ di/eodtime/test.csv | 20 ++++++- 3 files changed, 103 insertions(+), 76 deletions(-) diff --git a/di/eodtime/eodtime.md b/di/eodtime/eodtime.md index c2b1f207..021b52b7 100644 --- a/di/eodtime/eodtime.md +++ b/di/eodtime/eodtime.md @@ -1,6 +1,17 @@ # 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. +End-of-day time management for kdb+ 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. + +--- + +## Features + +- Compute the current trading date in a configurable roll timezone, handling DST transitions automatically +- Calculate the UTC timestamp of the next EOD roll from any given UTC timestamp, with support for non-midnight roll times via `rolltimeoffset` +- Compute a live UTC offset for a configurable data timezone, refreshable after each roll to capture DST changes +- Store and expose the current trading date, next roll timestamp, and daily adjustment offset as inspectable module state +- State setters allow tickerplant and segmented tickerplant processes to advance state after a roll without re-initialising +- `"GMT"`, `"UTC"`, and `"Etc/GMT"` are handled as zero-offset shortcuts without a timezone lookup - the module works out of the box with TorQ defaults --- @@ -8,114 +19,115 @@ End-of-day time management for TorQ-based tickerplant processes. Resolves the cu | Dependency | Key | Required | Description | |---|---|---|---| -| logger | `log` | yes | `info`, `warn`, `error` — each binary `{[c;m]}` where `c` is a symbol context and `m` is a string | +| logger | `` `log `` | yes | `info` - binary `{[c;m]}` where `c` is a symbol context and `m` is a string. `warn` and `error` are accepted if present but never called by this module | + +**Hard dependency:** `di.tz` - loaded automatically when the module is imported. -**Hard dependency:** `di.tz` — loaded automatically when the module is imported. +The `log` dependency must be passed to `init` inside the `configs` dict keyed on `` `log ``. The module throws immediately if `log` is absent or `info` is missing. `warn` and `error` are optional - a full `kx.log` instance or a complete `info`/`warn`/`error` dict both work without any changes from the caller. -The `log` dependency must be passed to `init` inside the `configs` dict. The module -throws immediately if it is absent or missing any of the three required keys. +Configuration keys `rolltimezone`, `datatimezone`, and `rolltimeoffset` are all optional - omit any or all of them and the module falls back to sensible defaults (GMT timezone, midnight roll). See Initialisation for full details. -A `kx.log` instance can be passed directly — the module normalises monadic functions -to the binary `{[c;m]}` contract automatically: +A `kx.log` instance can be passed directly - the module normalises monadic functions to the binary `{[c;m]}` contract automatically: ```q kxlog:use`kx.log eodtime:use`di.eodtime + +/ minimal - just the log dep, all config defaults +eodtime.init[enlist[`log]!enlist kxlog.createLog[]] + +/ with timezone override eodtime.init[`log`rolltimezone!(kxlog.createLog[];`$"Europe/London")] + +/ fully specified +eodtime.init[`log`rolltimezone`datatimezone`rolltimeoffset!(kxlog.createLog[];`$"Europe/London";`$"Europe/London";0D17:00:00.000)] ``` --- ## Initialisation -`init[configs]` takes a single dictionary combining the `log` dependency with -any configuration overrides. +`init[deps]` takes a single dictionary combining the `log` dependency with any configuration overrides. | Key | Required | Description | |---|---|---| -| `log` | yes | Binary log dep — `info`, `warn`, `error` functions each `{[c;m]}` | -| `rolltimezone` | no | Timezone used for EOD roll scheduling. Default: `` `$"GMT" `` | -| `datatimezone` | no | Timezone used for stamping incoming data. Default: `` `$"GMT" `` | -| `rolltimeoffset` | no | Offset from midnight for the EOD roll (e.g. `0D17:00:00.000` for a 5pm roll). Default: `0D` | +| `` `log `` | yes | Log dep - at minimum `info` function `{[c;m]}` | +| `` `rolltimezone `` | no | Timezone for EOD roll scheduling. Default: `` `$"GMT" `` | +| `` `datatimezone `` | no | Timezone for stamping incoming data. Default: `` `$"GMT" `` | +| `` `rolltimeoffset `` | no | Offset from midnight for the EOD roll (e.g. `0D17:00:00.000` for a 5pm roll). Default: `0D` | -Passing only the required `log` key loads the module with defaults that match TorQ's original `eodtime.q` behaviour: +`init` must be called before any other function. It computes the initial values of `d`, `nextroll`, and `dailyadj`. Calling getters before `init` returns uninitialised defaults (`0Nd`, `0Wp`, `0D`) which will produce wrong results in downstream processes. -```q -eodtime.init[enlist[`log]!enlist logdep] -``` - -`init` must be called before any other function is used. It computes the initial values of `d`, `nextroll`, and `dailyadj`. Calling getters before `init` returns uninitialised defaults (`0Nd`, `0Wp`, `0D`) which will silently produce wrong results in downstream processes. - -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 zero-offset shortcuts directly, without a timezone lookup. `"GMT"` is not recognised by `di.tz` but is the TorQ default, so this ensures the module works out of the box with existing TorQ products. Note: `"Etc/UTC"` is a valid argument in `di.tz`. +Timezone values should be standard identifiers in `"Region/City"` format (e.g. `"Europe/London"`, `"America/New_York"`). `"GMT"`, `"UTC"`, and `"Etc/GMT"` are zero-offset shortcuts handled directly without a `di.tz` lookup. Note: `"Etc/UTC"` is valid and passed through to `di.tz` normally. --- -## Exported functions +## Exported Functions -### `init` -Initialises the module. Validates the log dependency, applies config, and computes initial values of `d`, `nextroll`, and `dailyadj`. +### `init[deps]` +Initialise the module. Validates the log dependency, applies config, and computes initial values of `d`, `nextroll`, and `dailyadj`. ```q eodtime.init[`log`rolltimezone`datatimezone`rolltimeoffset!(logdep;`$"Europe/London";`$"GMT";0D)] / or with defaults only: eodtime.init[enlist[`log]!enlist logdep] ``` -### `getd` -Returns the current trading date in the roll timezone. +### `getd[]` +Return 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. +### `getnextroll[]` +Return 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: +### `getdailyadj[]` +Return the **cached** UTC offset for the data timezone - the value stored at the last `init` or `setdailyadj` call. ```q -.z.p + eodtime.getdailyadj[] -/ adds the stored offset to the current UTC time +eodtime.getdailyadj[] +/ 0D01:00:00.000000000 ``` -### `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. +### `getdailyadjustment[]` +**Recompute** the UTC offset for the data timezone at the current time. Call after an EOD roll to get a fresh offset - important when the data timezone observes DST. ```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. +### `getroll[p]` +Compute the UTC timestamp of the next EOD roll after UTC timestamp `p`. Returns today's roll time if the roll has not yet passed; 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. +### `setnextroll[x]` +Update the stored next roll timestamp. Called by the 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. +### `setdailyadj[x]` +Update 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. +### `setd[x]` +Update 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 +## Usage Example ```q kxlog:use`kx.log @@ -129,33 +141,30 @@ 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 +/ at EOD roll - tickerplant advances state eodtime.setnextroll eodtime.getroll[.z.p]; eodtime.setdailyadj eodtime.getdailyadjustment[]; -/ simulate what the segmented tickerplant does at EOD roll +/ at EOD roll - segmented tickerplant advances date eodtime.setd[1+eodtime.getd[]] ``` --- -## TorQ migration pattern +## Running Tests + +```q +k4unit:use`di.k4unit +k4unit.moduletest`di.eodtime +``` -| 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[]]` | +The test suite injects a no-op mock logger and a capturing logger that records `(level;msg)` pairs for assertion. It covers: dependency validation (non-dict deps throws; missing `log` key throws; non-dict log value throws; missing `info` key throws; `info`-only log dict succeeds); config defaults and overrides; `getroll` with GMT and non-GMT roll timezones including DST transitions (London winter vs summer); `getdailyadjustment` with UTC shortcuts and DST-aware timezones; state setters (`setnextroll`, `setdailyadj`, `setd`) and their corresponding getters; and a real `kx.log` integration test confirming the `normlog` wrapping works end-to-end. --- ## 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`. +- `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 live. Always call `getdailyadjustment` after an EOD roll rather than relying on the cached value +- `"GMT"`, `"UTC"`, and `"Etc/GMT"` are zero-offset shortcuts not passed to `di.tz`. `"Etc/UTC"` is valid and passes through normally +- Only `info` is required in the log dep - `warn` and `error` are accepted if present (e.g. from a full `kx.log` instance) but never called by this module \ No newline at end of file diff --git a/di/eodtime/eodtime.q b/di/eodtime/eodtime.q index c9ab78ff..975a8a43 100644 --- a/di/eodtime/eodtime.q +++ b/di/eodtime/eodtime.q @@ -82,27 +82,27 @@ setnextroll:{.z.m.nextroll:x}; setdailyadj:{.z.m.dailyadj:x}; setd:{.z.m.d:x}; -init:{[configs] +init:{[deps] / initialise the eodtime module - validate deps, apply config, compute initial state - / configs: dict containing `log (required) plus optional `rolltimezone, `datatimezone, `rolltimeoffset - / log dep: `info`warn`error!({[c;m]};{[c;m]};{[c;m]}) - binary, c=context symbol, m=string + / deps: dict containing `log (required) plus optional `rolltimezone, `datatimezone, `rolltimeoffset + / log dep: at minimum `info!{[c;m]} - binary, c=context symbol, m=string / examples: / eodtime.init[enlist[`log]!enlist logdep] / eodtime.init[`log`rolltimezone!(logdep;`$"Europe/London")] - if[99h<>type configs; - '"di.eodtime: configs must be a dict with `log key"]; - if[not `log in key configs; - '"di.eodtime: log dependency is required; pass `info`warn`error!(infofn;warnfn;errfn) keyed on `log"]; - if[99h<>type configs`log; - '"di.eodtime: log value must be a dict; pass `info`warn`error functions"]; - if[not all `info`warn`error in key configs`log; - '"di.eodtime: log dict must have `info`warn`error keys; got: ",(", " sv string key configs`log)]; - .z.m.log:normlog configs`log; - .z.m.rolltimezone:$[`rolltimezone in key configs;configs`rolltimezone;`$"GMT"]; - .z.m.datatimezone:$[`datatimezone in key configs;configs`datatimezone;`$"GMT"]; - .z.m.rolltimeoffset:$[`rolltimeoffset in key configs;configs`rolltimeoffset;0D00:00:00.000]; + if[99h<>type deps; + '"di.eodtime: deps must be a dict with `log key"]; + if[not `log in key deps; + '"di.eodtime: log dependency is required; pass at minimum `info!{[c;m]} keyed on `log"]; + if[99h<>type deps`log; + '"di.eodtime: log value must be a dict; pass at minimum `info!{[c;m]}"]; + if[not `info in key deps`log; + '"di.eodtime: log dict must have at minimum an `info key; got: ",(", " sv string key deps`log)]; + .z.m.log:normlog deps`log; + .z.m.rolltimezone:$[`rolltimezone in key deps;deps`rolltimezone;`$"GMT"]; + .z.m.datatimezone:$[`datatimezone in key deps;deps`datatimezone;`$"GMT"]; + .z.m.rolltimeoffset:$[`rolltimeoffset in key deps;deps`rolltimeoffset;0D00:00:00.000]; .z.m.dailyadj:getdailyadjustment[]; .z.m.d:getday[.z.p]; .z.m.nextroll:getroll[.z.p]; .z.m.log[`info][`eodtime;"initialised with rolltimezone=",string[rolltimezone]," datatimezone=",string[datatimezone]," rolltimeoffset=",string rolltimeoffset]; - }; + }; \ No newline at end of file diff --git a/di/eodtime/test.csv b/di/eodtime/test.csv index 94bcb899..04262d96 100644 --- a/di/eodtime/test.csv +++ b/di/eodtime/test.csv @@ -3,67 +3,85 @@ comment,,,,,,,setup - load module and initialise with gmt defaults and log dep before,0,0,q,eodtime:use`di.eodtime,1,1,load di.eodtime module before,0,0,q,logdep:`info`warn`error!({[c;m]};{[c;m]};{[c;m]}),1,1,silent no-op log mock - binary {[c;m]} matches the log contract before,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"GMT";0D00:00:00.000;logdep)],1,1,init with gmt defaults and log dep + comment,,,,,,,getd true,0,0,q,-14h=type eodtime.getd[],1,1,getd returns date type + comment,,,,,,,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 + comment,,,,,,,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 + comment,,,,,,,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 + comment,,,,,,,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 + comment,,,,,,,getroll - gmt with rolltimeoffset run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"GMT";0D17:00:00.000;logdep)],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 + comment,,,,,,,setnextroll run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"GMT";0D00:00:00.000;logdep)],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 + comment,,,,,,,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 + comment,,,,,,,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 + comment,,,,,,,utc-equivalent timezone shortcuts (GMT/UTC/Etc/GMT) run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"UTC";`$"Etc/GMT";0D00:00:00.000;logdep)],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 + comment,,,,,,,non-gmt rolltimezone (Europe/London) run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"Europe/London";`$"GMT";0D00:00:00.000;logdep)],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) + comment,,,,,,,non-gmt datatimezone (Europe/London - DST aware) run,0,0,q,eodtime.init[`rolltimezone`datatimezone`rolltimeoffset`log!(`$"GMT";`$"Europe/London";0D00:00:00.000;logdep)],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 + comment,,,,,,,init - empty config (just log dep) applies all defaults run,0,0,q,eodtime.init[enlist[`log]!enlist logdep],1,1,init with just log dep 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 + comment,,,,,,,init - non-dict configs throws fail,0,0,q,eodtime.init[(::)],1,1,errors when configs is not a dictionary + comment,,,,,,,error cases fail,0,0,q,eodtime.getroll[`notvalid],1,1,getroll throws on non-timestamp input + comment,,,,,,,init - dep validation fail,0,0,q,eodtime.init[()!()],1,1,errors when log dependency is missing fail,0,0,q,eodtime.init[enlist[`log]!enlist 42],1,1,errors when log value is not a dictionary -fail,0,0,q,eodtime.init[enlist[`log]!enlist `info`warn!(logdep`info;logdep`warn)],1,1,errors when log dict is missing the error key +run,0,0,q,eodtime.init[enlist[`log]!enlist `info`warn!(logdep`info;logdep`warn)],1,1,init succeeds with info and warn but no error key - only info is required run,0,0,q,.test.err:@[{eodtime.init[()!()]};(::);{x}],1,1,capture error string from init with missing log dep true,0,0,q,.test.err like "di.eodtime:*",1,1,error is prefixed di.eodtime: + comment,,,,,,,init - kx.log instance normalised automatically (no manual wrapping required) run,0,0,q,kxlogger:use`kx.log,1,1,load kx.log run,0,0,q,kxinst:kxlogger.createLog[],1,1,create kx.log instance run,0,0,q,eodtime.init[`log`rolltimezone!(kxinst;`$"GMT")],1,1,bare kx.log instance accepted without wrapping run,0,0,q,.m.di.0eodtime.log[`info][`eodtime;"normalisation test"],1,1,normalised logger accepts binary {[c;m]} call without error + comment,,,,,,,log call verification - capturing logger sees init message with binary {[c;m]} args run,0,0,q,caplog:`info`warn`error!({[c;m] `.test.cap upsert(`info;m)};{[c;m] `.test.cap upsert(`warn;m)};{[c;m] `.test.cap upsert(`error;m)}),1,1,capturing log mock - binary {[c;m]} run,0,0,q,.test.cap:([] fn:`symbol$();msg:()),1,1,initialise log capture table From eb051c8ea7ce5df37c26019f5ffa4a4310c5c5a5 Mon Sep 17 00:00:00 2001 From: alowrydi Date: Mon, 29 Jun 2026 14:11:39 +0100 Subject: [PATCH 7/7] retriggering AI review --- di/eodtime/eodtime.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/di/eodtime/eodtime.md b/di/eodtime/eodtime.md index 021b52b7..54122c32 100644 --- a/di/eodtime/eodtime.md +++ b/di/eodtime/eodtime.md @@ -164,7 +164,7 @@ The test suite injects a no-op mock logger and a capturing logger that records ` ## Notes -- `rolltimeoffset` adjusts the roll time from midnight - e.g. `0D17:00:00.000` produces a 5pm local roll +- `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 live. Always call `getdailyadjustment` after an EOD roll rather than relying on the cached value - `"GMT"`, `"UTC"`, and `"Etc/GMT"` are zero-offset shortcuts not passed to `di.tz`. `"Etc/UTC"` is valid and passes through normally -- Only `info` is required in the log dep - `warn` and `error` are accepted if present (e.g. from a full `kx.log` instance) but never called by this module \ No newline at end of file +- Only `info` is required in the log dep - `warn` and `error` are accepted if present (e.g. from a full `kx.log` instance) but never called by this module