trendkit 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 +122 -0
- package/dist/chunk-AEC4653J.js +1220 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +1 -0
- package/dist/react/index.d.ts +35 -0
- package/dist/react/index.js +29 -0
- package/package.json +79 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { Time, Logical, IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A point a drawing is pinned to.
|
|
5
|
+
*
|
|
6
|
+
* ANCHORED IN DATA SPACE, NEVER PIXELS. A trendline drawn through two candles
|
|
7
|
+
* has to stay through those candles when the user pans, zooms or resizes the
|
|
8
|
+
* window; a pixel offset would slide off them immediately. So a point is a
|
|
9
|
+
* price and a time, and pixel positions are recomputed every frame.
|
|
10
|
+
*
|
|
11
|
+
* `logical` is the bar INDEX, carried alongside `time` for one specific case:
|
|
12
|
+
* a user can drag a handle past the last bar, into the empty space on the
|
|
13
|
+
* right where the chart projects future sessions. There is no `Time` there to
|
|
14
|
+
* convert, and `timeToCoordinate` returns null. The logical index keeps
|
|
15
|
+
* counting, so it is what resolves the point when time cannot.
|
|
16
|
+
*
|
|
17
|
+
* Both are stored rather than deriving one from the other, because the
|
|
18
|
+
* mapping between them is not stable: it changes whenever bars are added, and
|
|
19
|
+
* an index recorded against yesterday's data means a different day tomorrow.
|
|
20
|
+
* Time is the durable identity; logical is the fallback for points that have
|
|
21
|
+
* no time.
|
|
22
|
+
*/
|
|
23
|
+
interface Point {
|
|
24
|
+
/** Session the point sits on. Null only for points beyond the last bar. */
|
|
25
|
+
time: Time | null;
|
|
26
|
+
/** Bar index, fractional between bars. Survives past the end of the data. */
|
|
27
|
+
logical: Logical;
|
|
28
|
+
/** Y position, in the instrument's own units. */
|
|
29
|
+
price: number;
|
|
30
|
+
}
|
|
31
|
+
/** A point already resolved to canvas pixels, ready to draw. */
|
|
32
|
+
interface ScreenPoint {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
}
|
|
36
|
+
/** Every drawing kind v1 understands. */
|
|
37
|
+
type ToolId = "trendline" | "horizontal-ray" | "fibonacci" | "rectangle" | "measure";
|
|
38
|
+
/** Per-drawing appearance. Everything optional; the theme supplies defaults. */
|
|
39
|
+
interface DrawingStyle {
|
|
40
|
+
colour?: string;
|
|
41
|
+
lineWidth?: number;
|
|
42
|
+
lineStyle?: "solid" | "dashed" | "dotted";
|
|
43
|
+
/** Fill for shapes that have an interior (rectangle, Fibonacci bands). */
|
|
44
|
+
fill?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* One drawing.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately a plain data object rather than a class with behaviour. It is
|
|
50
|
+
* what `toJSON` emits, so it has to survive a round trip through storage
|
|
51
|
+
* belonging to someone else's application -- and a class would not.
|
|
52
|
+
*/
|
|
53
|
+
interface Drawing {
|
|
54
|
+
id: string;
|
|
55
|
+
tool: ToolId;
|
|
56
|
+
points: Point[];
|
|
57
|
+
style?: DrawingStyle;
|
|
58
|
+
/** Set while a drawing is being created and not yet committed. */
|
|
59
|
+
draft?: boolean;
|
|
60
|
+
/** User-supplied label, where the tool renders one. */
|
|
61
|
+
text?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The persisted form.
|
|
66
|
+
*
|
|
67
|
+
* Versioned from the first release. Drawings end up in someone else's
|
|
68
|
+
* database, and the day the shape has to change, a stored blob with no
|
|
69
|
+
* version is one you can only guess at. A number costs nothing now and is
|
|
70
|
+
* unobtainable later.
|
|
71
|
+
*/
|
|
72
|
+
interface TrendkitSnapshot {
|
|
73
|
+
version: 1;
|
|
74
|
+
drawings: Drawing[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The drawings, and who is selected.
|
|
79
|
+
*
|
|
80
|
+
* A plain observable rather than a state library: this ships to other
|
|
81
|
+
* people's applications, and a library that drags in its own store is a
|
|
82
|
+
* library people refuse on bundle size alone.
|
|
83
|
+
*/
|
|
84
|
+
declare class DrawingStore {
|
|
85
|
+
private items;
|
|
86
|
+
private selected;
|
|
87
|
+
private readonly listeners;
|
|
88
|
+
/** Undo history. Snapshots, not diffs -- drawings are small and few. */
|
|
89
|
+
private readonly past;
|
|
90
|
+
private readonly future;
|
|
91
|
+
subscribe(listener: () => void): () => void;
|
|
92
|
+
private notify;
|
|
93
|
+
/** Snapshot for undo. Called before any mutation that should be undoable. */
|
|
94
|
+
private checkpoint;
|
|
95
|
+
all(): readonly Drawing[];
|
|
96
|
+
selectedId(): string | null;
|
|
97
|
+
get(id: string): Drawing | undefined;
|
|
98
|
+
add(drawing: Drawing, options?: {
|
|
99
|
+
undoable?: boolean;
|
|
100
|
+
}): void;
|
|
101
|
+
update(id: string, patch: Partial<Drawing>, options?: {
|
|
102
|
+
undoable?: boolean;
|
|
103
|
+
}): void;
|
|
104
|
+
remove(id: string, options?: {
|
|
105
|
+
undoable?: boolean;
|
|
106
|
+
}): void;
|
|
107
|
+
select(id: string | null): void;
|
|
108
|
+
replaceAll(drawings: Drawing[]): void;
|
|
109
|
+
undo(): void;
|
|
110
|
+
redo(): void;
|
|
111
|
+
canUndo(): boolean;
|
|
112
|
+
canRedo(): boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Colours, supplied by the host application.
|
|
117
|
+
*
|
|
118
|
+
* Deliberately not read from CSS custom properties, even though that would be
|
|
119
|
+
* convenient for a Tailwind app. The chart is a canvas: it cannot inherit CSS,
|
|
120
|
+
* and a library that reached into `getComputedStyle` would work in exactly one
|
|
121
|
+
* kind of host and silently produce black-on-black anywhere else.
|
|
122
|
+
*/
|
|
123
|
+
interface Theme {
|
|
124
|
+
/** Default colour for new drawings. */
|
|
125
|
+
accent: string;
|
|
126
|
+
/** Outline and fill of a selected drawing's handles. */
|
|
127
|
+
selection: string;
|
|
128
|
+
/** Label text. */
|
|
129
|
+
text: string;
|
|
130
|
+
/** Label plate behind text. */
|
|
131
|
+
labelBackground: string;
|
|
132
|
+
/** Fill for shapes with an interior. Should carry its own alpha. */
|
|
133
|
+
fill: string;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Light-mode defaults.
|
|
137
|
+
*
|
|
138
|
+
* The accent is the same teal Lightweight Charts uses for rising candles, so
|
|
139
|
+
* a drawing on a default chart looks deliberate rather than pasted on.
|
|
140
|
+
*/
|
|
141
|
+
declare const LIGHT_THEME: Theme;
|
|
142
|
+
declare const DARK_THEME: Theme;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What a tool needs in order to draw itself and answer hit tests.
|
|
146
|
+
*
|
|
147
|
+
* Passed fresh on every frame. Tools must not hold on to it: the coordinate
|
|
148
|
+
* mapping it wraps is only valid for the frame it was built in.
|
|
149
|
+
*/
|
|
150
|
+
interface RenderContext {
|
|
151
|
+
ctx: CanvasRenderingContext2D;
|
|
152
|
+
/** Device pixels per CSS pixel. Every coordinate must be scaled by this. */
|
|
153
|
+
ratio: number;
|
|
154
|
+
/** Canvas size in MEDIA (CSS) pixels. */
|
|
155
|
+
width: number;
|
|
156
|
+
height: number;
|
|
157
|
+
theme: Theme;
|
|
158
|
+
/** Resolve a data-space anchor to pixels, or null if it is off the scale. */
|
|
159
|
+
toScreen: (point: Point) => ScreenPoint | null;
|
|
160
|
+
/**
|
|
161
|
+
* Price to a y coordinate, with no time component.
|
|
162
|
+
*
|
|
163
|
+
* Separate from `toScreen` because a horizontal level -- a Fibonacci rung,
|
|
164
|
+
* a ray -- has a price but no meaningful time, and faking one risks
|
|
165
|
+
* resolving to null for an index that happens to be off the scale.
|
|
166
|
+
*/
|
|
167
|
+
toY: (price: number) => number | null;
|
|
168
|
+
}
|
|
169
|
+
/** A drawing tool: how to render it, and how to decide the pointer is on it. */
|
|
170
|
+
interface Tool {
|
|
171
|
+
id: string;
|
|
172
|
+
/** How many anchors before the drawing is complete. */
|
|
173
|
+
pointCount: number;
|
|
174
|
+
/**
|
|
175
|
+
* Discarded on pointer-up instead of being kept.
|
|
176
|
+
*
|
|
177
|
+
* For the measure tool, which answers "how far is it from here to there"
|
|
178
|
+
* and has no reason to persist afterwards -- keeping it would litter the
|
|
179
|
+
* chart with the arithmetic of decisions already made.
|
|
180
|
+
*/
|
|
181
|
+
transient?: boolean;
|
|
182
|
+
draw(drawing: Drawing, selected: boolean, context: RenderContext): void;
|
|
183
|
+
/**
|
|
184
|
+
* Prices this drawing should label on the price scale.
|
|
185
|
+
*
|
|
186
|
+
* A horizontal support line is far more useful when the axis says 1,240.50
|
|
187
|
+
* next to it -- that is the number a trader types into an order ticket.
|
|
188
|
+
*/
|
|
189
|
+
axisPrices?(drawing: Drawing): readonly number[];
|
|
190
|
+
/**
|
|
191
|
+
* Distance in CSS pixels from the pointer to this drawing's body, or null
|
|
192
|
+
* if it is not hittable right now. Handles are hit-tested generically by
|
|
193
|
+
* the primitive, since every tool's handles behave identically.
|
|
194
|
+
*/
|
|
195
|
+
distance(drawing: Drawing, at: ScreenPoint, context: Omit<RenderContext, "ctx">): number | null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A straight line between two points.
|
|
200
|
+
*
|
|
201
|
+
* What it is for: connecting successive higher lows, or successive lower
|
|
202
|
+
* highs, to see the slope of a move and where price would have to break to
|
|
203
|
+
* end it. It is the most-drawn object on any chart, and the one every other
|
|
204
|
+
* two-anchor tool is built from.
|
|
205
|
+
*/
|
|
206
|
+
declare const Trendline: Tool;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* A horizontal line from its anchor to the right edge, forever.
|
|
210
|
+
*
|
|
211
|
+
* The support/resistance line, and probably the most-used object on any
|
|
212
|
+
* chart. It marks a price at which something happened -- a high that stopped
|
|
213
|
+
* an advance, a low that held twice -- and the claim is that the same price
|
|
214
|
+
* matters again when it is next reached. Extending only to the RIGHT is the
|
|
215
|
+
* point: the level was established at the anchor and is being projected
|
|
216
|
+
* forward, so drawing it back over history would assert something the past
|
|
217
|
+
* has already disproved or confirmed.
|
|
218
|
+
*/
|
|
219
|
+
declare const HorizontalRay: Tool;
|
|
220
|
+
|
|
221
|
+
declare const Fibonacci: Tool;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* A rectangle between two corners.
|
|
225
|
+
*
|
|
226
|
+
* Used to mark a supply or demand zone: a price BAND, rather than the single
|
|
227
|
+
* line a horizontal ray gives you, where buying or selling previously
|
|
228
|
+
* overwhelmed the other side. The band matters because the orders that made
|
|
229
|
+
* it were filled across a range, not at one exact price, so a level drawn as
|
|
230
|
+
* a hairline is a false precision the market never had.
|
|
231
|
+
*/
|
|
232
|
+
declare const Rectangle: Tool;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Drag to measure: how far, how long, how much.
|
|
236
|
+
*
|
|
237
|
+
* Reports the move in rupees and percent, and its duration in SESSIONS
|
|
238
|
+
* rather than calendar days -- a trader sizing a position cares how many
|
|
239
|
+
* bars the move took, and the weekend in the middle of it is not one of
|
|
240
|
+
* them. The logical index counts bars, so the difference between two
|
|
241
|
+
* anchors is the session count directly.
|
|
242
|
+
*
|
|
243
|
+
* Transient: it disappears on release. A measurement answers a question in
|
|
244
|
+
* the moment; keeping it would litter the chart with the arithmetic of
|
|
245
|
+
* decisions already made.
|
|
246
|
+
*/
|
|
247
|
+
declare const Measure: Tool;
|
|
248
|
+
|
|
249
|
+
/** The minimum this needs from a bar. Nothing here looks at OHLC. */
|
|
250
|
+
interface Bar {
|
|
251
|
+
time: Time;
|
|
252
|
+
close: number;
|
|
253
|
+
}
|
|
254
|
+
type IndicatorSpec = {
|
|
255
|
+
type: "rsi";
|
|
256
|
+
period?: number;
|
|
257
|
+
colour?: string;
|
|
258
|
+
} | {
|
|
259
|
+
type: "macd";
|
|
260
|
+
fastPeriod?: number;
|
|
261
|
+
slowPeriod?: number;
|
|
262
|
+
signalPeriod?: number;
|
|
263
|
+
} | {
|
|
264
|
+
type: "bollinger";
|
|
265
|
+
period?: number;
|
|
266
|
+
multiplier?: number;
|
|
267
|
+
colour?: string;
|
|
268
|
+
};
|
|
269
|
+
interface AttachedIndicator {
|
|
270
|
+
/** Recompute from a new set of bars. */
|
|
271
|
+
setData(bars: readonly Bar[]): void;
|
|
272
|
+
/** Remove every series it created, and its pane if it made one. */
|
|
273
|
+
remove(): void;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Compute an indicator and render it.
|
|
277
|
+
*
|
|
278
|
+
* RSI and MACD get their OWN pane; Bollinger overlays the price pane. That
|
|
279
|
+
* split is not cosmetic -- RSI is 0-100 and MACD oscillates around zero, and
|
|
280
|
+
* neither shares units with a price in rupees. Putting them on the price
|
|
281
|
+
* scale would flatten the candles into a line at the top of the chart.
|
|
282
|
+
* Bollinger is in price units, so it belongs on the price pane and nowhere
|
|
283
|
+
* else.
|
|
284
|
+
*
|
|
285
|
+
* Every recompute is a full recompute, deliberately. Measured at 2,000 bars:
|
|
286
|
+
* RSI 0.16ms, MACD 0.21ms, Bollinger 0.24ms -- 0.61ms for all three, against
|
|
287
|
+
* a 16.7ms frame. An incremental path would mean a second code path to keep
|
|
288
|
+
* correct, with its own seeding and off-by-one risks around the window
|
|
289
|
+
* boundary, for a saving nobody can perceive. Worth revisiting only if
|
|
290
|
+
* intraday data makes the series an order of magnitude longer.
|
|
291
|
+
*/
|
|
292
|
+
declare function addIndicator(chart: IChartApi, spec: IndicatorSpec, bars: readonly Bar[]): AttachedIndicator;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Relative Strength Index.
|
|
296
|
+
*
|
|
297
|
+
* What it measures: the size of recent gains against the size of recent
|
|
298
|
+
* losses, scaled to 0-100. Above 70 is conventionally "overbought" and below
|
|
299
|
+
* 30 "oversold" -- though in a strong trend RSI can sit above 70 for weeks,
|
|
300
|
+
* which is why treating those lines as sell and buy signals is how people
|
|
301
|
+
* lose money shorting uptrends.
|
|
302
|
+
*
|
|
303
|
+
* More useful in practice: 50 as a trend divider, and DIVERGENCE -- price
|
|
304
|
+
* making a new high while RSI does not, which says the new high came on
|
|
305
|
+
* weaker momentum than the last one.
|
|
306
|
+
*
|
|
307
|
+
* Averages use Wilder's smoothing, not an SMA or a standard EMA. See
|
|
308
|
+
* `wilder()` -- getting this wrong shifts the value by several points, which
|
|
309
|
+
* is the difference between 68 and 71 on a line someone trades.
|
|
310
|
+
*/
|
|
311
|
+
declare function rsi(closes: readonly number[], period?: number): (number | null)[];
|
|
312
|
+
|
|
313
|
+
interface MacdResult {
|
|
314
|
+
/** Fast EMA minus slow EMA. */
|
|
315
|
+
macd: (number | null)[];
|
|
316
|
+
/** EMA of the MACD line. */
|
|
317
|
+
signal: (number | null)[];
|
|
318
|
+
/** MACD minus signal. The bars. */
|
|
319
|
+
histogram: (number | null)[];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Moving Average Convergence Divergence.
|
|
323
|
+
*
|
|
324
|
+
* Three things drawn together: the distance between a fast and a slow moving
|
|
325
|
+
* average (the MACD line), a smoothing of that distance (the signal line),
|
|
326
|
+
* and the gap between those two (the histogram).
|
|
327
|
+
*
|
|
328
|
+
* What it says: the MACD line crossing zero means the two averages crossed,
|
|
329
|
+
* i.e. the trend changed. The histogram crossing zero means the MACD crossed
|
|
330
|
+
* its signal, which happens earlier and is the more-watched event. The
|
|
331
|
+
* histogram shrinking while price still rises is the divergence traders look
|
|
332
|
+
* for -- the move continuing, but with less behind it each bar.
|
|
333
|
+
*
|
|
334
|
+
* Defaults are Appel's original 12/26/9, chosen for a six-day trading week
|
|
335
|
+
* and never updated. They persist because everyone watches them, which is
|
|
336
|
+
* the only reason they matter.
|
|
337
|
+
*/
|
|
338
|
+
declare function macd(closes: readonly number[], fastPeriod?: number, slowPeriod?: number, signalPeriod?: number): MacdResult;
|
|
339
|
+
|
|
340
|
+
interface BollingerResult {
|
|
341
|
+
upper: (number | null)[];
|
|
342
|
+
middle: (number | null)[];
|
|
343
|
+
lower: (number | null)[];
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Bollinger Bands.
|
|
347
|
+
*
|
|
348
|
+
* A moving average with a band either side at a fixed number of standard
|
|
349
|
+
* deviations. Because the width is derived from recent volatility, the bands
|
|
350
|
+
* widen when the market gets noisy and contract when it goes quiet.
|
|
351
|
+
*
|
|
352
|
+
* The contraction is the part worth watching. A "squeeze" -- bands at their
|
|
353
|
+
* narrowest in months -- says volatility has collapsed, and volatility
|
|
354
|
+
* historically reverts, so a large move often follows. It says nothing about
|
|
355
|
+
* DIRECTION, which is why a squeeze is a reason to pay attention rather than
|
|
356
|
+
* a reason to buy.
|
|
357
|
+
*
|
|
358
|
+
* Price touching a band is not a signal either: with 2 standard deviations,
|
|
359
|
+
* roughly 5% of bars close outside by construction. In a strong trend price
|
|
360
|
+
* "walks the band" for weeks.
|
|
361
|
+
*
|
|
362
|
+
* Deviation is POPULATION, not sample -- see `stdev()`.
|
|
363
|
+
*/
|
|
364
|
+
declare function bollinger(closes: readonly number[], period?: number, multiplier?: number): BollingerResult;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The moving averages and smoothing every indicator is built from.
|
|
368
|
+
*
|
|
369
|
+
* All of these emit `null` until their window is full, rather than starting
|
|
370
|
+
* with a partial average. A "20-period average" computed from 3 points is
|
|
371
|
+
* not a 20-period average, and drawing it anyway shows a level that never
|
|
372
|
+
* existed -- which on a chart someone trades from is worse than a gap.
|
|
373
|
+
*/
|
|
374
|
+
/** Simple moving average. `null` for the first `period - 1` positions. */
|
|
375
|
+
declare function sma(values: readonly number[], period: number): (number | null)[];
|
|
376
|
+
/**
|
|
377
|
+
* Exponential moving average, seeded with an SMA.
|
|
378
|
+
*
|
|
379
|
+
* The seed matters and is the usual source of disagreement between two
|
|
380
|
+
* implementations of the same indicator. An EMA is recursive, so it needs a
|
|
381
|
+
* first value from somewhere; seeding with a simple average of the first
|
|
382
|
+
* `period` points is what Appel's original MACD does and what charting
|
|
383
|
+
* platforms follow. Seeding with the first data point instead converges to
|
|
384
|
+
* the same curve eventually but is visibly wrong for the first hundred bars.
|
|
385
|
+
*/
|
|
386
|
+
declare function ema(values: readonly number[], period: number): (number | null)[];
|
|
387
|
+
/**
|
|
388
|
+
* Wilder's smoothing. Used by RSI, ATR and ADX -- and ONLY by them.
|
|
389
|
+
*
|
|
390
|
+
* It is an EMA with k = 1/period rather than 2/(period+1), which makes a
|
|
391
|
+
* 14-period Wilder average behave like a 27-period EMA. Using a plain EMA or
|
|
392
|
+
* an SMA in RSI is the most common way to get an indicator that looks
|
|
393
|
+
* plausible and disagrees with every other platform by several points --
|
|
394
|
+
* enough to move an RSI across the 70 line that someone is trading off.
|
|
395
|
+
*/
|
|
396
|
+
declare function wilder(values: readonly number[], period: number): (number | null)[];
|
|
397
|
+
/**
|
|
398
|
+
* Rolling POPULATION standard deviation.
|
|
399
|
+
*
|
|
400
|
+
* Population (÷n), not sample (÷n−1). Bollinger's bands are defined over the
|
|
401
|
+
* window itself rather than a sample drawn from a larger population, and
|
|
402
|
+
* using the sample formula widens every band slightly -- small, consistent,
|
|
403
|
+
* and wrong.
|
|
404
|
+
*/
|
|
405
|
+
declare function stdev(values: readonly number[], period: number): (number | null)[];
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Pure geometry, in screen pixels.
|
|
409
|
+
*
|
|
410
|
+
* Everything here is deliberately free of chart, canvas and DOM: hit-testing
|
|
411
|
+
* is where a drawing library feels precise or infuriating, and it is far
|
|
412
|
+
* easier to get right when it can be asserted on directly.
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* Shortest distance from a point to a line SEGMENT.
|
|
416
|
+
*
|
|
417
|
+
* Segment, not infinite line -- the difference is the whole reason this is
|
|
418
|
+
* not two lines of code. Distance to an infinite line would report the
|
|
419
|
+
* pointer as "on" a trendline while hovering somewhere far off the end of it,
|
|
420
|
+
* which in practice means clicking empty chart selects a drawing that is not
|
|
421
|
+
* visibly there.
|
|
422
|
+
*
|
|
423
|
+
* Standard projection onto the segment, with the parameter clamped to [0, 1]
|
|
424
|
+
* so anything past either end measures to that endpoint instead.
|
|
425
|
+
*/
|
|
426
|
+
declare function distanceToSegment(p: ScreenPoint, a: ScreenPoint, b: ScreenPoint): number;
|
|
427
|
+
/**
|
|
428
|
+
* Distance to the OUTLINE of a rectangle, zero when inside it.
|
|
429
|
+
*
|
|
430
|
+
* Zero inside rather than distance-to-nearest-edge, so a filled zone can be
|
|
431
|
+
* grabbed anywhere in its body -- which is how every other charting tool
|
|
432
|
+
* behaves and therefore what people expect.
|
|
433
|
+
*/
|
|
434
|
+
declare function distanceToRect(p: ScreenPoint, a: ScreenPoint, b: ScreenPoint): number;
|
|
435
|
+
/**
|
|
436
|
+
* The Fibonacci retracement levels, as fractions of the move.
|
|
437
|
+
*
|
|
438
|
+
* What they mean: after a move from A to B, traders watch for the pullback to
|
|
439
|
+
* pause at one of these fractions of it before the move resumes. 0.618 is the
|
|
440
|
+
* one most watched -- the golden ratio -- with 0.5 close behind despite not
|
|
441
|
+
* being a Fibonacci number at all. The levels have no predictive mechanism;
|
|
442
|
+
* they matter because enough participants place orders at them that they
|
|
443
|
+
* become self-fulfilling often enough to be worth drawing.
|
|
444
|
+
*/
|
|
445
|
+
declare const FIB_LEVELS: readonly [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
|
446
|
+
/**
|
|
447
|
+
* Price at each Fibonacci level between two prices.
|
|
448
|
+
*
|
|
449
|
+
* Level 0 sits at `from` and level 1 at `to`, so dragging bottom-to-top and
|
|
450
|
+
* top-to-bottom produce mirrored ladders -- which is correct. Traders draw
|
|
451
|
+
* low-to-high in an uptrend and high-to-low in a downtrend, and expect 0 to
|
|
452
|
+
* land on the end they started from.
|
|
453
|
+
*/
|
|
454
|
+
declare function fibonacciPrices(from: number, to: number): number[];
|
|
455
|
+
/**
|
|
456
|
+
* Percentage change between two prices.
|
|
457
|
+
*
|
|
458
|
+
* Signed, and relative to where the move started, which is what a trader
|
|
459
|
+
* reading a measure tool means by "how far did it go".
|
|
460
|
+
*/
|
|
461
|
+
declare function percentChange(from: number, to: number): number;
|
|
462
|
+
|
|
463
|
+
interface TrendkitOptions {
|
|
464
|
+
chart: IChartApi;
|
|
465
|
+
/** The series drawings are anchored to. Normally the candlestick series. */
|
|
466
|
+
series: ISeriesApi<SeriesType>;
|
|
467
|
+
/** Tools to enable. Only what you pass is bundled and only what you pass draws. */
|
|
468
|
+
tools: Tool[];
|
|
469
|
+
theme?: Theme;
|
|
470
|
+
/**
|
|
471
|
+
* Snap new anchors to the nearest OHLC within this many pixels. 0 disables.
|
|
472
|
+
*
|
|
473
|
+
* On by default because traders anchor to actual highs and lows, and
|
|
474
|
+
* hitting an exact value by hand is impossible at normal zoom. Hold shift
|
|
475
|
+
* while drawing to place freehand.
|
|
476
|
+
*/
|
|
477
|
+
magnetPx?: number;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
interface Trendkit {
|
|
481
|
+
store: DrawingStore;
|
|
482
|
+
/** Arm a tool; the next drag on the chart draws one, then disarms. */
|
|
483
|
+
setActiveTool(tool: ToolId | null): void;
|
|
484
|
+
activeTool(): ToolId | null;
|
|
485
|
+
setTheme(theme: Theme): void;
|
|
486
|
+
toJSON(): TrendkitSnapshot;
|
|
487
|
+
fromJSON(snapshot: TrendkitSnapshot): void;
|
|
488
|
+
/** Detach from the chart. Call before the chart itself is removed. */
|
|
489
|
+
destroy(): void;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Attach drawing tools to a Lightweight Charts series.
|
|
493
|
+
*
|
|
494
|
+
* Persistence is deliberately left to the caller: `toJSON` and `fromJSON` are
|
|
495
|
+
* the whole story, and where the result goes -- an API, localStorage, a file
|
|
496
|
+
* -- is an application decision. A library that picks one is a library people
|
|
497
|
+
* have to work around.
|
|
498
|
+
*/
|
|
499
|
+
declare function createTrendkit(options: TrendkitOptions): Trendkit;
|
|
500
|
+
|
|
501
|
+
export { type AttachedIndicator, type Bar, type BollingerResult, DARK_THEME, type Drawing, type DrawingStyle, FIB_LEVELS, Fibonacci, HorizontalRay, type IndicatorSpec, LIGHT_THEME, type MacdResult, Measure, type Point, Rectangle, type RenderContext, type ScreenPoint, type Theme, type Tool, type ToolId, type Trendkit, type TrendkitOptions, type TrendkitSnapshot, Trendline, addIndicator, bollinger, createTrendkit, distanceToRect, distanceToSegment, ema, fibonacciPrices, macd, percentChange, rsi, sma, stdev, wilder };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DARK_THEME, FIB_LEVELS, Fibonacci, HorizontalRay, LIGHT_THEME, Measure, Rectangle, Trendline, addIndicator, bollinger, createTrendkit, distanceToRect, distanceToSegment, ema, fibonacciPrices, macd, percentChange, rsi, sma, stdev, wilder } from './chunk-AEC4653J.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { IChartApi, ISeriesApi, SeriesType } from 'lightweight-charts';
|
|
2
|
+
import { Tool, Theme, Trendkit } from '../index.js';
|
|
3
|
+
|
|
4
|
+
interface UseTrendkitOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Null until the chart exists.
|
|
7
|
+
*
|
|
8
|
+
* Charts are almost always created in an effect, so on the first render
|
|
9
|
+
* there is nothing to attach to. Accepting null is what lets a caller
|
|
10
|
+
* write this as one hook rather than a conditional one.
|
|
11
|
+
*/
|
|
12
|
+
chart: IChartApi | null | undefined;
|
|
13
|
+
series: ISeriesApi<SeriesType> | null | undefined;
|
|
14
|
+
tools: Tool[];
|
|
15
|
+
theme?: Theme;
|
|
16
|
+
magnetPx?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Attach trendkit to a chart for the lifetime of a component.
|
|
20
|
+
*
|
|
21
|
+
* Returns null until both the chart and series are available, then the kit.
|
|
22
|
+
* Re-attaches when the chart or series identity changes -- which is what
|
|
23
|
+
* happens when a host recreates its chart to switch theme, and is exactly
|
|
24
|
+
* when a primitive left attached to a destroyed chart would throw.
|
|
25
|
+
*
|
|
26
|
+
* `tools` is intentionally NOT in the dependency list. Callers write it
|
|
27
|
+
* inline as an array literal, which is a new reference on every render, and
|
|
28
|
+
* depending on it would tear the kit down and rebuild it sixty times a
|
|
29
|
+
* second -- losing the selection and every in-progress drag. The tool set is
|
|
30
|
+
* read once at attach; changing which tools are available means remounting,
|
|
31
|
+
* which is a thing nobody does mid-session.
|
|
32
|
+
*/
|
|
33
|
+
declare function useTrendkit(options: UseTrendkitOptions): Trendkit | null;
|
|
34
|
+
|
|
35
|
+
export { Trendkit, type UseTrendkitOptions, useTrendkit };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createTrendkit } from '../chunk-AEC4653J.js';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
function useTrendkit(options) {
|
|
5
|
+
const { chart, series, theme, magnetPx } = options;
|
|
6
|
+
const [kit, setKit] = useState(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (chart === null || chart === void 0) return;
|
|
9
|
+
if (series === null || series === void 0) return;
|
|
10
|
+
const instance = createTrendkit({
|
|
11
|
+
chart,
|
|
12
|
+
series,
|
|
13
|
+
tools: options.tools,
|
|
14
|
+
...theme === void 0 ? {} : { theme },
|
|
15
|
+
...magnetPx === void 0 ? {} : { magnetPx }
|
|
16
|
+
});
|
|
17
|
+
setKit(instance);
|
|
18
|
+
return () => {
|
|
19
|
+
instance.destroy();
|
|
20
|
+
setKit(null);
|
|
21
|
+
};
|
|
22
|
+
}, [chart, series, magnetPx]);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (kit !== null && theme !== void 0) kit.setTheme(theme);
|
|
25
|
+
}, [kit, theme]);
|
|
26
|
+
return kit;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { useTrendkit };
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trendkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drawing tools and indicators for TradingView's Lightweight Charts \u2014 trendlines, rays, Fibonacci, zones and a measure tool, built on the v5 plugin API.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"lightweight-charts",
|
|
7
|
+
"trading",
|
|
8
|
+
"charts",
|
|
9
|
+
"drawing-tools",
|
|
10
|
+
"trendline",
|
|
11
|
+
"fibonacci",
|
|
12
|
+
"technical-analysis",
|
|
13
|
+
"candlestick",
|
|
14
|
+
"finance"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"sideEffects": false,
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"module": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./dist/react/index.d.ts",
|
|
31
|
+
"import": "./dist/react/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "vite examples/vanilla",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"test:watch": "vitest",
|
|
40
|
+
"lint": "eslint .",
|
|
41
|
+
"format": "prettier --write .",
|
|
42
|
+
"verify": "npm run typecheck && npm run lint && npm run test && npm run build",
|
|
43
|
+
"prepublishOnly": "npm run verify && node scripts/check-package.mjs"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"lightweight-charts": ">=5.0.0",
|
|
47
|
+
"react": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"react": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^20",
|
|
56
|
+
"@types/react": "^19.3.0",
|
|
57
|
+
"@typescript-eslint/eslint-plugin": "^8",
|
|
58
|
+
"@typescript-eslint/parser": "^8",
|
|
59
|
+
"eslint": "^9",
|
|
60
|
+
"lightweight-charts": "^5.2.1",
|
|
61
|
+
"prettier": "^3",
|
|
62
|
+
"tsup": "^8",
|
|
63
|
+
"typescript": "^5",
|
|
64
|
+
"vite": "^7",
|
|
65
|
+
"vitest": "^3.2.7"
|
|
66
|
+
},
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/darshansachaniya/trendkit.git"
|
|
70
|
+
},
|
|
71
|
+
"homepage": "https://github.com/darshansachaniya/trendkit#readme",
|
|
72
|
+
"bugs": {
|
|
73
|
+
"url": "https://github.com/darshansachaniya/trendkit/issues"
|
|
74
|
+
},
|
|
75
|
+
"author": "Darshan Sachaniya",
|
|
76
|
+
"engines": {
|
|
77
|
+
"node": ">=18"
|
|
78
|
+
}
|
|
79
|
+
}
|