Skip to content

Commit 0ada036

Browse files
committed
refactor(transform): rename Transformer to Transform and fluent API 👋
- Document setWeekStartDay, toTimeframe options, Validator and Time constants in README - Export Transform and Validator from index; remove Transformer export - Add Transform with fromCandles, setAnchorTime, toTimeframe, setWeekStartDay and execute options - Add tests for 1W/1Mc, setWeekStartDay, validation option, and rename from/anchor/to - Remove Transformer.ts - Update README examples and API reference to new method names
1 parent c87ca18 commit 0ada036

4 files changed

Lines changed: 441 additions & 186 deletions

File tree

README.md

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ High-precision OHLC transformation with strict anchor time alignment.
1010

1111
## Features
1212

13-
- **Strict Anchor Alignment**: Ensures candles align with specific sessions (e.g., 4h at 23:00 or 23:30 UTC).
1413
- **High Performance**: Batch processing optimized for thousands of candles at once.
15-
- **Flexible Timeframes**: Supports `m`, `h`, `d`, `w`, `M` (e.g. `15m`, `4h`, `1d`, `1w`, `1M`).
14+
- **Strict Anchor Alignment**: Ensures candles align with specific sessions (e.g., 4h at 23:00 or 23:30 UTC).
15+
- **Flexible Timeframes**: Supports `m`, `h`, `d`, `w`, `M` (fixed) and `1W` (calendar week), `1Mc` (calendar month).
16+
- **Input validation**: Optional `validate: true` to check time, OHLC, duplicates, and order before transform.
1617

1718
## Installation
1819

@@ -42,62 +43,85 @@ const data = [
4243
]
4344

4445
// Convert to 4-hour chart (Default Anchor 23:00 UTC)
45-
const h4 = Transform.from(data).to('4h')
46+
const h4 = Transform.fromCandles(data).toTimeframe('4h')
4647
console.log(h4) // Output: [ { time: 1704063600000, open: 1, high: 2, low: 0.5, close: 1.5, ... }, ... ]
4748

4849
// Convert to 1-day chart with custom anchor (e.g., 00:00 UTC)
49-
const daily = Transform.from(data).anchor(0).to('1d')
50+
const daily = Transform.fromCandles(data).setAnchorTime(0).toTimeframe('1d')
5051

5152
// Anchor with hour and minute (e.g., 23:30 UTC)
52-
const session = Transform.from(data).anchor(23, 30).to('4h')
53+
const session = Transform.fromCandles(data).setAnchorTime(23, 30).toTimeframe('4h')
54+
55+
// Calendar week (Monday start) and calendar month
56+
const weekly = Transform.fromCandles(data).toTimeframe('1W')
57+
const monthly = Transform.fromCandles(data).toTimeframe('1Mc')
58+
59+
// Optional validation before transform
60+
const safe = Transform.fromCandles(data).toTimeframe('4h', { validate: true })
5361
```
5462

5563
## API Reference
5664

57-
### Transform.from
65+
### Transform.fromCandles
5866

5967
```typescript
60-
Transform.from(data)
68+
Transform.fromCandles(data)
6169
```
6270

6371
- `data` `<CandleData[]>`: Array of source OHLC candles
6472
- Returns: `Transform`
6573
- Description: Creates a transformation instance for fluent chaining.
6674

67-
### Transform.prototype.anchor
75+
### Transform.prototype.setAnchorTime
6876

6977
```typescript
70-
transform.anchor(hour, minute)
78+
transform.setAnchorTime(hour, minute)
7179
```
7280

7381
- `hour` `<number>`: Anchor hour in UTC (0–23). Defaults to `23`.
7482
- `minute` `<number>`: (Optional) Anchor minute (0–59). Defaults to `0`.
7583
- Returns: `this`
7684
- Description: Sets the anchor time for bucket alignment (e.g. 23:30 UTC).
7785

78-
### Transform.prototype.to
86+
### Transform.prototype.toTimeframe
7987

8088
```typescript
81-
transform.to(timeframe)
89+
transform.toTimeframe(timeframe, options)
8290
```
8391

84-
- `timeframe` `<string>`: Target timeframe (e.g. `'15m'`, `'4h'`, `'1d'`, `'1w'`, `'1M'`).
92+
- `timeframe` `<string>`: Target (e.g. `'4h'`, `'1d'`, `'1W'`, `'1Mc'`).
93+
- `options` `<object>`: (Optional) `{ validate?: boolean }` — run input validation before transform.
8594
- Returns: `CandleData[]`
8695
- Description: Runs the transformation and returns aggregated candles.
8796

97+
### Transform.prototype.setWeekStartDay
98+
99+
```typescript
100+
transform.setWeekStartDay(weekStartDay)
101+
```
102+
103+
- `weekStartDay` `<number>`: Week start (0=Sun, 1=Mon, …, 6=Sat). Default `1` (Monday). Used for `'1W'`.
104+
- Returns: `this`
105+
- Description: Sets week start day for calendar week timeframe.
106+
88107
### Transform.execute
89108

90109
```typescript
91-
Transform.execute(candles, timeframe, anchorHour, anchorMinute)
110+
Transform.execute(candles, timeframe, anchorHour, anchorMinute, options)
92111
```
93112

94113
- `candles` `<CandleData[]>`: Source candle array
95-
- `timeframe` `<string>`: Target timeframe (e.g. `'4h'`, `'1d'`)
114+
- `timeframe` `<string>`: Target (e.g. `'4h'`, `'1d'`, `'1W'`, `'1Mc'`)
96115
- `anchorHour` `<number>`: (Optional) Anchor hour (0–23). Defaults to `23`.
97116
- `anchorMinute` `<number>`: (Optional) Anchor minute (0–59). Defaults to `0`.
117+
- `options` `<object>`: (Optional) `{ weekStartDay?: 0|1|…|6, validate?: boolean }`
98118
- Returns: `CandleData[]`
99119
- Description: Runs batch transformation without a fluent instance.
100120

121+
### Time (constants)
122+
123+
Static readonly: `msPerMinute`, `msPerHour`, `msPerDay`, `msPerWeek`, `msPerMonth` (milliseconds per unit), `defaultAnchorOffset` (23h in ms).
124+
101125
### Time.alignTime
102126

103127
```typescript
@@ -111,19 +135,43 @@ Time.alignTime(timestamp, intervalMs, anchorHour, anchorMinute)
111135
- Returns: `number`
112136
- Description: Aligns timestamp to the bucket open time on the anchor grid.
113137

