1
1
import React , { useEffect , useRef , useState } from 'react' ;
2
2
import { tsvParse } from "d3-dsv" ;
3
- import { SegmentedControl } from '@mantine/core' ;
3
+ import { Checkbox , Group , SegmentedControl , Table } from '@mantine/core' ;
4
4
5
5
6
6
// https://github.com/tradingview/lightweight-charts/issues/543
7
7
// const createChart = dynamic(() => import('lightweight-charts'));
8
- import { createChart , CrosshairMode } from 'lightweight-charts' ;
8
+ import { createChart , CrosshairMode , MouseEventParams , TimeRange } from 'lightweight-charts' ;
9
9
import { ReportSummary } from "../types" ;
10
10
import moment from "moment" ;
11
11
@@ -128,8 +128,10 @@ const parseInterval = (s: string) => {
128
128
} ;
129
129
130
130
interface Order {
131
+ order_id : number ;
131
132
order_type : string ;
132
133
side : string ;
134
+ symbol : string ;
133
135
price : number ;
134
136
quantity : number ;
135
137
executed_quantity : number ;
@@ -147,7 +149,7 @@ interface Marker {
147
149
text : string ;
148
150
}
149
151
150
- const ordersToMarkets = ( interval : string , orders : Array < Order > | void ) : Array < Marker > => {
152
+ const ordersToMarkers = ( interval : string , orders : Array < Order > | void ) : Array < Marker > => {
151
153
const markers : Array < Marker > = [ ] ;
152
154
const intervalSecs = parseInterval ( interval ) ;
153
155
@@ -376,6 +378,10 @@ const TradingViewChart = (props: TradingViewChartProps) => {
376
378
const resizeObserver = useRef < any > ( ) ;
377
379
const intervals = props . reportSummary . intervals || [ ] ;
378
380
const [ currentInterval , setCurrentInterval ] = useState ( intervals . length > 0 ? intervals [ 0 ] : '1m' ) ;
381
+ const [ showPositionBase , setShowPositionBase ] = useState ( false ) ;
382
+ const [ showCanceledOrders , setShowCanceledOrders ] = useState ( false ) ;
383
+ const [ showPositionAverageCost , setShowPositionAverageCost ] = useState ( false ) ;
384
+ const [ orders , setOrders ] = useState < Order [ ] > ( [ ] ) ;
379
385
380
386
useEffect ( ( ) => {
381
387
if ( ! chartContainerRef . current || chartContainerRef . current . children . length > 0 ) {
@@ -384,10 +390,13 @@ const TradingViewChart = (props: TradingViewChartProps) => {
384
390
385
391
const chartData : any = { } ;
386
392
const fetchers = [ ] ;
387
- const ordersFetcher = fetchOrders ( props . basePath , props . runID ) . then ( ( orders ) => {
388
- const markers = ordersToMarkets ( currentInterval , orders ) ;
389
- chartData . orders = orders ;
390
- chartData . markers = markers ;
393
+ const ordersFetcher = fetchOrders ( props . basePath , props . runID ) . then ( ( orders : Order [ ] | void ) => {
394
+ if ( orders ) {
395
+ const markers = ordersToMarkers ( currentInterval , orders ) ;
396
+ chartData . orders = orders ;
397
+ chartData . markers = markers ;
398
+ setOrders ( orders ) ;
399
+ }
391
400
return orders ;
392
401
} ) ;
393
402
fetchers . push ( ordersFetcher ) ;
@@ -409,11 +418,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
409
418
410
419
Promise . all ( fetchers ) . then ( ( ) => {
411
420
console . log ( "createChart" )
412
-
413
- if ( chart . current ) {
414
- chart . current . remove ( ) ;
415
- }
416
-
417
421
chart . current = createBaseChart ( chartContainerRef ) ;
418
422
419
423
const series = chart . current . addCandlestickSeries ( {
@@ -426,6 +430,38 @@ const TradingViewChart = (props: TradingViewChartProps) => {
426
430
series . setData ( chartData . klines ) ;
427
431
series . setMarkers ( chartData . markers ) ;
428
432
433
+ [ 9 , 27 , 99 ] . forEach ( ( w , i ) => {
434
+ const emaValues = calculateEMA ( chartData . klines , w )
435
+ const emaColor = 'rgba(' + w + ', ' + ( 111 - w ) + ', 232, 0.9)'
436
+ const emaLine = chart . current . addLineSeries ( {
437
+ color : emaColor ,
438
+ lineWidth : 1 ,
439
+ } ) ;
440
+ emaLine . setData ( emaValues ) ;
441
+
442
+ const legend = document . createElement ( 'div' ) ;
443
+ legend . className = 'ema-legend' ;
444
+ legend . style . display = 'block' ;
445
+ legend . style . position = 'absolute' ;
446
+ legend . style . left = 3 + 'px' ;
447
+ legend . style . zIndex = '99' ;
448
+ legend . style . top = 3 + ( i * 22 ) + 'px' ;
449
+ chartContainerRef . current . appendChild ( legend ) ;
450
+
451
+ const setLegendText = ( priceValue : any ) => {
452
+ let val = '∅' ;
453
+ if ( priceValue !== undefined ) {
454
+ val = ( Math . round ( priceValue * 100 ) / 100 ) . toFixed ( 2 ) ;
455
+ }
456
+ legend . innerHTML = 'EMA' + w + ' <span style="color:' + emaColor + '">' + val + '</span>' ;
457
+ }
458
+
459
+ setLegendText ( emaValues [ emaValues . length - 1 ] . value ) ;
460
+ chart . current . subscribeCrosshairMove ( ( param : MouseEventParams ) => {
461
+ setLegendText ( param . seriesPrices . get ( emaLine ) ) ;
462
+ } ) ;
463
+ } )
464
+
429
465
const volumeData = klinesToVolumeData ( chartData . klines ) ;
430
466
const volumeSeries = chart . current . addHistogramSeries ( {
431
467
color : '#182233' ,
@@ -442,64 +478,178 @@ const TradingViewChart = (props: TradingViewChartProps) => {
442
478
volumeSeries . setData ( volumeData ) ;
443
479
444
480
if ( chartData . positionHistory ) {
445
- const lineSeries = chart . current . addLineSeries ( ) ;
446
- const costLine = positionAverageCostHistoryToLineData ( currentInterval , chartData . positionHistory ) ;
447
- lineSeries . setData ( costLine ) ;
481
+ if ( showPositionAverageCost ) {
482
+ const costLineSeries = chart . current . addLineSeries ( ) ;
483
+ const costLine = positionAverageCostHistoryToLineData ( currentInterval , chartData . positionHistory ) ;
484
+ costLineSeries . setData ( costLine ) ;
485
+ }
448
486
449
- const baseLineSeries = chart . current . addLineSeries ( {
450
- priceScaleId : 'left' ,
451
- color : '#98338C' ,
452
- } ) ;
453
- const baseLine = positionBaseHistoryToLineData ( currentInterval , chartData . positionHistory )
454
- baseLineSeries . setData ( baseLine ) ;
487
+ if ( showPositionBase ) {
488
+ const baseLineSeries = chart . current . addLineSeries ( {
489
+ priceScaleId : 'left' ,
490
+ color : '#98338C' ,
491
+ } ) ;
492
+ const baseLine = positionBaseHistoryToLineData ( currentInterval , chartData . positionHistory )
493
+ baseLineSeries . setData ( baseLine ) ;
494
+ }
455
495
}
456
496
457
497
chart . current . timeScale ( ) . fitContent ( ) ;
498
+
499
+ /*
500
+ chart.current.timeScale().setVisibleRange({
501
+ from: (new Date(Date.UTC(2018, 0, 1, 0, 0, 0, 0))).getTime() / 1000,
502
+ to: (new Date(Date.UTC(2018, 1, 1, 0, 0, 0, 0))).getTime() / 1000,
503
+ });
504
+ */
505
+
506
+ // see:
507
+ // https://codesandbox.io/s/9inkb?file=/src/styles.css
508
+ resizeObserver . current = new ResizeObserver ( entries => {
509
+ if ( ! chart . current ) {
510
+ return ;
511
+ }
512
+
513
+ const { width, height} = entries [ 0 ] . contentRect ;
514
+ chart . current . applyOptions ( { width, height} ) ;
515
+
516
+ setTimeout ( ( ) => {
517
+ if ( chart . current ) {
518
+ chart . current . timeScale ( ) . fitContent ( ) ;
519
+ }
520
+ } , 0 ) ;
521
+ } ) ;
522
+
523
+ resizeObserver . current . observe ( chartContainerRef . current ) ;
458
524
} ) ;
459
525
460
526
return ( ) => {
527
+ console . log ( "removeChart" )
528
+
529
+ resizeObserver . current . disconnect ( ) ;
530
+
461
531
if ( chart . current ) {
462
532
chart . current . remove ( ) ;
463
533
}
464
- } ;
465
- } , [ props . runID , props . reportSummary , currentInterval ] )
466
-
467
- // see:
468
- // https://codesandbox.io/s/9inkb?file=/src/styles.css
469
- useEffect ( ( ) => {
470
- resizeObserver . current = new ResizeObserver ( entries => {
471
- if ( ! chart . current ) {
472
- return ;
534
+ if ( chartContainerRef . current ) {
535
+ // remove all the children because we created the legend elements
536
+ chartContainerRef . current . replaceChildren ( ) ;
473
537
}
474
538
475
- const { width, height} = entries [ 0 ] . contentRect ;
476
- chart . current . applyOptions ( { width, height} ) ;
477
-
478
- setTimeout ( ( ) => {
479
- chart . current . timeScale ( ) . fitContent ( ) ;
480
- } , 0 ) ;
481
- } ) ;
482
-
483
- resizeObserver . current . observe ( chartContainerRef . current ) ;
484
- return ( ) => resizeObserver . current . disconnect ( ) ;
485
- } , [ ] ) ;
486
-
539
+ } ;
540
+ } , [ props . runID , props . reportSummary , currentInterval , showPositionBase , showPositionAverageCost ] )
487
541
488
542
return (
489
543
< div >
490
- < div >
544
+ < Group >
491
545
< SegmentedControl
492
546
data = { intervals . map ( ( interval ) => {
493
547
return { label : interval , value : interval }
494
548
} ) }
495
549
onChange = { setCurrentInterval }
496
550
/>
497
- </ div >
551
+ < Checkbox label = "Position Base" checked = { showPositionBase }
552
+ onChange = { ( event ) => setShowPositionBase ( event . currentTarget . checked ) } />
553
+ < Checkbox label = "Position Average Cost" checked = { showPositionAverageCost }
554
+ onChange = { ( event ) => setShowPositionAverageCost ( event . currentTarget . checked ) } />
555
+ </ Group >
556
+
557
+ < div ref = { chartContainerRef } style = { { 'flex' : 1 , 'minHeight' : 500 , position : 'relative' } } >
498
558
499
- < div ref = { chartContainerRef } style = { { 'flex' : 1 , 'minHeight' : 300 } } >
500
559
</ div >
560
+
561
+ < Group >
562
+ < Checkbox label = "Show Canceled" checked = { showCanceledOrders }
563
+ onChange = { ( event ) => setShowCanceledOrders ( event . currentTarget . checked ) } />
564
+
565
+ </ Group >
566
+ < OrderListTable orders = { orders } showCanceled = { showCanceledOrders } onClick = { ( order ) => {
567
+ console . log ( "selected order" , order ) ;
568
+ const visibleRange = chart . current . timeScale ( ) . getVisibleRange ( )
569
+ const seconds = parseInterval ( currentInterval )
570
+ const bars = 12
571
+ const orderTime = order . creation_time . getTime ( ) / 1000
572
+ const from = orderTime - bars * seconds
573
+ const to = orderTime + bars * seconds
574
+
575
+ console . log ( "orderTime" , orderTime )
576
+ console . log ( "visibleRange" , visibleRange )
577
+ console . log ( "setVisibleRange" , from , to , to - from )
578
+ chart . current . timeScale ( ) . setVisibleRange ( { from, to } as TimeRange ) ;
579
+ // chart.current.timeScale().scrollToPosition(20, true);
580
+ } } />
501
581
</ div >
502
582
) ;
503
583
} ;
504
584
585
+ interface OrderListTableProps {
586
+ orders : Order [ ] ;
587
+ showCanceled : boolean ;
588
+ onClick ?: ( order : Order ) => void ;
589
+ }
590
+
591
+ const OrderListTable = ( props : OrderListTableProps ) => {
592
+ let orders = props . orders ;
593
+
594
+ if ( ! props . showCanceled ) {
595
+ orders = orders . filter ( ( order : Order ) => {
596
+ return order . status != "CANCELED"
597
+ } )
598
+ }
599
+
600
+ const rows = orders . map ( ( order : Order ) => (
601
+ < tr key = { order . order_id } onClick = { ( e ) => {
602
+ props . onClick ? props . onClick ( order ) : null ;
603
+ const nodes = e . currentTarget ?. parentNode ?. querySelectorAll ( ".selected" )
604
+ nodes ?. forEach ( ( node , i ) => {
605
+ node . classList . remove ( "selected" )
606
+ } )
607
+ e . currentTarget . classList . add ( "selected" )
608
+ } } >
609
+ < td > { order . order_id } </ td >
610
+ < td > { order . symbol } </ td >
611
+ < td > { order . side } </ td >
612
+ < td > { order . order_type } </ td >
613
+ < td > { order . price } </ td >
614
+ < td > { order . quantity } </ td >
615
+ < td > { order . status } </ td >
616
+ < td > { order . creation_time . toString ( ) } </ td >
617
+ </ tr >
618
+ ) ) ;
619
+ return < Table highlightOnHover striped >
620
+ < thead >
621
+ < tr >
622
+ < th > Order ID</ th >
623
+ < th > Symbol</ th >
624
+ < th > Side</ th >
625
+ < th > Order Type</ th >
626
+ < th > Price</ th >
627
+ < th > Quantity</ th >
628
+ < th > Status</ th >
629
+ < th > Creation Time</ th >
630
+ </ tr >
631
+ </ thead >
632
+ < tbody > { rows } </ tbody >
633
+ </ Table >
634
+ }
635
+
636
+ const calculateEMA = ( a : KLine [ ] , r : number ) => {
637
+ return a . map ( ( k ) => {
638
+ return { time : k . time , value : k . close }
639
+ } ) . reduce ( ( p : any [ ] , n : any , i : number ) => {
640
+ if ( i ) {
641
+ const last = p [ p . length - 1 ]
642
+ const v = 2 * n . value / ( r + 1 ) + last . value * ( r - 1 ) / ( r + 1 )
643
+ return p . concat ( { value : v , time : n . time } )
644
+ }
645
+
646
+ return p
647
+ } , [ {
648
+ value : a [ 0 ] . close ,
649
+ time : a [ 0 ] . time
650
+ } ] )
651
+ }
652
+
653
+
505
654
export default TradingViewChart ;
655
+
0 commit comments