Skip to content

Commit

Permalink
Split up chart.jl and place in new module TechnicalIndicatorCharts
Browse files Browse the repository at this point in the history
  • Loading branch information
g-gundam committed Jul 14, 2024
1 parent 9421bf1 commit 060eed6
Show file tree
Hide file tree
Showing 4 changed files with 468 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/TechnicalIndicatorCharts.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
module TechnicalIndicatorCharts

using Dates
using NanoDates
using Chain
using DataFrames
using DataFramesMeta
using OnlineTechnicalIndicators
using OnlineTechnicalIndicators: TechnicalIndicator
using LightweightCharts

# Write your package code here.
# structs
# - exported

@kwdef mutable struct Candle
ts::DateTime
o::Float64
h::Float64
l::Float64
c::Float64
v::Float64
end

@kwdef mutable struct Chart
# This is the user-facing data.
name::AbstractString # name
tf::Period # time frame
indicators::Vector{TechnicalIndicator} # indicators to add to the dataframe
visuals::Vector # visualization parameters for each indicator
df::DataFrame # dataframe

# There is also some internal data that I use to keep track of computations in progress.
ts::Union{DateTime,Missing}
candle::Union{Candle,Missing}
end

export Candle
export Chart

# helpers
# - abbrev
# - private
include("./helpers.jl")

# data calculation
# - export update!
include("./data.jl")
export update!
export chart

# visualization
# - a function for each indicator
# - exported
include("./visualize.jl")
export visualize