138+
### Time.getBucketStart
139+
140+
```typescript
141+
Time.getBucketStart(timestamp, timeframe, anchorHour, anchorMinute, weekStartDay)
142+
```
143+
144+
- `timestamp` `<number>`: Input time in ms
145+
- `timeframe` `<string>`: e.g. `'4h'`, `'1d'`, `'1W'`, `'1Mc'`
146+
- `anchorHour` / `anchorMinute`: Used for fixed intervals only
147+
- `weekStartDay` `<number>`: (Optional) 0–6 for `'1W'`. Default `1` (Monday)
148+
- Returns: `number`
149+
- Description: Bucket start for any timeframe (fixed or calendar).
150+
114151
### Time.parseTimeframe
115152

116153
```typescript
117-
Time.parseTimeframe(tf)
154+
Time.parseTimeframe(timeframeStr)
118155
```
119156

120-
- `tf` `<string>`: Timeframe string (e.g. `'15m'`, `'4h'`, `'1d'`, `'1w'`, `'1M'`)
157+
- `timeframeStr` `<string>`: Fixed timeframe only (e.g. `'15m'`, `'4h'`, `'1d'`, `'1w'`, `'1M'`). Use `Time.getBucketStart` for `'1W'` / `'1Mc'`.
121158
- Returns: `number`
122159
- Description: Parses timeframe string to duration in milliseconds.
123160

161+
### Validator.validateCandles
162+
163+
```typescript
164+
Validator.validateCandles(candles, options)
165+
```
166+
167+
- `candles` `<CandleData[]>`: Array to validate
168+
- `options` `<object>`: (Optional) `{ allowDuplicates?, allowUnordered?, strictOHLC? }`
169+
- Returns: `{ valid: boolean, errors: string[] }`
170+
- Description: Validates time (finite, non-negative), OHLC, duplicates, and order.
171+
124172
## Note
125173

126-
- `1w` = 7 days; `1M` = 30-day period (fixed length, not calendar month).
174+
- `1w` = 7 days (fixed); `1M` = 30-day period (fixed). `1W` = calendar week (default Monday); `1Mc` = calendar month (first of month UTC). Validation is opt-in via `toTimeframe(..., { validate: true })` or `Transform.execute(..., { validate: true })`.
127175

128176
## License
129177

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import type { CandleData, TimeframeStr } from '@app/Types.ts'
1+
import type * as Types from '@app/Types.ts'
22
import { Time } from '@app/Time.ts'
3+
import { Validator } from '@app/Validator.ts'
34

45
/**
56
* OHLC timeframe transform with anchor alignment.
@@ -11,7 +12,9 @@ export class Transform {
1112
/** Anchor minute (0-59) */
1213
private anchorMinute = 0
1314
/** Source candles data */
14-
private sourceData: CandleData[]
15+
private sourceData: Types.CandleData[]
16+
/** Week start for 1W (0-6), default 1 (Monday) */
17+
private weekStartDay: Types.CalendarWeekStart = 1
1518

