Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save matthewdowney/f75d67217ece4134c91992fb188f4a49 to your computer and use it in GitHub Desktop.
Save matthewdowney/f75d67217ece4134c91992fb188f4a49 to your computer and use it in GitHub Desktop.
Clojurescript + Reagent port of react-financial-charts sample candles code (StockChart.tsx). See https://react-financial.github.io/react-financial-charts/?path=/story/features-full-screen--daily.
(ns stock-chart-example
"Clojurescript + Reagent port of react-financial-charts StockChart.tsx
candles example[1][2].
Assumes a project generated via https://github.com/bhauman/figwheel-main-template
with Reagent included, and https://github.com/react-financial/react-financial-charts
required via https://figwheel.org/docs/npm.html.
See also
- The BasicCandlestick.tsx[3] example
- Reagent docs on using React components[4]
- The investopedia entry[5] for the 'elder ray' indicator included in the example,
because it's hilarious
[1] https://github.com/react-financial/react-financial-charts/blob/b6bac7f5b1a375633eb9fc5a46da45880cd11f5a/packages/stories/src/features/StockChart.tsx
[2] https://react-financial.github.io/react-financial-charts/?path=/story/features-full-screen--daily
[3] https://github.com/react-financial/react-financial-charts/blob/b6bac7f5b1a375633eb9fc5a46da45880cd11f5a/packages/stories/src/series/candlestick/BasicCandlestick.tsx
[4] https://github.com/reagent-project/reagent/blob/master/doc/InteropWithReact.md#creating-reagent-components-from-react-components
[5] https://www.investopedia.com/articles/trading/03/022603.asp"
(:require [goog.dom :as gdom]
[reagent.core :as reagent :refer [atom]]
[reagent.dom :as rdom]
["react-financial-charts" :as rfc]))
;;; Some code to generate OHLC data in ~ a random walk
(defn rand-normal []
; https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform
(* (Math/sqrt (* -2 (Math/log (rand))))
(Math/cos (* 2 (.-PI js/Math) (rand)))))
(defn generate-candle [{:keys [date interval close volume]}]
(let [open close
random-price #(+ open (* (rand-normal) 0.005 open))
close (random-price)
[high low] [(random-price) (random-price)]
[low _ _ high] (sort [open high low close])
vol (max 250000 (+ volume (* (rand-normal) 0.20 volume)))]
{:date (new js/Date (+ (.getTime date) interval))
:interval 60000
:open open
:high high
:low low
:close close
:volume vol}))
(defn generate-candles
"Generate 1-minute candles for the past `n` minutes of data."
[n]
(let [now (.getTime (new js/Date))]
(->> {:date (new js/Date (- (* (quot now 60000) 60000) (* n 60000)))
:interval 60000
:close 20000.0
:volume 1000000.0}
(iterate generate-candle)
(drop 1)
(take n))))
(defonce data (atom (generate-candles 1000)))
(comment
; To get new data
(reset! data (generate-candles 1000)))
;;; Partition the chart into two, with the "elder ray" indicator taking up the
;;; bottom 100px, and the candles taking up the rest of the space. Volume bars
;;; are displayed behind the bottom 4th of the candle area.
(def margin {:left 0 :right 80 :top 0 :bottom 24})
(def height 500)
(def width 750)
(def grid-height (- height (:top margin) (:bottom margin)))
(def elder-ray-height 100)
(defn elder-ray-origin [_ h] (clj->js [0 (- h elder-ray-height)]))
(def bar-chart-height (/ grid-height 4))
(defn bar-chart-origin [_ h] (clj->js [0 (- h bar-chart-height elder-ray-height)]))
(def chart-height (- grid-height elder-ray-height))
;;; Formatting functions use for the axis / tooltips
(defn price-fmt [n] (when n (.toFixed n 2)))
(defn time-fmt [d] (.toISOString d))
;;; Signals which are plotted in addition to OHLC data. EMAs are plotted as
;;; lines alongside the candles, the "elder ray" is plotted below.
(def ema12
(-> (rfc/ema)
(.id 1)
(.options #js {:windowSize 12})
(.merge (fn [d c] (set! (.-ema12 d) c) d))
(.accessor (fn [d] (.-ema12 d)))))
(def ema26
(-> (rfc/ema)
(.id 2)
(.options #js {:windowSize 26})
(.merge (fn [d c] (set! (.-ema26 d) c) d))
(.accessor (fn [d] (.-ema26 d)))))
(def elder (rfc/elderRay))
;;; Helper functions for building the React components
(def x-scale-provider
(-> (new rfc/discontinuousTimeScaleProviderBuilder)
(.inputDateAccessor (fn [x] (.-date x)))))
; Allow destructuring the output of x-scale-provider for convenience
(defn apply-xsp [data]
(let [xsp (x-scale-provider data)]
{:data (.-data xsp)
:xScale (.-xScale xsp)
:xAccessor (.-xAccessor xsp)
:displayXAccessor (.-displayXAccessor xsp)}))
(def candle-chart-extents
(fn [data]
(clj->js [(.-high data) (.-low data)])))
(defn open-close-color [data]
(if (> (.-close data) (.-open data))
"#26a69a"
"#ef5350"))
(defn volume-color [data]
(if (> (.-close data) (.-open data))
"rgba(38, 166, 154, 0.3)"
"rgba(239, 83, 80, 0.3)"))
(defn chart [data]
(let [data (clj->js @data) ; this is probably unnecessarily slow
calculated-data (-> data ema12 ema26 elder)
{:keys [data xScale xAccessor displayXAccessor]} (apply-xsp calculated-data)
max (xAccessor (nth data (dec (count data))))
min (xAccessor (nth data (Math/max 0 (- (count data) 100))))
xExtents (clj->js [min max])]
[:> rfc/ChartCanvas
{:height height
:ratio 1
:width width
:margin (clj->js margin)
:data data
:displayXAccessor displayXAccessor
:seriesName "Data"
:xScale xScale
:xAccessor xAccessor
:xExtents xExtents
:zoomAnchor rfc/lastVisibleItemBasedZoomAnchor}
;;; First define the volume bars
[:> rfc/Chart {:id 2
:height bar-chart-height
:origin bar-chart-origin
:yExtents #(.-volume %)}
[:> rfc/BarSeries {:fillStyle volume-color :yAccessor #(.-volume %)}]]
;;; Then the candles
[:> rfc/Chart {:id 3
:height chart-height
:yExtents candle-chart-extents}
[:> rfc/XAxis {:showGridLines true :showTicks false :showTickLabel false}]
[:> rfc/YAxis {:showGridLines true :tickFormat price-fmt}]
[:> rfc/CandlestickSeries]
[:> rfc/LineSeries {:yAccessor (.accessor ema26)
:strokeStyle (.stroke ema26)}]
[:> rfc/CurrentCoordinate {:yAccessor (.accessor ema26)
:strokeStyle (.stroke ema26)}]
[:> rfc/LineSeries {:yAccessor (.accessor ema12)
:strokeStyle (.stroke ema12)}]
[:> rfc/CurrentCoordinate {:yAccessor (.accessor ema12)
:strokeStyle (.stroke ema12)}]
[:> rfc/MouseCoordinateY {:rectWidth (:right margin)
:displayFormat price-fmt}]
[:> rfc/EdgeIndicator
{:itemType "last"
:rectWidth (:right margin)
:fill open-close-color
:lineStroke open-close-color
:displayFormat price-fmt
:yAccessor #(.-close %)}]
[:> rfc/MovingAverageTooltip
{:origin #js [8 24]
:options (clj->js
[{:yAccessor (.accessor ema26)
:type "EMA"
:stroke (.stroke ema26)
:windowSize (-> ema26 .options .-windowSize)}
{:yAccessor (.accessor ema12)
:type "EMA"
:stroke (.stroke ema12)
:windowSize (-> ema12 .options .-windowSize)}])}]
[:> rfc/ZoomButtons]
[:> rfc/OHLCTooltip {:origin #js[8 16]}]]
;;; Then finally the elder ray chart
[:> rfc/Chart
{:id 4
:height elder-ray-height
:yExtents (clj->js [0 (.accessor elder)])
:origin elder-ray-origin
:padding #js{:top 8 :bottom 8}}
[:> rfc/XAxis {:showGridLines true :gridLinesStrokeStyle "#e0e3eb"}]
[:> rfc/YAxis {:ticks 4 :tickFormat price-fmt}]
[:> rfc/MouseCoordinateX {:displayFormat time-fmt}]
[:> rfc/MouseCoordinateY {:rectWidth (:right margin)
:displayFormat price-fmt}]
[:> rfc/ElderRaySeries {:yAccessor (.accessor elder)}]
[:> rfc/SingleValueTooltip
{:yAccessor (.accessor elder)
:yLabel "Elder Ray"
:yDisplayFormat (fn [d]
(clj->js
[(price-fmt (.-bullPower d))
(price-fmt (.-bearPower d))]))
:origin #js [8 16]}]]
[:> rfc/CrossHairCursor]]))
;;; Boilerplate figwheel-main + reagent
(defn get-app-element []
(gdom/getElement "app"))
(defn mount [el]
(rdom/render [chart data] el))
(defn mount-app-element []
(when-let [el (get-app-element)]
(mount el)))
(mount-app-element)
(defn ^:after-load on-reload []
(mount-app-element))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment