@tony01/astroneum 0.4.1-beta.2
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/CHANGELOG.md +237 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/Indicator-BBI3L_lx.d.ts +1181 -0
- package/dist/ar-SA-AEPUSRMF.js +2 -0
- package/dist/astroneum.css +1 -0
- package/dist/chunk-2SN3RH42.js +2 -0
- package/dist/chunk-2YYGUSVX.js +3 -0
- package/dist/chunk-3GJSZFLM.js +2 -0
- package/dist/chunk-4YOLQ5DC.js +2 -0
- package/dist/chunk-I4YQBP2M.js +3 -0
- package/dist/chunk-OOMZRONH.js +35 -0
- package/dist/chunk-PV2GAINM.js +2 -0
- package/dist/chunk-SDXH2WAI.js +3 -0
- package/dist/chunk-YFPXTHZX.js +2 -0
- package/dist/chunk-YW3STIPY.js +653 -0
- package/dist/de-DE-4PR3NRQ7.js +2 -0
- package/dist/entries/alerts.d.ts +93 -0
- package/dist/entries/alerts.js +2 -0
- package/dist/entries/datafeeds/crypto.d.ts +65 -0
- package/dist/entries/datafeeds/crypto.js +2 -0
- package/dist/entries/datafeeds/polygon.d.ts +113 -0
- package/dist/entries/datafeeds/polygon.js +2 -0
- package/dist/entries/multichart.d.ts +96 -0
- package/dist/entries/multichart.js +2 -0
- package/dist/entries/portfolio.d.ts +55 -0
- package/dist/entries/portfolio.js +2 -0
- package/dist/entries/replay.d.ts +141 -0
- package/dist/entries/replay.js +2 -0
- package/dist/entries/script.d.ts +68 -0
- package/dist/entries/script.js +2 -0
- package/dist/entries/watchlist.d.ts +43 -0
- package/dist/entries/watchlist.js +2 -0
- package/dist/es-ES-HNDVPSLW.js +2 -0
- package/dist/fr-FR-Y7XWRGF2.js +2 -0
- package/dist/hi-IN-KEGIKIMV.js +2 -0
- package/dist/id-ID-QXDIUQUZ.js +2 -0
- package/dist/index.d.ts +1434 -0
- package/dist/index.js +2 -0
- package/dist/it-IT-UDTDFSRB.js +2 -0
- package/dist/ja-JP-NIODTBW3.js +2 -0
- package/dist/ko-KR-DQHPXKBA.js +2 -0
- package/dist/nl-NL-BB3ZHCXS.js +2 -0
- package/dist/pl-PL-I5MZDLFD.js +2 -0
- package/dist/pt-BR-MAMRTOYR.js +2 -0
- package/dist/ru-RU-F43D6B7G.js +2 -0
- package/dist/th-TH-KTFIUAD6.js +2 -0
- package/dist/tr-TR-WFTKJ22Y.js +2 -0
- package/dist/vi-VN-CD3WI2FQ.js +2 -0
- package/dist/zh-CN-Y2C6RSOI.js +3 -0
- package/docs/STRUCTURE.md +231 -0
- package/docs/TODO-DESIGN.md +261 -0
- package/docs/TODO.md +222 -0
- package/docs/api.md +528 -0
- package/docs/datafeed-guide.md +311 -0
- package/docs/design-astroneum.md +479 -0
- package/docs/plugin-development.md +565 -0
- package/docs/tv-functions-skill.md +706 -0
- package/package.json +147 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Building a Datafeed
|
|
2
|
+
|
|
3
|
+
A **Datafeed** is a simple interface that lets Astroneum fetch historical data and subscribe to real-time ticks from any source. This guide shows 3 patterns: minimal mock, REST API, and real-time stream.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Datafeed Interface
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
interface Datafeed {
|
|
11
|
+
searchSymbols(search?: string): Promise<SymbolInfo[]>
|
|
12
|
+
getHistoryData(
|
|
13
|
+
symbol: SymbolInfo,
|
|
14
|
+
period: Period,
|
|
15
|
+
from: number,
|
|
16
|
+
to: number
|
|
17
|
+
): Promise<CandleData[]>
|
|
18
|
+
subscribe(
|
|
19
|
+
symbol: SymbolInfo,
|
|
20
|
+
period: Period,
|
|
21
|
+
callback: DatafeedSubscribeCallback
|
|
22
|
+
): void
|
|
23
|
+
unsubscribe(symbol: SymbolInfo, period: Period): void
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Key methods:**
|
|
28
|
+
- `searchSymbols(search)` — Return matching symbols (used in symbol search bar).
|
|
29
|
+
- `getHistoryData(symbol, period, from, to)` — Fetch bars between timestamps (Unix ms).
|
|
30
|
+
- `subscribe(symbol, period, callback)` — Stream real-time ticks; call `callback(tick)` on each update.
|
|
31
|
+
- `unsubscribe(symbol, period)` — Stop streaming for this symbol/period pair.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Pattern 1: Minimal Mock (for demos)
|
|
36
|
+
|
|
37
|
+
The simplest datafeed just generates fake data locally:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import type {
|
|
41
|
+
Datafeed,
|
|
42
|
+
SymbolInfo,
|
|
43
|
+
Period,
|
|
44
|
+
CandleData,
|
|
45
|
+
DatafeedSubscribeCallback
|
|
46
|
+
} from '@tony01/astroneum'
|
|
47
|
+
|
|
48
|
+
const SYMBOLS: SymbolInfo[] = [
|
|
49
|
+
{
|
|
50
|
+
ticker: 'BTC',
|
|
51
|
+
shortName: 'BTC',
|
|
52
|
+
name: 'Bitcoin',
|
|
53
|
+
exchange: 'DEMO',
|
|
54
|
+
market: 'crypto'
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
const myDatafeed: Datafeed = {
|
|
59
|
+
searchSymbols(search) {
|
|
60
|
+
const q = search?.toLowerCase() ?? ''
|
|
61
|
+
return Promise.resolve(
|
|
62
|
+
SYMBOLS.filter(s => s.ticker.toLowerCase().includes(q))
|
|
63
|
+
)
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
getHistoryData(symbol, period, from, to) {
|
|
67
|
+
// Generate synthetic bars between `from` and `to` timestamps
|
|
68
|
+
const step = period.multiplier * 60_000 // assume period is in minutes for demo
|
|
69
|
+
const bars: CandleData[] = []
|
|
70
|
+
let price = 50000
|
|
71
|
+
|
|
72
|
+
for (let ts = from; ts <= to; ts += step) {
|
|
73
|
+
const change = (Math.random() - 0.5) * 1000
|
|
74
|
+
bars.push({
|
|
75
|
+
timestamp: ts,
|
|
76
|
+
open: price,
|
|
77
|
+
high: price + Math.abs(change),
|
|
78
|
+
low: price - Math.abs(change),
|
|
79
|
+
close: price + change,
|
|
80
|
+
volume: Math.random() * 10000,
|
|
81
|
+
turnover: Math.random() * 100000,
|
|
82
|
+
})
|
|
83
|
+
price += change
|
|
84
|
+
}
|
|
85
|
+
return Promise.resolve(bars)
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
subscribe(symbol, period, callback) {
|
|
89
|
+
// Emit a tick every 100ms
|
|
90
|
+
let price = 50000
|
|
91
|
+
const interval = setInterval(() => {
|
|
92
|
+
price += (Math.random() - 0.5) * 100
|
|
93
|
+
callback({
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
open: price,
|
|
96
|
+
high: price + 50,
|
|
97
|
+
low: price - 50,
|
|
98
|
+
close: price,
|
|
99
|
+
volume: Math.random() * 100,
|
|
100
|
+
turnover: Math.random() * 1000,
|
|
101
|
+
})
|
|
102
|
+
}, 100)
|
|
103
|
+
|
|
104
|
+
// Cleanup when unsubscribed
|
|
105
|
+
const originalUnsubscribe = myDatafeed.unsubscribe
|
|
106
|
+
myDatafeed.unsubscribe = (s, p) => {
|
|
107
|
+
if (s.ticker === symbol.ticker && p.text === period.text) {
|
|
108
|
+
clearInterval(interval)
|
|
109
|
+
}
|
|
110
|
+
originalUnsubscribe(s, p)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
unsubscribe() {
|
|
115
|
+
// Cleanup happens in subscribe() above
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Pattern 2: REST API (static bars only)
|
|
123
|
+
|
|
124
|
+
Fetch bars from an HTTP API (e.g., `GET /api/bars?symbol=BTC&from=123&to=456`):
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const myDatafeed: Datafeed = {
|
|
128
|
+
async searchSymbols(search) {
|
|
129
|
+
const res = await fetch(`/api/symbols?q=${search}`)
|
|
130
|
+
return res.json()
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async getHistoryData(symbol, period, from, to) {
|
|
134
|
+
const res = await fetch('/api/bars', {
|
|
135
|
+
method: 'POST',
|
|
136
|
+
headers: { 'Content-Type': 'application/json' },
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
ticker: symbol.ticker,
|
|
139
|
+
timeframe: period.text,
|
|
140
|
+
from,
|
|
141
|
+
to
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
const bars = await res.json() as CandleData[]
|
|
145
|
+
return bars
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
subscribe(symbol, period, callback) {
|
|
149
|
+
// Poll for new bars every 5 seconds
|
|
150
|
+
const interval = setInterval(async () => {
|
|
151
|
+
const now = Date.now()
|
|
152
|
+
const res = await fetch('/api/bars', {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
ticker: symbol.ticker,
|
|
157
|
+
timeframe: period.text,
|
|
158
|
+
from: now - 60_000,
|
|
159
|
+
to: now
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
const bars = await res.json() as CandleData[]
|
|
163
|
+
if (bars.length > 0) {
|
|
164
|
+
callback(bars[bars.length - 1])
|
|
165
|
+
}
|
|
166
|
+
}, 5000)
|
|
167
|
+
|
|
168
|
+
// Store interval ID so unsubscribe can clear it
|
|
169
|
+
const unsubKey = `${symbol.ticker}::${period.text}`
|
|
170
|
+
const intervals = (myDatafeed as any)._intervals ??= new Map()
|
|
171
|
+
intervals.set(unsubKey, interval)
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
unsubscribe(symbol, period) {
|
|
175
|
+
const unsubKey = `${symbol.ticker}::${period.text}`
|
|
176
|
+
const intervals = (myDatafeed as any)._intervals
|
|
177
|
+
if (intervals?.has(unsubKey)) {
|
|
178
|
+
clearInterval(intervals.get(unsubKey))
|
|
179
|
+
intervals.delete(unsubKey)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Pattern 3: WebSocket Streaming (real-time)
|
|
188
|
+
|
|
189
|
+
Connect to a live market data stream (e.g., Binance WebSocket):
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const myDatafeed: Datafeed = {
|
|
193
|
+
searchSymbols(search) {
|
|
194
|
+
return Promise.resolve([
|
|
195
|
+
{
|
|
196
|
+
ticker: 'BTCUSDT',
|
|
197
|
+
shortName: 'BTC',
|
|
198
|
+
name: 'Bitcoin USDT',
|
|
199
|
+
exchange: 'BINANCE',
|
|
200
|
+
market: 'crypto'
|
|
201
|
+
}
|
|
202
|
+
])
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async getHistoryData(symbol, period, from, to) {
|
|
206
|
+
// Fetch from REST: GET https://api.binance.com/api/v3/klines
|
|
207
|
+
const interval = '1m' // or '5m', '1h', etc.
|
|
208
|
+
const res = await fetch(
|
|
209
|
+
`https://api.binance.com/api/v3/klines?` +
|
|
210
|
+
`symbol=${symbol.ticker}&interval=${interval}&startTime=${from}&endTime=${to}`
|
|
211
|
+
)
|
|
212
|
+
const rows = await res.json() as any[][]
|
|
213
|
+
return rows.map(row => ({
|
|
214
|
+
timestamp: row[0] as number,
|
|
215
|
+
open: parseFloat(row[1]),
|
|
216
|
+
high: parseFloat(row[2]),
|
|
217
|
+
low: parseFloat(row[3]),
|
|
218
|
+
close: parseFloat(row[4]),
|
|
219
|
+
volume: parseFloat(row[7]),
|
|
220
|
+
turnover: parseFloat(row[7]) // or custom calculation
|
|
221
|
+
}))
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
subscribe(symbol, period, callback) {
|
|
225
|
+
const interval = '1m' // map period to interval
|
|
226
|
+
const stream = `${symbol.ticker.toLowerCase()}@kline_${interval}`
|
|
227
|
+
const ws = new WebSocket(`wss://stream.binance.com/ws/${stream}`)
|
|
228
|
+
|
|
229
|
+
ws.onmessage = (event) => {
|
|
230
|
+
try {
|
|
231
|
+
const data = JSON.parse(event.data)
|
|
232
|
+
if (data.e === 'kline') {
|
|
233
|
+
callback({
|
|
234
|
+
timestamp: data.k.t,
|
|
235
|
+
open: parseFloat(data.k.o),
|
|
236
|
+
high: parseFloat(data.k.h),
|
|
237
|
+
low: parseFloat(data.k.l),
|
|
238
|
+
close: parseFloat(data.k.c),
|
|
239
|
+
volume: parseFloat(data.k.v),
|
|
240
|
+
turnover: parseFloat(data.k.q)
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error('Parse error:', e)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
ws.onerror = () => ws.close()
|
|
249
|
+
ws.onclose = () => {
|
|
250
|
+
// Implement reconnect logic if needed
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Store socket so unsubscribe can close it
|
|
254
|
+
const key = `${symbol.ticker}::${period.text}`
|
|
255
|
+
const sockets = (myDatafeed as any)._sockets ??= new Map()
|
|
256
|
+
sockets.set(key, ws)
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
unsubscribe(symbol, period) {
|
|
260
|
+
const key = `${symbol.ticker}::${period.text}`
|
|
261
|
+
const sockets = (myDatafeed as any)._sockets
|
|
262
|
+
if (sockets?.has(key)) {
|
|
263
|
+
sockets.get(key).close()
|
|
264
|
+
sockets.delete(key)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Using a Datafeed in AstroneumChart
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
import { AstroneumChart } from '@tony01/astroneum'
|
|
276
|
+
import { myDatafeed } from './datafeed'
|
|
277
|
+
|
|
278
|
+
export function Chart() {
|
|
279
|
+
return (
|
|
280
|
+
<AstroneumChart
|
|
281
|
+
symbol={{ ticker: 'BTC', shortName: 'BTC', name: 'Bitcoin', market: 'crypto' }}
|
|
282
|
+
period={{ multiplier: 1, timespan: 'minute', text: '1m' }}
|
|
283
|
+
datafeed={myDatafeed}
|
|
284
|
+
locale="en-US"
|
|
285
|
+
style={{ width: '100%', height: 600 }}
|
|
286
|
+
/>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Tips
|
|
294
|
+
|
|
295
|
+
1. **Start simple** — Begin with a REST API; add WebSocket if you need real-time.
|
|
296
|
+
2. **Handle errors gracefully** — Return empty arrays if data fetches fail; reconnect on socket close.
|
|
297
|
+
3. **Cache symbols** — Don't re-fetch the symbol list on every search.
|
|
298
|
+
4. **Timestamp precision** — Use milliseconds (Unix ms) throughout; ensure `from` and `to` are in ms.
|
|
299
|
+
5. **Smooth real-time** — Emit ticks smoothly at ~100–200 ms intervals instead of burst updates.
|
|
300
|
+
6. **Use TickAnimator** — The library exports `TickAnimator` to interpolate ticks smoothly across frames (see `../src/engine/common/TickAnimator.ts`).
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Advanced: Mock with Smooth Animation
|
|
305
|
+
|
|
306
|
+
For testing, the demo `mockDatafeed.ts` uses:
|
|
307
|
+
- **Seeded random generation** — Reproducible fake data across reloads
|
|
308
|
+
- **TickAnimator** — Smooth frame-based interpolation of real-time ticks
|
|
309
|
+
- **Fallback simulation** — Local bar generation if Binance fails
|
|
310
|
+
|
|
311
|
+
See `demo/src/mockDatafeed.ts` for the full implementation.
|