Last active
September 9, 2022 14:42
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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