1619
/**
1720
* Sets anchor time (hour and optional minute).
@@ -20,7 +23,7 @@ export class Transform {
2023
* @param minute - Minute between 0-59, default 0
2124
* @returns Current instance
2225
*/
23-
anchor(hour: number, minute = 0): this {
26+
setAnchorTime(hour: number, minute = 0): this {
2427
if (hour < 0 || hour > 23) {
2528
throw new Error('Anchor hour must be between 0 and 23')
2629
}
@@ -37,34 +40,48 @@ export class Transform {
3740
* @description Initializes with source candle data.
3841
* @param data - Input candle array
3942
*/
40-
constructor(data: CandleData[]) {
43+
constructor(data: Types.CandleData[]) {
4144
this.sourceData = data
4245
}
4346

4447
/**
4548
* Runs batch transformation.
4649
* @description Aggregates candles to target timeframe and anchor.
4750
* @param candles - Source data array
48-
* @param timeframe - Target timeframe string
51+
* @param timeframe - Target timeframe (e.g. 4h, 1d, 1W, 1Mc)
4952
* @param anchorHour - Alignment anchor hour (0-23)
5053
* @param anchorMinute - Alignment anchor minute (0-59), default 0
54+
* @param options - Optional: weekStartDay (for 1W), validate
5155
* @returns Transformed candle array
5256
*/
5357
static execute(
54-
candles: CandleData[],
55-
timeframe: TimeframeStr,
58+
candles: Types.CandleData[],
59+
timeframe: Types.TimeframeStr,
5660
anchorHour = 23,
57-
anchorMinute = 0
58-
): CandleData[] {
61+
anchorMinute = 0,
62+
options?: { weekStartDay?: Types.CalendarWeekStart; validate?: boolean }
63+
): Types.CandleData[] {
5964
if (candles.length === 0) {
6065
return []
6166
}
62-
const intervalMs = Time.parseTimeframe(timeframe)
63-
const results: CandleData[] = []
64-
let currentCandle: CandleData | null = null
67+
if (options?.validate) {
68+
const result = Validator.validateCandles(candles, { allowUnordered: true })
69+
if (!result.valid) {
70+
throw new Error(`Validation failed: ${result.errors.join('; ')}`)
71+
}
72+
}
73+
const weekStart = options?.weekStartDay ?? 1
74+
const results: Types.CandleData[] = []
75+
let currentCandle: Types.CandleData | null = null
6576
const sortedCandles = [...candles].sort((a, b) => a.time - b.time)
6677
for (const candle of sortedCandles) {
67-
const bucketStart = Time.alignTime(candle.time, intervalMs, anchorHour, anchorMinute)
78+
const bucketStart = Time.getBucketStart(
79+
candle.time,
80+
timeframe,
81+
anchorHour,
82+
anchorMinute,
83+
weekStart
84+
)
6885
if (!currentCandle) {
6986
currentCandle = {
7087
time: bucketStart,
@@ -107,17 +124,32 @@ export class Transform {
107124
* @param data - Input candle array
108125
* @returns New Transform instance
109126
*/
110-
static from(data: CandleData[]): Transform {
127+
static fromCandles(data: Types.CandleData[]): Transform {
111128
return new Transform(data)
112129
}
113130

131+
/**
132+
* Sets week start day for 1W (calendar week).
133+
* @description 0=Sun, 1=Mon, etc. Default 1.
134+
* @param weekStartDay - Day of week (0-6)
135+
* @returns Current instance
136+
*/
137+
setWeekStartDay(weekStartDay: Types.CalendarWeekStart): this {
138+
this.weekStartDay = weekStartDay
139+
return this
140+
}
141+
114142
/**
115143
* Runs transformation and returns candles.
116144
* @description Aggregates to target timeframe with current anchor.
117-
* @param timeframe - Target timeframe string
145+
* @param timeframe - Target timeframe string (e.g. 4h, 1W, 1Mc)
146+
* @param options - Optional: validate
118147
* @returns Resulting candle array
119148
*/
120-
to(timeframe: TimeframeStr): CandleData[] {
121-
return Transform.execute(this.sourceData, timeframe, this.anchorHour, this.anchorMinute)
149+
toTimeframe(timeframe: Types.TimeframeStr, options?: { validate?: boolean }): Types.CandleData[] {
150+
return Transform.execute(this.sourceData, timeframe, this.anchorHour, this.anchorMinute, {
151+
weekStartDay: this.weekStartDay,
152+
...(options?.validate !== undefined ? { validate: options.validate } : {})
153+
})
122154
}
123155
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
* @description Exports all public modules.
44
*/
55
export type * from '@app/Types.ts'
6-
export * from '@app/Transformer.ts'
6+
export * from '@app/Transform.ts'
77
export * from '@app/Time.ts'
8+
export * from '@app/Validator.ts'

0 commit comments

Comments
 (0)