end
201 changes: 201 additions & 0 deletions src/data.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""
Return a tuple of symbol names to be used for the output of `ind`.
"""
function indicator_fields(ind::OnlineTechnicalIndicators.TechnicalIndicatorSingleOutput)
name = @chain ind begin
typeof
string
replace(_, r"\{.*}$" => "")
replace(_, r"OnlineTechnicalIndicators." => "")
lowercase
end
if hasproperty(ind, :period)
return (Symbol("$(name)$(string(ind.period))"),)
else
return (Symbol(name),)
end
end

function indicator_fields(ind::OnlineTechnicalIndicators.TechnicalIndicatorMultiOutput)
name = @chain ind begin
typeof
string
replace(_, r"\{.*}$" => "")
replace(_, r"OnlineTechnicalIndicators." => "")
lowercase
end
fnames = @chain ind begin
typeof
fieldtypes
(ts -> ts[1])(_)
getproperty(_, :b)
fieldnames
map(n -> Symbol("$(name)_$(string(n))"), _)
end
end

function indicator_fields_count(ind::OnlineTechnicalIndicators.TechnicalIndicatorMultiOutput)
@chain ind typeof fieldtypes (ts -> ts[1])(_) getproperty(_, :b) fieldcount
end


function df_fields(indicators)
base = (:ts, :o, :h, :l, :c, :v)
fs = indicator_fields.(indicators) |> Iterators.flatten |> collect
combined = Iterators.flatten([base, fs]) |> collect
map(k -> ifelse(k == :ts, k=>DateTime[], k=>Union{Missing,Float64}[]), combined)
end

function extract_value(value)
# - value is a Value like BBVal, but I couldn't find a supertype that encompassed all indicator values.
# - it's only intended to be used indicators that emit multiple values per tick.
fnames = @chain value begin
typeof
fieldnames
end
res = []
for f in fnames
push!(res, getproperty(value, f))
end
res
end

""" merge_candle!(last_candle, c)
If last candle is not provided, construct a new candle with the given OHLCV data.
If last candle is provided, mutate last_candle such that it's HLCV are updated.
It's assumed that last_candle and c have the same timestamp.
"""
function merge_candle!(last_candle::Union{Missing, Candle}, c::Union{Candle,DataFrameRow})
if ismissing(last_candle)
return Candle(ts=c.ts, o=c.o, h=c.h, l=c.l, c=c.c, v=c.v)
else
last_candle.h = max(last_candle.h, c.h)
last_candle.l = min(last_candle.l, c.l)
last_candle.c = c.c
last_candle.v = c.v
return last_candle
end
end

function flatten_indicator_values(vs)
Iterators.flatmap(x -> ifelse(ismissing(x), (missing,), x), vs)
end

"""
This is meant to be called on timeframe boundaries to onto the chart's
dataframe. It also does indicator calculation at this time.
"""
function push_new_candle!(chart::Chart, c::Candle)
vs = map(chart.indicators) do ind
# what kind of inputs does this indicator want?
if ismultiinput(ind)
# TODO - feed it a whole candle instead
fit!(ind, c.c)
else
fit!(ind, c.c)
end
if ismultioutput(ind)
if ismissing(ind.value)
return repeat([missing], indicator_fields_count(ind))
else
return [ind.value.lower, ind.value.central, ind.value.upper]
end
else
return ind.value
end
end
fvs = flatten_indicator_values(vs)
push!(chart.df, (
c.ts,
c.o,
c.h,
c.l,
c.c,
c.v,
fvs...
))
end

"""
This is for internal housekeeping inside chart.candle.
This happens when we're away from a chart.tf boundary.
This doesn't go into a DataFrame.
"""
function update_last_candle!(chart::Chart, c::Candle)
row = last(chart.df)
row.h = c.h
row.l = c.l
row.c = c.c
row.v = c.v
end

# I need a way to feed it candles and indicators
function update!(chart::Chart, c::Candle)
# aggregation when tf > Minute(1)
# fit! on series after candle close only
if ismissing(chart.ts)
# initial case
chart.ts = floor(c.ts, chart.tf)
chart.candle = merge_candle!(missing, c)
push_new_candle!(chart, c)
return
end

if chart.ts != floor(c.ts, chart.tf)
# timeframe boundary case
chart.ts = floor(c.ts, chart.tf)
chart.candle = merge_candle!(missing, c)
push_new_candle!(chart, c)
else
# normal case
chart.candle = merge_candle!(chart.candle, c)
update_last_candle!(chart, c)
end
end

# This translates a DataFrameRow to a Candle before sending it to
# the original update! function.
function update!(chart::Chart, dfr::DataFrameRow)
c = Candle(
ts=dfr.ts,
o=dfr.o,
h=dfr.h,
l=dfr.l,
c=dfr.c,
v=dfr.v
)
update!(chart, c)
end

""" chart(name, tf; indicators, visuals)
Construct a Chart instance configured with the given indicators and visual parameters.
# Example
```julia-repl
julia> golden_cross = chart(
"BTCUSD", Hour(4);
indicators = [
SMA{Float64}(;period=50),
SMA{Float64}(;period=200)
],
visuals = [
Dict(
:label_name => "SMA 50",
:line_color => "#E072A4",
:line_width => 2
),
Dict(
:label_name => "SMA 200",
:line_color => "#3D3B8E",
:line_width => 5
)
]
)
```
"""
function chart(name, tf; indicators::Vector=[], visuals::Vector{Dict}=[])
end
51 changes: 51 additions & 0 deletions src/helpers.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
abbrev(ns::Nanosecond) = "$(ns.value)ns"
abbrev(us::Microsecond) = "$(us.value)us"
abbrev(ms::Millisecond) = "$(ms.value)ms"
abbrev(s::Second) = "$(s.value)s"
abbrev(m::Minute) = "$(m.value)m"
abbrev(h::Hour) = "$(h.value)h"
abbrev(d::Day) = "$(d.value)d"
abbrev(w::Week) = "$(w.value)w"
abbrev(M::Month) = "$(M.value)M"
abbrev(Q::Quarter) = "$(Q.value)Q"
abbrev(Y::Year) = "$(Y.value)Y"
function abbrev(canon::Dates.CompoundPeriod; minimum=Minute)
@chain canon.periods begin
filter(p -> typeof(p) >= minimum, _)
map(abbrev, _)
join(" ")
end
end

""" abbrev(p::Period)
Return an abbreviated string representation of the given period.
# Example
```julia
abbrev(Hour(4)) # "4h"
abbrev(Day(1)) # "1d"
```
"""
abbrev(p::Period)

"""
This is a wrapper around `OnlineTechnicalIndicators.ismultiinput` that takes
any instance of a TechnicalIndicator and digs out its unparametrized type before running
the original ismultiinput method.
"""
function ismultiinput(i::TechnicalIndicator)
t = typeof(i)
OnlineTechnicalIndicators.ismultiinput(t.name.wrapper)
end

"""
This is a wrapper around `OnlineTechnicalIndicators.ismultioutput` that takes
any instance of a TechnicalIndicator and digs out its unparametrized type before running
the original ismultioutput method.
"""
function ismultioutput(i::TechnicalIndicator)
t = typeof(i)
OnlineTechnicalIndicators.ismultioutput(t.name.wrapper)
end
Loading

0 comments on commit 060eed6

Please sign in to comment.