-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split up chart.jl and place in new module TechnicalIndicatorCharts
- Loading branch information
g-gundam
committed
Jul 14, 2024
1 parent
9421bf1
commit 060eed6
Showing
4 changed files
with
468 additions
and
0 deletions.
There are no files selected for viewing
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
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 |
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
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 |
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
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 |
Oops, something went wrong.