lightweight-charts-mtf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.d.mts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +358 -0
- package/dist/index.mjs +328 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 lightweight-charts-mtf contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# lightweight-charts-mtf
|
|
2
|
+
|
|
3
|
+
Multi-timeframe (MTF) candle display plugin for [TradingView Lightweight Charts](https://github.com/nickolasburr/lightweight-charts). Renders higher-timeframe candles as native candlestick series alongside your main chart.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Display any number of higher-timeframe columns next to your main chart
|
|
12
|
+
- Native candlestick rendering — HTF candles share the same price scale
|
|
13
|
+
- Accepts any timeframe string as label (e.g. `'4H'`, `'1D'`, `'1W'`, `'1M'`)
|
|
14
|
+
- Built-in OHLC aggregation from lower timeframes (4H, 8H, 12H, 1D, 1W, 1M, 3M, 6M, 1Y)
|
|
15
|
+
- Per-column candle colors (body + wick, up + down)
|
|
16
|
+
- Separator lines and labels between columns
|
|
17
|
+
- Configurable gap between main chart and HTF zone, and between HTF columns
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install lightweight-charts-mtf
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### With pre-aggregated data (from API)
|
|
28
|
+
|
|
29
|
+
If your data source already provides higher-timeframe candles (e.g., Bybit, Binance):
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { createChart, CandlestickSeries } from 'lightweight-charts';
|
|
33
|
+
import { MTFPrimitive } from 'lightweight-charts-mtf';
|
|
34
|
+
|
|
35
|
+
const chart = createChart(container);
|
|
36
|
+
const mainSeries = chart.addSeries(CandlestickSeries);
|
|
37
|
+
|
|
38
|
+
// Fetch data at different timeframes from your API
|
|
39
|
+
const hourlyData = await fetchKlines('BTCUSDT', '1h');
|
|
40
|
+
const dailyData = await fetchKlines('BTCUSDT', '1d');
|
|
41
|
+
const weeklyData = await fetchKlines('BTCUSDT', '1w');
|
|
42
|
+
|
|
43
|
+
// Create and attach
|
|
44
|
+
const mtf = new MTFPrimitive({
|
|
45
|
+
columns: [
|
|
46
|
+
{ timeframe: '1D', data: dailyData, candleCount: 6 },
|
|
47
|
+
{ timeframe: '1W', data: weeklyData, candleCount: 4 },
|
|
48
|
+
],
|
|
49
|
+
gap: 5,
|
|
50
|
+
columnGap: 2,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
mtf.attach(chart, mainSeries, hourlyData);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### With self-aggregated data
|
|
57
|
+
|
|
58
|
+
If you only have lower-timeframe data and need to derive higher timeframes:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { createChart, CandlestickSeries } from 'lightweight-charts';
|
|
62
|
+
import { MTFPrimitive, aggregateOHLC } from 'lightweight-charts-mtf';
|
|
63
|
+
|
|
64
|
+
const chart = createChart(container);
|
|
65
|
+
const mainSeries = chart.addSeries(CandlestickSeries);
|
|
66
|
+
|
|
67
|
+
const hourlyData = [...];
|
|
68
|
+
|
|
69
|
+
// Aggregate into higher timeframes
|
|
70
|
+
const fourHour = aggregateOHLC(hourlyData, '4H');
|
|
71
|
+
const daily = aggregateOHLC(hourlyData, '1D');
|
|
72
|
+
const weekly = aggregateOHLC(hourlyData, '1W');
|
|
73
|
+
|
|
74
|
+
const mtf = new MTFPrimitive({
|
|
75
|
+
columns: [
|
|
76
|
+
{ timeframe: '4H', data: fourHour, candleCount: 10 },
|
|
77
|
+
{ timeframe: '1D', data: daily, candleCount: 6 },
|
|
78
|
+
{ timeframe: '1W', data: weekly, candleCount: 4 },
|
|
79
|
+
],
|
|
80
|
+
gap: 5,
|
|
81
|
+
columnGap: 2,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
mtf.attach(chart, mainSeries, hourlyData);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Data Format
|
|
88
|
+
|
|
89
|
+
The plugin accepts standard Lightweight Charts OHLC data:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Unix timestamp (seconds)
|
|
93
|
+
{ time: 1704067200, open: 42000, high: 42500, low: 41800, close: 42300, volume: 1500 }
|
|
94
|
+
|
|
95
|
+
// Date string
|
|
96
|
+
{ time: '2024-01-01', open: 42000, high: 42500, low: 41800, close: 42300 }
|
|
97
|
+
|
|
98
|
+
// Object
|
|
99
|
+
{ time: { year: 2024, month: 1, day: 1 }, open: 42000, high: 42500, low: 41800, close: 42300 }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `volume` field is optional.
|
|
103
|
+
|
|
104
|
+
## Aggregation
|
|
105
|
+
|
|
106
|
+
If your data source doesn't provide higher-timeframe candles directly, use `aggregateOHLC`:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { aggregateOHLC } from 'lightweight-charts-mtf';
|
|
110
|
+
|
|
111
|
+
const fourHour = aggregateOHLC(hourlyCandles, '4H');
|
|
112
|
+
const daily = aggregateOHLC(hourlyCandles, '1D');
|
|
113
|
+
const weekly = aggregateOHLC(hourlyCandles, '1W');
|
|
114
|
+
const monthly = aggregateOHLC(dailyCandles, '1M');
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Supported periods: `'4H'`, `'8H'`, `'12H'`, `'1D'`, `'1W'`, `'1M'`, `'3M'`, `'6M'`, `'1Y'`
|
|
118
|
+
|
|
119
|
+
Legacy helpers are also available:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { aggregateToWeekly, aggregateToMonthly } from 'lightweight-charts-mtf';
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Options
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
interface MTFOptions {
|
|
129
|
+
columns: HTFColumnConfig[];
|
|
130
|
+
gap?: number; // whitespace bars between main chart and HTF (default: 5)
|
|
131
|
+
columnGap?: number; // whitespace bars between HTF columns (default: 2)
|
|
132
|
+
showSeparators?: boolean; // default: true
|
|
133
|
+
separatorColor?: string; // default: 'rgba(255,255,255,0.08)'
|
|
134
|
+
showLabels?: boolean; // default: true
|
|
135
|
+
labelFont?: string; // default: '11px sans-serif'
|
|
136
|
+
labelColor?: string; // default: 'rgba(255,255,255,0.5)'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface HTFColumnConfig {
|
|
140
|
+
timeframe: string; // any label: '4H', '1D', '1W', etc.
|
|
141
|
+
data: OHLCData[];
|
|
142
|
+
candleCount?: number; // default: 6
|
|
143
|
+
upColor?: string; // default: '#26a69a'
|
|
144
|
+
downColor?: string; // default: '#ef5350'
|
|
145
|
+
wickUpColor?: string; // defaults to upColor
|
|
146
|
+
wickDownColor?: string; // defaults to downColor
|
|
147
|
+
borderVisible?: boolean; // default: false
|
|
148
|
+
borderUpColor?: string; // defaults to upColor
|
|
149
|
+
borderDownColor?: string; // defaults to downColor
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## API
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Create the plugin
|
|
157
|
+
const mtf = new MTFPrimitive(options);
|
|
158
|
+
|
|
159
|
+
// Attach to chart (creates HTF series and renders)
|
|
160
|
+
mtf.attach(chart, mainSeries, mainData);
|
|
161
|
+
|
|
162
|
+
// Update with new data and/or options
|
|
163
|
+
mtf.update(newMainData);
|
|
164
|
+
mtf.update(newMainData, { columns: [...], gap: 8 });
|
|
165
|
+
|
|
166
|
+
// Update a single column's data
|
|
167
|
+
mtf.updateColumnData(0, newWeeklyData);
|
|
168
|
+
|
|
169
|
+
// Detach and clean up
|
|
170
|
+
mtf.detach();
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Example
|
|
174
|
+
|
|
175
|
+
See [`example/index.html`](./example/index.html) for a live demo using BTC data from Bybit with configurable base timeframe, HTF columns, gap, and column gap.
|
|
176
|
+
|
|
177
|
+
To run locally:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npm run build
|
|
181
|
+
npx serve .
|
|
182
|
+
# Open http://localhost:3000/example/
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npm install
|
|
189
|
+
npm run build # Build CJS, ESM, and type declarations
|
|
190
|
+
npm run dev # Watch mode
|
|
191
|
+
npm run typecheck # Type check without emitting
|
|
192
|
+
npm test # Run unit tests
|
|
193
|
+
npm run test:watch # Watch mode for tests
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Time, IChartApi, ISeriesApi } from 'lightweight-charts';
|
|
2
|
+
|
|
3
|
+
/** OHLC candle data for a single timeframe period */
|
|
4
|
+
interface OHLCData {
|
|
5
|
+
time: Time;
|
|
6
|
+
open: number;
|
|
7
|
+
high: number;
|
|
8
|
+
low: number;
|
|
9
|
+
close: number;
|
|
10
|
+
volume?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Supported higher timeframes — any string label is accepted */
|
|
13
|
+
type HTFTimeframe = string;
|
|
14
|
+
/** Configuration for a single HTF column displayed to the right */
|
|
15
|
+
interface HTFColumnConfig {
|
|
16
|
+
/** Timeframe label (e.g., '1W', '1M') */
|
|
17
|
+
timeframe: HTFTimeframe;
|
|
18
|
+
/** OHLC data for this timeframe — most recent N candles */
|
|
19
|
+
data: OHLCData[];
|
|
20
|
+
/** Number of candles to display (default: 6) */
|
|
21
|
+
candleCount?: number;
|
|
22
|
+
/** Up candle color */
|
|
23
|
+
upColor?: string;
|
|
24
|
+
/** Down candle color */
|
|
25
|
+
downColor?: string;
|
|
26
|
+
/** Wick color for up candles (defaults to upColor) */
|
|
27
|
+
wickUpColor?: string;
|
|
28
|
+
/** Wick color for down candles (defaults to downColor) */
|
|
29
|
+
wickDownColor?: string;
|
|
30
|
+
/** Border visible on candle bodies (default: false) */
|
|
31
|
+
borderVisible?: boolean;
|
|
32
|
+
/** Border up color */
|
|
33
|
+
borderUpColor?: string;
|
|
34
|
+
/** Border down color */
|
|
35
|
+
borderDownColor?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Options for the MTF plugin */
|
|
38
|
+
interface MTFOptions {
|
|
39
|
+
/** Array of HTF columns to display, ordered left-to-right */
|
|
40
|
+
columns: HTFColumnConfig[];
|
|
41
|
+
/** Number of whitespace bars between main chart and first HTF column (default: 5) */
|
|
42
|
+
gap?: number;
|
|
43
|
+
/** Number of whitespace bars between HTF columns (default: 2) */
|
|
44
|
+
columnGap?: number;
|
|
45
|
+
/** Show separator lines between columns (default: true) */
|
|
46
|
+
showSeparators?: boolean;
|
|
47
|
+
/** Separator line color (default: 'rgba(255,255,255,0.08)') */
|
|
48
|
+
separatorColor?: string;
|
|
49
|
+
/** Show timeframe labels (default: true) */
|
|
50
|
+
showLabels?: boolean;
|
|
51
|
+
/** Label font (default: '11px sans-serif') */
|
|
52
|
+
labelFont?: string;
|
|
53
|
+
/** Label color (default: 'rgba(255,255,255,0.5)') */
|
|
54
|
+
labelColor?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare class MTFPrimitive {
|
|
58
|
+
private _chart;
|
|
59
|
+
private _mainSeries;
|
|
60
|
+
private _htfSeries;
|
|
61
|
+
private _separator;
|
|
62
|
+
private _options;
|
|
63
|
+
private _barDuration;
|
|
64
|
+
constructor(options: MTFOptions);
|
|
65
|
+
attach(chart: IChartApi, mainSeries: ISeriesApi<any>, mainData: OHLCData[]): void;
|
|
66
|
+
detach(): void;
|
|
67
|
+
update(mainData: OHLCData[], options?: Partial<MTFOptions>): void;
|
|
68
|
+
updateColumnData(columnIndex: number, data: OHLCData[]): void;
|
|
69
|
+
private _createHTFSeries;
|
|
70
|
+
private _removeHTFSeries;
|
|
71
|
+
private _render;
|
|
72
|
+
private _detectBarDuration;
|
|
73
|
+
private _getTimeAsNumber;
|
|
74
|
+
private _resolveOptions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type AggregationPeriod = '4H' | '8H' | '12H' | '1D' | '1W' | '1M' | '3M' | '6M' | '1Y';
|
|
78
|
+
declare function aggregateOHLC(data: OHLCData[], period: AggregationPeriod): OHLCData[];
|
|
79
|
+
declare function aggregateToWeekly(data: OHLCData[]): OHLCData[];
|
|
80
|
+
declare function aggregateToMonthly(data: OHLCData[]): OHLCData[];
|
|
81
|
+
|
|
82
|
+
export { type AggregationPeriod, type HTFColumnConfig, type HTFTimeframe, type MTFOptions, MTFPrimitive, type OHLCData, aggregateOHLC, aggregateToMonthly, aggregateToWeekly };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Time, IChartApi, ISeriesApi } from 'lightweight-charts';
|
|
2
|
+
|
|
3
|
+
/** OHLC candle data for a single timeframe period */
|
|
4
|
+
interface OHLCData {
|
|
5
|
+
time: Time;
|
|
6
|
+
open: number;
|
|
7
|
+
high: number;
|
|
8
|
+
low: number;
|
|
9
|
+
close: number;
|
|
10
|
+
volume?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Supported higher timeframes — any string label is accepted */
|
|
13
|
+
type HTFTimeframe = string;
|
|
14
|
+
/** Configuration for a single HTF column displayed to the right */
|
|
15
|
+
interface HTFColumnConfig {
|
|
16
|
+
/** Timeframe label (e.g., '1W', '1M') */
|
|
17
|
+
timeframe: HTFTimeframe;
|
|
18
|
+
/** OHLC data for this timeframe — most recent N candles */
|
|
19
|
+
data: OHLCData[];
|
|
20
|
+
/** Number of candles to display (default: 6) */
|
|
21
|
+
candleCount?: number;
|
|
22
|
+
/** Up candle color */
|
|
23
|
+
upColor?: string;
|
|
24
|
+
/** Down candle color */
|
|
25
|
+
downColor?: string;
|
|
26
|
+
/** Wick color for up candles (defaults to upColor) */
|
|
27
|
+
wickUpColor?: string;
|
|
28
|
+
/** Wick color for down candles (defaults to downColor) */
|
|
29
|
+
wickDownColor?: string;
|
|
30
|
+
/** Border visible on candle bodies (default: false) */
|
|
31
|
+
borderVisible?: boolean;
|
|
32
|
+
/** Border up color */
|
|
33
|
+
borderUpColor?: string;
|
|
34
|
+
/** Border down color */
|
|
35
|
+
borderDownColor?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Options for the MTF plugin */
|
|
38
|
+
interface MTFOptions {
|
|
39
|
+
/** Array of HTF columns to display, ordered left-to-right */
|
|
40
|
+
columns: HTFColumnConfig[];
|
|
41
|
+
/** Number of whitespace bars between main chart and first HTF column (default: 5) */
|
|
42
|
+
gap?: number;
|
|
43
|
+
/** Number of whitespace bars between HTF columns (default: 2) */
|
|
44
|
+
columnGap?: number;
|
|
45
|
+
/** Show separator lines between columns (default: true) */
|
|
46
|
+
showSeparators?: boolean;
|
|
47
|
+
/** Separator line color (default: 'rgba(255,255,255,0.08)') */
|
|
48
|
+
separatorColor?: string;
|
|
49
|
+
/** Show timeframe labels (default: true) */
|
|
50
|
+
showLabels?: boolean;
|
|
51
|
+
/** Label font (default: '11px sans-serif') */
|
|
52
|
+
labelFont?: string;
|
|
53
|
+
/** Label color (default: 'rgba(255,255,255,0.5)') */
|
|
54
|
+
labelColor?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
declare class MTFPrimitive {
|
|
58
|
+
private _chart;
|
|
59
|
+
private _mainSeries;
|
|
60
|
+
private _htfSeries;
|
|
61
|
+
private _separator;
|
|
62
|
+
private _options;
|
|
63
|
+
private _barDuration;
|
|
64
|
+
constructor(options: MTFOptions);
|
|
65
|
+
attach(chart: IChartApi, mainSeries: ISeriesApi<any>, mainData: OHLCData[]): void;
|
|
66
|
+
detach(): void;
|
|
67
|
+
update(mainData: OHLCData[], options?: Partial<MTFOptions>): void;
|
|
68
|
+
updateColumnData(columnIndex: number, data: OHLCData[]): void;
|
|
69
|
+
private _createHTFSeries;
|
|
70
|
+
private _removeHTFSeries;
|
|
71
|
+
private _render;
|
|
72
|
+
private _detectBarDuration;
|
|
73
|
+
private _getTimeAsNumber;
|
|
74
|
+
private _resolveOptions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type AggregationPeriod = '4H' | '8H' | '12H' | '1D' | '1W' | '1M' | '3M' | '6M' | '1Y';
|
|
78
|
+
declare function aggregateOHLC(data: OHLCData[], period: AggregationPeriod): OHLCData[];
|
|
79
|
+
declare function aggregateToWeekly(data: OHLCData[]): OHLCData[];
|
|
80
|
+
declare function aggregateToMonthly(data: OHLCData[]): OHLCData[];
|
|
81
|
+
|
|
82
|
+
export { type AggregationPeriod, type HTFColumnConfig, type HTFTimeframe, type MTFOptions, MTFPrimitive, type OHLCData, aggregateOHLC, aggregateToMonthly, aggregateToWeekly };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
MTFPrimitive: () => MTFPrimitive,
|
|
24
|
+
aggregateOHLC: () => aggregateOHLC,
|
|
25
|
+
aggregateToMonthly: () => aggregateToMonthly,
|
|
26
|
+
aggregateToWeekly: () => aggregateToWeekly
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/mtf-primitive.ts
|
|
31
|
+
var import_lightweight_charts = require("lightweight-charts");
|
|
32
|
+
var DEFAULT_OPTIONS = {
|
|
33
|
+
gap: 5,
|
|
34
|
+
columnGap: 2,
|
|
35
|
+
showSeparators: true,
|
|
36
|
+
separatorColor: "rgba(255,255,255,0.08)",
|
|
37
|
+
showLabels: true,
|
|
38
|
+
labelFont: "11px sans-serif",
|
|
39
|
+
labelColor: "rgba(255,255,255,0.5)"
|
|
40
|
+
};
|
|
41
|
+
var DEFAULT_COLUMN = {
|
|
42
|
+
candleCount: 6,
|
|
43
|
+
upColor: "#26a69a",
|
|
44
|
+
downColor: "#ef5350",
|
|
45
|
+
wickUpColor: "#26a69a",
|
|
46
|
+
wickDownColor: "#ef5350",
|
|
47
|
+
borderVisible: false,
|
|
48
|
+
borderUpColor: "#26a69a",
|
|
49
|
+
borderDownColor: "#ef5350"
|
|
50
|
+
};
|
|
51
|
+
var MTFPrimitive = class {
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this._chart = null;
|
|
54
|
+
this._mainSeries = null;
|
|
55
|
+
this._htfSeries = [];
|
|
56
|
+
this._separator = null;
|
|
57
|
+
this._barDuration = 86400;
|
|
58
|
+
this._options = this._resolveOptions(options);
|
|
59
|
+
}
|
|
60
|
+
attach(chart, mainSeries, mainData) {
|
|
61
|
+
this._chart = chart;
|
|
62
|
+
this._mainSeries = mainSeries;
|
|
63
|
+
this._barDuration = this._detectBarDuration(mainData);
|
|
64
|
+
this._createHTFSeries();
|
|
65
|
+
this._render(mainData);
|
|
66
|
+
}
|
|
67
|
+
detach() {
|
|
68
|
+
this._removeHTFSeries();
|
|
69
|
+
if (this._separator && this._mainSeries) {
|
|
70
|
+
this._mainSeries.detachPrimitive(this._separator);
|
|
71
|
+
this._separator = null;
|
|
72
|
+
}
|
|
73
|
+
this._chart = null;
|
|
74
|
+
this._mainSeries = null;
|
|
75
|
+
}
|
|
76
|
+
update(mainData, options) {
|
|
77
|
+
if (options) {
|
|
78
|
+
this._options = this._resolveOptions({ ...this._options, ...options });
|
|
79
|
+
this._removeHTFSeries();
|
|
80
|
+
this._createHTFSeries();
|
|
81
|
+
}
|
|
82
|
+
this._barDuration = this._detectBarDuration(mainData);
|
|
83
|
+
this._render(mainData);
|
|
84
|
+
}
|
|
85
|
+
updateColumnData(columnIndex, data) {
|
|
86
|
+
if (columnIndex < this._options.columns.length) {
|
|
87
|
+
this._options.columns[columnIndex].data = data;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
_createHTFSeries() {
|
|
91
|
+
if (!this._chart) return;
|
|
92
|
+
for (const col of this._options.columns) {
|
|
93
|
+
const seriesOptions = {
|
|
94
|
+
upColor: col.upColor,
|
|
95
|
+
downColor: col.downColor,
|
|
96
|
+
wickUpColor: col.wickUpColor,
|
|
97
|
+
wickDownColor: col.wickDownColor,
|
|
98
|
+
borderVisible: col.borderVisible,
|
|
99
|
+
borderUpColor: col.borderUpColor,
|
|
100
|
+
borderDownColor: col.borderDownColor
|
|
101
|
+
};
|
|
102
|
+
const series = this._chart.addSeries(import_lightweight_charts.CandlestickSeries, seriesOptions);
|
|
103
|
+
this._htfSeries.push(series);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
_removeHTFSeries() {
|
|
107
|
+
if (!this._chart) return;
|
|
108
|
+
for (const series of this._htfSeries) {
|
|
109
|
+
this._chart.removeSeries(series);
|
|
110
|
+
}
|
|
111
|
+
this._htfSeries = [];
|
|
112
|
+
}
|
|
113
|
+
_render(mainData) {
|
|
114
|
+
if (!this._chart || !this._mainSeries || mainData.length === 0) return;
|
|
115
|
+
const lastMainTime = this._getTimeAsNumber(mainData[mainData.length - 1].time);
|
|
116
|
+
const { columns, gap, columnGap } = this._options;
|
|
117
|
+
let currentOffset = gap;
|
|
118
|
+
const columnPositions = [];
|
|
119
|
+
for (let i = 0; i < columns.length; i++) {
|
|
120
|
+
const col = columns[i];
|
|
121
|
+
const visibleData = col.data.slice(-col.candleCount);
|
|
122
|
+
const count = visibleData.length;
|
|
123
|
+
columnPositions.push({ startOffset: currentOffset, count });
|
|
124
|
+
const htfData = visibleData.map((bar, idx) => ({
|
|
125
|
+
time: lastMainTime + (currentOffset + idx + 1) * this._barDuration,
|
|
126
|
+
open: bar.open,
|
|
127
|
+
high: bar.high,
|
|
128
|
+
low: bar.low,
|
|
129
|
+
close: bar.close
|
|
130
|
+
}));
|
|
131
|
+
this._htfSeries[i].setData(htfData);
|
|
132
|
+
currentOffset += count + columnGap;
|
|
133
|
+
}
|
|
134
|
+
const totalBars = currentOffset + 2;
|
|
135
|
+
const whitespace = [];
|
|
136
|
+
for (let i = 1; i <= totalBars; i++) {
|
|
137
|
+
whitespace.push({ time: lastMainTime + i * this._barDuration });
|
|
138
|
+
}
|
|
139
|
+
this._mainSeries.setData([...mainData, ...whitespace]);
|
|
140
|
+
if (this._separator && this._mainSeries) {
|
|
141
|
+
this._mainSeries.detachPrimitive(this._separator);
|
|
142
|
+
}
|
|
143
|
+
if (this._options.showSeparators || this._options.showLabels) {
|
|
144
|
+
this._separator = new MTFSeparatorPrimitive(
|
|
145
|
+
this._options,
|
|
146
|
+
lastMainTime,
|
|
147
|
+
this._barDuration,
|
|
148
|
+
columnPositions
|
|
149
|
+
);
|
|
150
|
+
this._mainSeries.attachPrimitive(this._separator);
|
|
151
|
+
}
|
|
152
|
+
this._chart.timeScale().fitContent();
|
|
153
|
+
}
|
|
154
|
+
_detectBarDuration(data) {
|
|
155
|
+
if (data.length < 2) return 86400;
|
|
156
|
+
const t1 = this._getTimeAsNumber(data[data.length - 1].time);
|
|
157
|
+
const t2 = this._getTimeAsNumber(data[data.length - 2].time);
|
|
158
|
+
return t1 - t2;
|
|
159
|
+
}
|
|
160
|
+
_getTimeAsNumber(time) {
|
|
161
|
+
if (typeof time === "number") return time;
|
|
162
|
+
if (typeof time === "string") return Math.floor(new Date(time).getTime() / 1e3);
|
|
163
|
+
return Math.floor(
|
|
164
|
+
(/* @__PURE__ */ new Date(`${time.year}-${String(time.month).padStart(2, "0")}-${String(time.day).padStart(2, "0")}`)).getTime() / 1e3
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
_resolveOptions(options) {
|
|
168
|
+
const columns = options.columns.map((col) => ({
|
|
169
|
+
timeframe: col.timeframe,
|
|
170
|
+
data: col.data,
|
|
171
|
+
candleCount: col.candleCount ?? DEFAULT_COLUMN.candleCount,
|
|
172
|
+
upColor: col.upColor ?? DEFAULT_COLUMN.upColor,
|
|
173
|
+
downColor: col.downColor ?? DEFAULT_COLUMN.downColor,
|
|
174
|
+
wickUpColor: col.wickUpColor ?? col.upColor ?? DEFAULT_COLUMN.wickUpColor,
|
|
175
|
+
wickDownColor: col.wickDownColor ?? col.downColor ?? DEFAULT_COLUMN.wickDownColor,
|
|
176
|
+
borderVisible: col.borderVisible ?? DEFAULT_COLUMN.borderVisible,
|
|
177
|
+
borderUpColor: col.borderUpColor ?? col.upColor ?? DEFAULT_COLUMN.borderUpColor,
|
|
178
|
+
borderDownColor: col.borderDownColor ?? col.downColor ?? DEFAULT_COLUMN.borderDownColor
|
|
179
|
+
}));
|
|
180
|
+
return {
|
|
181
|
+
gap: options.gap ?? DEFAULT_OPTIONS.gap,
|
|
182
|
+
columnGap: options.columnGap ?? DEFAULT_OPTIONS.columnGap,
|
|
183
|
+
showSeparators: options.showSeparators ?? DEFAULT_OPTIONS.showSeparators,
|
|
184
|
+
separatorColor: options.separatorColor ?? DEFAULT_OPTIONS.separatorColor,
|
|
185
|
+
showLabels: options.showLabels ?? DEFAULT_OPTIONS.showLabels,
|
|
186
|
+
labelFont: options.labelFont ?? DEFAULT_OPTIONS.labelFont,
|
|
187
|
+
labelColor: options.labelColor ?? DEFAULT_OPTIONS.labelColor,
|
|
188
|
+
columns
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var MTFSeparatorPrimitive = class {
|
|
193
|
+
constructor(options, lastMainTime, barDuration, columnPositions) {
|
|
194
|
+
this._options = options;
|
|
195
|
+
this._lastMainTime = lastMainTime;
|
|
196
|
+
this._barDuration = barDuration;
|
|
197
|
+
this._columnPositions = columnPositions;
|
|
198
|
+
}
|
|
199
|
+
attached() {
|
|
200
|
+
}
|
|
201
|
+
detached() {
|
|
202
|
+
}
|
|
203
|
+
updateAllViews() {
|
|
204
|
+
}
|
|
205
|
+
paneViews() {
|
|
206
|
+
return [{ renderer: () => this, zOrder: () => "bottom" }];
|
|
207
|
+
}
|
|
208
|
+
draw(target) {
|
|
209
|
+
target.useBitmapCoordinateSpace((scope) => {
|
|
210
|
+
const ctx = scope.context;
|
|
211
|
+
const hRatio = scope.horizontalPixelRatio;
|
|
212
|
+
const vRatio = scope.verticalPixelRatio;
|
|
213
|
+
const chartHeight = scope.bitmapSize.height;
|
|
214
|
+
const timeScale = this._chart?.timeScale?.();
|
|
215
|
+
if (!timeScale) return;
|
|
216
|
+
if (this._options.showSeparators) {
|
|
217
|
+
ctx.strokeStyle = this._options.separatorColor;
|
|
218
|
+
ctx.lineWidth = 1 * hRatio;
|
|
219
|
+
ctx.setLineDash([4 * hRatio, 3 * hRatio]);
|
|
220
|
+
for (const pos of this._columnPositions) {
|
|
221
|
+
const sepTime = this._lastMainTime + pos.startOffset * this._barDuration;
|
|
222
|
+
const x = timeScale.timeToCoordinate(sepTime);
|
|
223
|
+
if (x === null) continue;
|
|
224
|
+
const px = x * hRatio;
|
|
225
|
+
ctx.beginPath();
|
|
226
|
+
ctx.moveTo(px, 0);
|
|
227
|
+
ctx.lineTo(px, chartHeight);
|
|
228
|
+
ctx.stroke();
|
|
229
|
+
}
|
|
230
|
+
ctx.setLineDash([]);
|
|
231
|
+
}
|
|
232
|
+
if (this._options.showLabels) {
|
|
233
|
+
ctx.font = this._options.labelFont;
|
|
234
|
+
ctx.fillStyle = this._options.labelColor;
|
|
235
|
+
ctx.textAlign = "center";
|
|
236
|
+
ctx.textBaseline = "top";
|
|
237
|
+
for (let i = 0; i < this._columnPositions.length; i++) {
|
|
238
|
+
const pos = this._columnPositions[i];
|
|
239
|
+
const midOffset = pos.startOffset + Math.floor(pos.count / 2) + 1;
|
|
240
|
+
const labelTime = this._lastMainTime + midOffset * this._barDuration;
|
|
241
|
+
const x = timeScale.timeToCoordinate(labelTime);
|
|
242
|
+
if (x === null) continue;
|
|
243
|
+
ctx.fillText(this._options.columns[i].timeframe, x * hRatio, 8 * vRatio);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// src/data-aggregator.ts
|
|
251
|
+
function aggregateOHLC(data, period) {
|
|
252
|
+
const getPeriodKey = getPeriodKeyFn(period);
|
|
253
|
+
return aggregateByPeriod(data, getPeriodKey);
|
|
254
|
+
}
|
|
255
|
+
function aggregateToWeekly(data) {
|
|
256
|
+
return aggregateByPeriod(data, getWeekStart);
|
|
257
|
+
}
|
|
258
|
+
function aggregateToMonthly(data) {
|
|
259
|
+
return aggregateByPeriod(data, getMonthStart);
|
|
260
|
+
}
|
|
261
|
+
function getPeriodKeyFn(period) {
|
|
262
|
+
switch (period) {
|
|
263
|
+
case "4H":
|
|
264
|
+
return (ts) => getHourBucket(ts, 4);
|
|
265
|
+
case "8H":
|
|
266
|
+
return (ts) => getHourBucket(ts, 8);
|
|
267
|
+
case "12H":
|
|
268
|
+
return (ts) => getHourBucket(ts, 12);
|
|
269
|
+
case "1D":
|
|
270
|
+
return getDayStart;
|
|
271
|
+
case "1W":
|
|
272
|
+
return getWeekStart;
|
|
273
|
+
case "1M":
|
|
274
|
+
return getMonthStart;
|
|
275
|
+
case "3M":
|
|
276
|
+
return (ts) => getQuarterStart(ts, 3);
|
|
277
|
+
case "6M":
|
|
278
|
+
return (ts) => getQuarterStart(ts, 6);
|
|
279
|
+
case "1Y":
|
|
280
|
+
return getYearStart;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function aggregateByPeriod(data, getPeriodKey) {
|
|
284
|
+
if (data.length === 0) return [];
|
|
285
|
+
const periods = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const bar of data) {
|
|
287
|
+
const timestamp = timeToTimestamp(bar.time);
|
|
288
|
+
const periodKey = getPeriodKey(timestamp);
|
|
289
|
+
const existing = periods.get(periodKey);
|
|
290
|
+
if (!existing) {
|
|
291
|
+
periods.set(periodKey, {
|
|
292
|
+
time: bar.time,
|
|
293
|
+
open: bar.open,
|
|
294
|
+
high: bar.high,
|
|
295
|
+
low: bar.low,
|
|
296
|
+
close: bar.close,
|
|
297
|
+
volume: bar.volume ?? 0
|
|
298
|
+
});
|
|
299
|
+
} else {
|
|
300
|
+
existing.high = Math.max(existing.high, bar.high);
|
|
301
|
+
existing.low = Math.min(existing.low, bar.low);
|
|
302
|
+
existing.close = bar.close;
|
|
303
|
+
existing.volume = (existing.volume ?? 0) + (bar.volume ?? 0);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return Array.from(periods.values());
|
|
307
|
+
}
|
|
308
|
+
function timeToTimestamp(time) {
|
|
309
|
+
if (typeof time === "number") return time;
|
|
310
|
+
if (typeof time === "string") return Math.floor(new Date(time).getTime() / 1e3);
|
|
311
|
+
return Math.floor(
|
|
312
|
+
(/* @__PURE__ */ new Date(`${time.year}-${String(time.month).padStart(2, "0")}-${String(time.day).padStart(2, "0")}`)).getTime() / 1e3
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
function getHourBucket(timestamp, hours) {
|
|
316
|
+
const bucketSeconds = hours * 3600;
|
|
317
|
+
return Math.floor(timestamp / bucketSeconds) * bucketSeconds;
|
|
318
|
+
}
|
|
319
|
+
function getDayStart(timestamp) {
|
|
320
|
+
const date = new Date(timestamp * 1e3);
|
|
321
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
322
|
+
return Math.floor(date.getTime() / 1e3);
|
|
323
|
+
}
|
|
324
|
+
function getWeekStart(timestamp) {
|
|
325
|
+
const date = new Date(timestamp * 1e3);
|
|
326
|
+
const day = date.getUTCDay();
|
|
327
|
+
const diff = day === 0 ? 6 : day - 1;
|
|
328
|
+
date.setUTCDate(date.getUTCDate() - diff);
|
|
329
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
330
|
+
return Math.floor(date.getTime() / 1e3);
|
|
331
|
+
}
|
|
332
|
+
function getMonthStart(timestamp) {
|
|
333
|
+
const date = new Date(timestamp * 1e3);
|
|
334
|
+
date.setUTCDate(1);
|
|
335
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
336
|
+
return Math.floor(date.getTime() / 1e3);
|
|
337
|
+
}
|
|
338
|
+
function getQuarterStart(timestamp, months) {
|
|
339
|
+
const date = new Date(timestamp * 1e3);
|
|
340
|
+
const month = date.getUTCMonth();
|
|
341
|
+
const quarterMonth = Math.floor(month / months) * months;
|
|
342
|
+
date.setUTCMonth(quarterMonth, 1);
|
|
343
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
344
|
+
return Math.floor(date.getTime() / 1e3);
|
|
345
|
+
}
|
|
346
|
+
function getYearStart(timestamp) {
|
|
347
|
+
const date = new Date(timestamp * 1e3);
|
|
348
|
+
date.setUTCMonth(0, 1);
|
|
349
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
350
|
+
return Math.floor(date.getTime() / 1e3);
|
|
351
|
+
}
|
|
352
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
353
|
+
0 && (module.exports = {
|
|
354
|
+
MTFPrimitive,
|
|
355
|
+
aggregateOHLC,
|
|
356
|
+
aggregateToMonthly,
|
|
357
|
+
aggregateToWeekly
|
|
358
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// src/mtf-primitive.ts
|
|
2
|
+
import { CandlestickSeries } from "lightweight-charts";
|
|
3
|
+
var DEFAULT_OPTIONS = {
|
|
4
|
+
gap: 5,
|
|
5
|
+
columnGap: 2,
|
|
6
|
+
showSeparators: true,
|
|
7
|
+
separatorColor: "rgba(255,255,255,0.08)",
|
|
8
|
+
showLabels: true,
|
|
9
|
+
labelFont: "11px sans-serif",
|
|
10
|
+
labelColor: "rgba(255,255,255,0.5)"
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_COLUMN = {
|
|
13
|
+
candleCount: 6,
|
|
14
|
+
upColor: "#26a69a",
|
|
15
|
+
downColor: "#ef5350",
|
|
16
|
+
wickUpColor: "#26a69a",
|
|
17
|
+
wickDownColor: "#ef5350",
|
|
18
|
+
borderVisible: false,
|
|
19
|
+
borderUpColor: "#26a69a",
|
|
20
|
+
borderDownColor: "#ef5350"
|
|
21
|
+
};
|
|
22
|
+
var MTFPrimitive = class {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this._chart = null;
|
|
25
|
+
this._mainSeries = null;
|
|
26
|
+
this._htfSeries = [];
|
|
27
|
+
this._separator = null;
|
|
28
|
+
this._barDuration = 86400;
|
|
29
|
+
this._options = this._resolveOptions(options);
|
|
30
|
+
}
|
|
31
|
+
attach(chart, mainSeries, mainData) {
|
|
32
|
+
this._chart = chart;
|
|
33
|
+
this._mainSeries = mainSeries;
|
|
34
|
+
this._barDuration = this._detectBarDuration(mainData);
|
|
35
|
+
this._createHTFSeries();
|
|
36
|
+
this._render(mainData);
|
|
37
|
+
}
|
|
38
|
+
detach() {
|
|
39
|
+
this._removeHTFSeries();
|
|
40
|
+
if (this._separator && this._mainSeries) {
|
|
41
|
+
this._mainSeries.detachPrimitive(this._separator);
|
|
42
|
+
this._separator = null;
|
|
43
|
+
}
|
|
44
|
+
this._chart = null;
|
|
45
|
+
this._mainSeries = null;
|
|
46
|
+
}
|
|
47
|
+
update(mainData, options) {
|
|
48
|
+
if (options) {
|
|
49
|
+
this._options = this._resolveOptions({ ...this._options, ...options });
|
|
50
|
+
this._removeHTFSeries();
|
|
51
|
+
this._createHTFSeries();
|
|
52
|
+
}
|
|
53
|
+
this._barDuration = this._detectBarDuration(mainData);
|
|
54
|
+
this._render(mainData);
|
|
55
|
+
}
|
|
56
|
+
updateColumnData(columnIndex, data) {
|
|
57
|
+
if (columnIndex < this._options.columns.length) {
|
|
58
|
+
this._options.columns[columnIndex].data = data;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
_createHTFSeries() {
|
|
62
|
+
if (!this._chart) return;
|
|
63
|
+
for (const col of this._options.columns) {
|
|
64
|
+
const seriesOptions = {
|
|
65
|
+
upColor: col.upColor,
|
|
66
|
+
downColor: col.downColor,
|
|
67
|
+
wickUpColor: col.wickUpColor,
|
|
68
|
+
wickDownColor: col.wickDownColor,
|
|
69
|
+
borderVisible: col.borderVisible,
|
|
70
|
+
borderUpColor: col.borderUpColor,
|
|
71
|
+
borderDownColor: col.borderDownColor
|
|
72
|
+
};
|
|
73
|
+
const series = this._chart.addSeries(CandlestickSeries, seriesOptions);
|
|
74
|
+
this._htfSeries.push(series);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
_removeHTFSeries() {
|
|
78
|
+
if (!this._chart) return;
|
|
79
|
+
for (const series of this._htfSeries) {
|
|
80
|
+
this._chart.removeSeries(series);
|
|
81
|
+
}
|
|
82
|
+
this._htfSeries = [];
|
|
83
|
+
}
|
|
84
|
+
_render(mainData) {
|
|
85
|
+
if (!this._chart || !this._mainSeries || mainData.length === 0) return;
|
|
86
|
+
const lastMainTime = this._getTimeAsNumber(mainData[mainData.length - 1].time);
|
|
87
|
+
const { columns, gap, columnGap } = this._options;
|
|
88
|
+
let currentOffset = gap;
|
|
89
|
+
const columnPositions = [];
|
|
90
|
+
for (let i = 0; i < columns.length; i++) {
|
|
91
|
+
const col = columns[i];
|
|
92
|
+
const visibleData = col.data.slice(-col.candleCount);
|
|
93
|
+
const count = visibleData.length;
|
|
94
|
+
columnPositions.push({ startOffset: currentOffset, count });
|
|
95
|
+
const htfData = visibleData.map((bar, idx) => ({
|
|
96
|
+
time: lastMainTime + (currentOffset + idx + 1) * this._barDuration,
|
|
97
|
+
open: bar.open,
|
|
98
|
+
high: bar.high,
|
|
99
|
+
low: bar.low,
|
|
100
|
+
close: bar.close
|
|
101
|
+
}));
|
|
102
|
+
this._htfSeries[i].setData(htfData);
|
|
103
|
+
currentOffset += count + columnGap;
|
|
104
|
+
}
|
|
105
|
+
const totalBars = currentOffset + 2;
|
|
106
|
+
const whitespace = [];
|
|
107
|
+
for (let i = 1; i <= totalBars; i++) {
|
|
108
|
+
whitespace.push({ time: lastMainTime + i * this._barDuration });
|
|
109
|
+
}
|
|
110
|
+
this._mainSeries.setData([...mainData, ...whitespace]);
|
|
111
|
+
if (this._separator && this._mainSeries) {
|
|
112
|
+
this._mainSeries.detachPrimitive(this._separator);
|
|
113
|
+
}
|
|
114
|
+
if (this._options.showSeparators || this._options.showLabels) {
|
|
115
|
+
this._separator = new MTFSeparatorPrimitive(
|
|
116
|
+
this._options,
|
|
117
|
+
lastMainTime,
|
|
118
|
+
this._barDuration,
|
|
119
|
+
columnPositions
|
|
120
|
+
);
|
|
121
|
+
this._mainSeries.attachPrimitive(this._separator);
|
|
122
|
+
}
|
|
123
|
+
this._chart.timeScale().fitContent();
|
|
124
|
+
}
|
|
125
|
+
_detectBarDuration(data) {
|
|
126
|
+
if (data.length < 2) return 86400;
|
|
127
|
+
const t1 = this._getTimeAsNumber(data[data.length - 1].time);
|
|
128
|
+
const t2 = this._getTimeAsNumber(data[data.length - 2].time);
|
|
129
|
+
return t1 - t2;
|
|
130
|
+
}
|
|
131
|
+
_getTimeAsNumber(time) {
|
|
132
|
+
if (typeof time === "number") return time;
|
|
133
|
+
if (typeof time === "string") return Math.floor(new Date(time).getTime() / 1e3);
|
|
134
|
+
return Math.floor(
|
|
135
|
+
(/* @__PURE__ */ new Date(`${time.year}-${String(time.month).padStart(2, "0")}-${String(time.day).padStart(2, "0")}`)).getTime() / 1e3
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
_resolveOptions(options) {
|
|
139
|
+
const columns = options.columns.map((col) => ({
|
|
140
|
+
timeframe: col.timeframe,
|
|
141
|
+
data: col.data,
|
|
142
|
+
candleCount: col.candleCount ?? DEFAULT_COLUMN.candleCount,
|
|
143
|
+
upColor: col.upColor ?? DEFAULT_COLUMN.upColor,
|
|
144
|
+
downColor: col.downColor ?? DEFAULT_COLUMN.downColor,
|
|
145
|
+
wickUpColor: col.wickUpColor ?? col.upColor ?? DEFAULT_COLUMN.wickUpColor,
|
|
146
|
+
wickDownColor: col.wickDownColor ?? col.downColor ?? DEFAULT_COLUMN.wickDownColor,
|
|
147
|
+
borderVisible: col.borderVisible ?? DEFAULT_COLUMN.borderVisible,
|
|
148
|
+
borderUpColor: col.borderUpColor ?? col.upColor ?? DEFAULT_COLUMN.borderUpColor,
|
|
149
|
+
borderDownColor: col.borderDownColor ?? col.downColor ?? DEFAULT_COLUMN.borderDownColor
|
|
150
|
+
}));
|
|
151
|
+
return {
|
|
152
|
+
gap: options.gap ?? DEFAULT_OPTIONS.gap,
|
|
153
|
+
columnGap: options.columnGap ?? DEFAULT_OPTIONS.columnGap,
|
|
154
|
+
showSeparators: options.showSeparators ?? DEFAULT_OPTIONS.showSeparators,
|
|
155
|
+
separatorColor: options.separatorColor ?? DEFAULT_OPTIONS.separatorColor,
|
|
156
|
+
showLabels: options.showLabels ?? DEFAULT_OPTIONS.showLabels,
|
|
157
|
+
labelFont: options.labelFont ?? DEFAULT_OPTIONS.labelFont,
|
|
158
|
+
labelColor: options.labelColor ?? DEFAULT_OPTIONS.labelColor,
|
|
159
|
+
columns
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
var MTFSeparatorPrimitive = class {
|
|
164
|
+
constructor(options, lastMainTime, barDuration, columnPositions) {
|
|
165
|
+
this._options = options;
|
|
166
|
+
this._lastMainTime = lastMainTime;
|
|
167
|
+
this._barDuration = barDuration;
|
|
168
|
+
this._columnPositions = columnPositions;
|
|
169
|
+
}
|
|
170
|
+
attached() {
|
|
171
|
+
}
|
|
172
|
+
detached() {
|
|
173
|
+
}
|
|
174
|
+
updateAllViews() {
|
|
175
|
+
}
|
|
176
|
+
paneViews() {
|
|
177
|
+
return [{ renderer: () => this, zOrder: () => "bottom" }];
|
|
178
|
+
}
|
|
179
|
+
draw(target) {
|
|
180
|
+
target.useBitmapCoordinateSpace((scope) => {
|
|
181
|
+
const ctx = scope.context;
|
|
182
|
+
const hRatio = scope.horizontalPixelRatio;
|
|
183
|
+
const vRatio = scope.verticalPixelRatio;
|
|
184
|
+
const chartHeight = scope.bitmapSize.height;
|
|
185
|
+
const timeScale = this._chart?.timeScale?.();
|
|
186
|
+
if (!timeScale) return;
|
|
187
|
+
if (this._options.showSeparators) {
|
|
188
|
+
ctx.strokeStyle = this._options.separatorColor;
|
|
189
|
+
ctx.lineWidth = 1 * hRatio;
|
|
190
|
+
ctx.setLineDash([4 * hRatio, 3 * hRatio]);
|
|
191
|
+
for (const pos of this._columnPositions) {
|
|
192
|
+
const sepTime = this._lastMainTime + pos.startOffset * this._barDuration;
|
|
193
|
+
const x = timeScale.timeToCoordinate(sepTime);
|
|
194
|
+
if (x === null) continue;
|
|
195
|
+
const px = x * hRatio;
|
|
196
|
+
ctx.beginPath();
|
|
197
|
+
ctx.moveTo(px, 0);
|
|
198
|
+
ctx.lineTo(px, chartHeight);
|
|
199
|
+
ctx.stroke();
|
|
200
|
+
}
|
|
201
|
+
ctx.setLineDash([]);
|
|
202
|
+
}
|
|
203
|
+
if (this._options.showLabels) {
|
|
204
|
+
ctx.font = this._options.labelFont;
|
|
205
|
+
ctx.fillStyle = this._options.labelColor;
|
|
206
|
+
ctx.textAlign = "center";
|
|
207
|
+
ctx.textBaseline = "top";
|
|
208
|
+
for (let i = 0; i < this._columnPositions.length; i++) {
|
|
209
|
+
const pos = this._columnPositions[i];
|
|
210
|
+
const midOffset = pos.startOffset + Math.floor(pos.count / 2) + 1;
|
|
211
|
+
const labelTime = this._lastMainTime + midOffset * this._barDuration;
|
|
212
|
+
const x = timeScale.timeToCoordinate(labelTime);
|
|
213
|
+
if (x === null) continue;
|
|
214
|
+
ctx.fillText(this._options.columns[i].timeframe, x * hRatio, 8 * vRatio);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/data-aggregator.ts
|
|
222
|
+
function aggregateOHLC(data, period) {
|
|
223
|
+
const getPeriodKey = getPeriodKeyFn(period);
|
|
224
|
+
return aggregateByPeriod(data, getPeriodKey);
|
|
225
|
+
}
|
|
226
|
+
function aggregateToWeekly(data) {
|
|
227
|
+
return aggregateByPeriod(data, getWeekStart);
|
|
228
|
+
}
|
|
229
|
+
function aggregateToMonthly(data) {
|
|
230
|
+
return aggregateByPeriod(data, getMonthStart);
|
|
231
|
+
}
|
|
232
|
+
function getPeriodKeyFn(period) {
|
|
233
|
+
switch (period) {
|
|
234
|
+
case "4H":
|
|
235
|
+
return (ts) => getHourBucket(ts, 4);
|
|
236
|
+
case "8H":
|
|
237
|
+
return (ts) => getHourBucket(ts, 8);
|
|
238
|
+
case "12H":
|
|
239
|
+
return (ts) => getHourBucket(ts, 12);
|
|
240
|
+
case "1D":
|
|
241
|
+
return getDayStart;
|
|
242
|
+
case "1W":
|
|
243
|
+
return getWeekStart;
|
|
244
|
+
case "1M":
|
|
245
|
+
return getMonthStart;
|
|
246
|
+
case "3M":
|
|
247
|
+
return (ts) => getQuarterStart(ts, 3);
|
|
248
|
+
case "6M":
|
|
249
|
+
return (ts) => getQuarterStart(ts, 6);
|
|
250
|
+
case "1Y":
|
|
251
|
+
return getYearStart;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function aggregateByPeriod(data, getPeriodKey) {
|
|
255
|
+
if (data.length === 0) return [];
|
|
256
|
+
const periods = /* @__PURE__ */ new Map();
|
|
257
|
+
for (const bar of data) {
|
|
258
|
+
const timestamp = timeToTimestamp(bar.time);
|
|
259
|
+
const periodKey = getPeriodKey(timestamp);
|
|
260
|
+
const existing = periods.get(periodKey);
|
|
261
|
+
if (!existing) {
|
|
262
|
+
periods.set(periodKey, {
|
|
263
|
+
time: bar.time,
|
|
264
|
+
open: bar.open,
|
|
265
|
+
high: bar.high,
|
|
266
|
+
low: bar.low,
|
|
267
|
+
close: bar.close,
|
|
268
|
+
volume: bar.volume ?? 0
|
|
269
|
+
});
|
|
270
|
+
} else {
|
|
271
|
+
existing.high = Math.max(existing.high, bar.high);
|
|
272
|
+
existing.low = Math.min(existing.low, bar.low);
|
|
273
|
+
existing.close = bar.close;
|
|
274
|
+
existing.volume = (existing.volume ?? 0) + (bar.volume ?? 0);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return Array.from(periods.values());
|
|
278
|
+
}
|
|
279
|
+
function timeToTimestamp(time) {
|
|
280
|
+
if (typeof time === "number") return time;
|
|
281
|
+
if (typeof time === "string") return Math.floor(new Date(time).getTime() / 1e3);
|
|
282
|
+
return Math.floor(
|
|
283
|
+
(/* @__PURE__ */ new Date(`${time.year}-${String(time.month).padStart(2, "0")}-${String(time.day).padStart(2, "0")}`)).getTime() / 1e3
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
function getHourBucket(timestamp, hours) {
|
|
287
|
+
const bucketSeconds = hours * 3600;
|
|
288
|
+
return Math.floor(timestamp / bucketSeconds) * bucketSeconds;
|
|
289
|
+
}
|
|
290
|
+
function getDayStart(timestamp) {
|
|
291
|
+
const date = new Date(timestamp * 1e3);
|
|
292
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
293
|
+
return Math.floor(date.getTime() / 1e3);
|
|
294
|
+
}
|
|
295
|
+
function getWeekStart(timestamp) {
|
|
296
|
+
const date = new Date(timestamp * 1e3);
|
|
297
|
+
const day = date.getUTCDay();
|
|
298
|
+
const diff = day === 0 ? 6 : day - 1;
|
|
299
|
+
date.setUTCDate(date.getUTCDate() - diff);
|
|
300
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
301
|
+
return Math.floor(date.getTime() / 1e3);
|
|
302
|
+
}
|
|
303
|
+
function getMonthStart(timestamp) {
|
|
304
|
+
const date = new Date(timestamp * 1e3);
|
|
305
|
+
date.setUTCDate(1);
|
|
306
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
307
|
+
return Math.floor(date.getTime() / 1e3);
|
|
308
|
+
}
|
|
309
|
+
function getQuarterStart(timestamp, months) {
|
|
310
|
+
const date = new Date(timestamp * 1e3);
|
|
311
|
+
const month = date.getUTCMonth();
|
|
312
|
+
const quarterMonth = Math.floor(month / months) * months;
|
|
313
|
+
date.setUTCMonth(quarterMonth, 1);
|
|
314
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
315
|
+
return Math.floor(date.getTime() / 1e3);
|
|
316
|
+
}
|
|
317
|
+
function getYearStart(timestamp) {
|
|
318
|
+
const date = new Date(timestamp * 1e3);
|
|
319
|
+
date.setUTCMonth(0, 1);
|
|
320
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
321
|
+
return Math.floor(date.getTime() / 1e3);
|
|
322
|
+
}
|
|
323
|
+
export {
|
|
324
|
+
MTFPrimitive,
|
|
325
|
+
aggregateOHLC,
|
|
326
|
+
aggregateToMonthly,
|
|
327
|
+
aggregateToWeekly
|
|
328
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lightweight-charts-mtf",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-timeframe candle display plugin for TradingView Lightweight Charts",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/nsulistiyawan/lightweight-charts-mtf.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/nsulistiyawan/lightweight-charts-mtf#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/nsulistiyawan/lightweight-charts-mtf/issues"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"tradingview",
|
|
28
|
+
"lightweight-charts",
|
|
29
|
+
"multi-timeframe",
|
|
30
|
+
"mtf",
|
|
31
|
+
"plugin",
|
|
32
|
+
"candlestick",
|
|
33
|
+
"trading"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"lightweight-charts": ">=4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"lightweight-charts": "^5.0.0",
|
|
41
|
+
"tsup": "^8.0.0",
|
|
42
|
+
"typescript": "^5.4.0",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
44
|
+
}
|
|
45
|
+
}
|