diff --git a/src/basemisc.jl b/src/basemisc.jl index 595199e9..18fa2029 100644 --- a/src/basemisc.jl +++ b/src/basemisc.jl @@ -37,12 +37,10 @@ macro _mapbase(sig::Expr, imp::Expr) doc = " $sig" fdef = Expr(:function, sig, fbody) - return esc( - quote - $doc - $fdef - end, - ) + return esc(quote + $doc + $fdef + end) end # Cumulative functions diff --git a/src/broadcast.jl b/src/broadcast.jl index 0a7bd59e..7b597210 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -13,7 +13,7 @@ Base.broadcastable(x::AbstractTimeSeries) = x Base.Broadcast.instantiate(bc::Broadcasted{<:TimeArrayStyle}) = # skip the default axes checking - Broadcast.flatten(bc) +Broadcast.flatten(bc) function Base.copy(bc′::Broadcasted{<:TimeArrayStyle}) tas = find_ta(bc′) diff --git a/src/combine.jl b/src/combine.jl index beec8692..04a6711e 100644 --- a/src/combine.jl +++ b/src/combine.jl @@ -206,9 +206,11 @@ collapse(ta, month, last) collapse(ta, month, last, mean) ``` """ -collapse( +function collapse( ta::TimeArray, period::Period, timestamp::Function, value::Function=timestamp; kw... -) = collapse(ta, x -> floor(x, period), timestamp, value; kw...) +) + collapse(ta, x -> floor(x, period), timestamp, value; kw...) +end # vcat ###################### diff --git a/src/modify.jl b/src/modify.jl index 6088adcb..3d0585f4 100644 --- a/src/modify.jl +++ b/src/modify.jl @@ -49,12 +49,15 @@ for f in [:rename, :rename!] @eval begin $f(ta::TimeArray, colnames::Symbol) = $f(ta, [colnames]) $f(ta::TimeArray) = throw(MethodError($f, (ta,))) - $f(ta::TimeArray, pairs::Pair{Symbol,Symbol}...) = - $f(ta, _mapcol(colnames(ta), pairs)) - $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{Symbol}=Symbol) = - $f(ta, map(f, colnames(ta))) - $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{String}) = - $f(Symbol ∘ f ∘ string, ta) + $f(ta::TimeArray, pairs::Pair{Symbol,Symbol}...) = $f( + ta, _mapcol(colnames(ta), pairs) + ) + $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{Symbol}=Symbol) = $f( + ta, map(f, colnames(ta)) + ) + $f(f::Base.Callable, ta::TimeArray, colnametyp::Type{String}) = $f( + Symbol ∘ f ∘ string, ta + ) end end diff --git a/src/retime.jl b/src/retime.jl index 1d0a972f..8df412fd 100644 --- a/src/retime.jl +++ b/src/retime.jl @@ -46,7 +46,7 @@ _toAggregationMethod(::Val{:sum}) = Sum() _toAggregationMethod(::Val{:median}) = Median() _toAggregationMethod(::Val{:first}) = First() _toAggregationMethod(::Val{:last}) = Last() -_toAggregationMethof(f::Function) = AggregationFunction(f) +_toAggregationMethod(f::Function) = AggregationFunction(f) _toAggregationMethod(x::AggregationMethod) = x _toExtrapolationMethod(x::Symbol) = _toExtrapolationMethod(Val(x)) diff --git a/src/split.jl b/src/split.jl index a8d3eb17..23ad8712 100644 --- a/src/split.jl +++ b/src/split.jl @@ -81,8 +81,9 @@ Split `data` by `period` function, returns a vector of `TimeSeries.TimeArray`. - `data::TimeSeries.TimeArray`: Data to split - `period::Function`: Function, e.g. `Dates.day` that is used to split the `data`. """ -Base.split(data::TimeSeries.TimeArray, period::Function) = +function Base.split(data::TimeSeries.TimeArray, period::Function) Iterators.map(i -> data[i], _split(TimeSeries.timestamp(data), period)) +end function _split(ts::AbstractVector{D}, period::Function) where {D<:TimeType} m = length(ts) diff --git a/src/timearray.jl b/src/timearray.jl index 351b44ca..2e1e6dc6 100644 --- a/src/timearray.jl +++ b/src/timearray.jl @@ -237,8 +237,8 @@ end io::IO, ::MIME"text/plain", ta::TimeArray; - allrows=!get(io, :limit, false), - allcols=!get(io, :limit, false), + allrows=(!get(io, :limit, false)), + allcols=(!get(io, :limit, false)), ) nrow = size(values(ta), 1) ncol = size(values(ta), 2) @@ -278,8 +278,8 @@ else io::IO, ::MIME"text/plain", ta::TimeArray; - allrows=!get(io, :limit, false), - allcols=!get(io, :limit, false), + allrows=(!get(io, :limit, false)), + allcols=(!get(io, :limit, false)), ) nrow = size(values(ta), 1) ncol = size(values(ta), 2) @@ -301,8 +301,8 @@ else row_label_column_alignment=:r, column_label_alignment=:l, continuation_row_alignment=:c, - fit_table_in_display_horizontally=!allcols, # replaced crop keyword for v3 of PrettyTables - fit_table_in_display_vertically=!allrows, # replaced crop keyword for v3 of PrettyTables + fit_table_in_display_horizontally=(!allcols), # replaced crop keyword for v3 of PrettyTables + fit_table_in_display_vertically=(!allrows), # replaced crop keyword for v3 of PrettyTables vertical_crop_mode=:middle, ) end @@ -318,27 +318,34 @@ getindex(ta::TimeArray) = throw(BoundsError(typeof(ta), [])) # single row @propagate_inbounds getindex(ta::TimeArray, n::Integer) = # avoid conversion to column vector - TimeArray(timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta)) +TimeArray( + timestamp(ta)[n], values(ta)[n:n, :], colnames(ta), meta(ta) +) # single row 1d -@propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = - TimeArray(timestamp(ta)[n], values(ta)[[n]], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, n::Integer) where {T} = TimeArray( + timestamp(ta)[n], values(ta)[[n]], colnames(ta), meta(ta) +) # range of rows -@propagate_inbounds getindex(ta::TimeArray, r::UnitRange{<:Integer}) = - TimeArray(timestamp(ta)[r], values(ta)[r, :], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray, r::UnitRange{<:Integer}) = TimeArray( + timestamp(ta)[r], values(ta)[r, :], colnames(ta), meta(ta) +) # range of 1d rows -@propagate_inbounds getindex(ta::TimeArray{T,1}, r::UnitRange{<:Integer}) where {T} = - TimeArray(timestamp(ta)[r], values(ta)[r], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, r::UnitRange{<:Integer}) where {T} = TimeArray( + timestamp(ta)[r], values(ta)[r], colnames(ta), meta(ta) +) # array of rows -@propagate_inbounds getindex(ta::TimeArray, a::AbstractVector{<:Integer}) = - TimeArray(timestamp(ta)[a], values(ta)[a, :], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray, a::AbstractVector{<:Integer}) = TimeArray( + timestamp(ta)[a], values(ta)[a, :], colnames(ta), meta(ta) +) # array of 1d rows -@propagate_inbounds getindex(ta::TimeArray{T,1}, a::AbstractVector{<:Integer}) where {T} = - TimeArray(timestamp(ta)[a], values(ta)[a], colnames(ta), meta(ta)) +@propagate_inbounds getindex(ta::TimeArray{T,1}, a::AbstractVector{<:Integer}) where {T} = TimeArray( + timestamp(ta)[a], values(ta)[a], colnames(ta), meta(ta) +) # single column by name @propagate_inbounds function getindex(ta::TimeArray, s::Symbol) @@ -348,15 +355,12 @@ end # array of columns by name @propagate_inbounds getindex(ta::TimeArray, ss::Symbol...) = getindex(ta, collect(ss)) -@propagate_inbounds getindex(ta::TimeArray, ss::Vector{Symbol}) = - TimeArray(ta; values=values(ta)[:, map(s -> findcol(ta, s), ss)], colnames=ss) +@propagate_inbounds getindex(ta::TimeArray, ss::Vector{Symbol}) = TimeArray( + ta; values=values(ta)[:, map(s -> findcol(ta, s), ss)], colnames=ss +) # ta[rows, cols] -@propagate_inbounds getindex( - ta::TimeArray, - rows::Union{AbstractVector{<:Integer},Colon}, - cols::AbstractVector{Symbol}, -) = TimeArray( +@propagate_inbounds getindex(ta::TimeArray, rows::Union{AbstractVector{<:Integer},Colon}, cols::AbstractVector{Symbol}) = TimeArray( ta; timestamp=timestamp(ta)[rows], values=values(ta)[rows, map(s -> findcol(ta, s), cols)], @@ -371,8 +375,9 @@ end @propagate_inbounds getindex(ta::TimeArray, rows, col::Symbol) = getindex(ta, rows, [col]) # ta[n, col] -@propagate_inbounds getindex(ta::TimeArray, n::Integer, col::Symbol) = - getindex(ta, [n], [col]) +@propagate_inbounds getindex(ta::TimeArray, n::Integer, col::Symbol) = getindex( + ta, [n], [col] +) # single date @propagate_inbounds function getindex(ta::TimeArray{T,N,D}, d::D) where {T,N,D} @@ -388,8 +393,9 @@ end end # StepRange{Date,...} -@propagate_inbounds getindex(ta::TimeArray{T,N,D}, r::StepRange{D}) where {T,N,D} = - ta[collect(r)] +@propagate_inbounds getindex(ta::TimeArray{T,N,D}, r::StepRange{D}) where {T,N,D} = ta[collect( + r +)] @propagate_inbounds getindex(ta::TimeArray, k::TimeArray{Bool,1}) = ta[findwhen(k)] diff --git a/test/retime.jl b/test/retime.jl index 1914a120..6fc2097c 100644 --- a/test/retime.jl +++ b/test/retime.jl @@ -24,6 +24,12 @@ using Statistics @test TimeSeries._toAggregationMethod(:first) == TimeSeries.First() @test TimeSeries._toAggregationMethod(:last) == TimeSeries.Last() + # Test function conversion to AggregationFunction + custom_func = x -> sum(x) / length(x) + agg_method = TimeSeries._toAggregationMethod(custom_func) + @test agg_method isa TimeSeries.AggregationFunction + @test agg_method.func === custom_func + @test_throws MethodError TimeSeries._toAggregationMethod(:foo) end @@ -34,6 +40,10 @@ using Statistics @test TimeSeries._toExtrapolationMethod(:missing) == TimeSeries.MissingExtrapolate() @test TimeSeries._toExtrapolationMethod(:nan) == TimeSeries.NaNExtrapolate() + # Test custom FillConstant values + @test TimeSeries.FillConstant(42.0).value == 42.0 + @test TimeSeries.FillConstant(-1).value == -1 + @test_throws MethodError TimeSeries._toExtrapolationMethod(:foo) end @@ -112,6 +122,11 @@ using Statistics @test timestamp(cl_new) == new_timestamps @test values(cl_new[:Close][1])[1] == 0.0 + # Test custom fill value + cl_new = retime(cl, new_timestamps; extrapolate=TimeSeries.FillConstant(99.9)) + @test timestamp(cl_new) == new_timestamps + @test values(cl_new[:Close][1])[1] == 99.9 + cl_new = retime(cl, new_timestamps; extrapolate=TimeSeries.NearestExtrapolate()) @test timestamp(cl_new) == new_timestamps @test values(cl_new[:Close][1])[1] == values(cl[:Close][1])[1] @@ -277,4 +292,70 @@ using Statistics ta_new = retime(ta, Hour(1); upsample=:linear) end + + @testset "AggregationFunction with custom function" begin + new_timestamps = collect(Dates.Date(2000):Dates.Week(1):Dates.Date(2001)) + + # Test with custom function: weighted average favouring first value + custom_agg = x -> 0.7 * first(x) + 0.3 * last(x) + cl_new = retime(cl, new_timestamps; downsample=custom_agg) + + @test timestamp(cl_new) == new_timestamps + + # Verify custom function is applied + idx = new_timestamps[2] .<= timestamp(cl) .< new_timestamps[3] + cl_values = values(cl[:Close][idx]) + expected = 0.7 * first(cl_values) + 0.3 * last(cl_values) + @test expected == values(cl_new[:Close][2])[1] + + # Test with standard deviation function + cl_new_std = retime(cl, new_timestamps; downsample=std) + @test timestamp(cl_new_std) == new_timestamps + + # Test with custom function on multi-column + ohlc_new = retime(ohlc, new_timestamps; downsample=x -> median(x)) + @test timestamp(ohlc_new) == new_timestamps + end + + @testset "Count aggregation" begin + new_timestamps = collect(Dates.Date(2000):Dates.Week(1):Dates.Date(2001)) + + cl_new = retime(cl, new_timestamps; downsample=TimeSeries.Count()) + @test timestamp(cl_new) == new_timestamps + + # Verify count is correct + idx = new_timestamps[2] .<= timestamp(cl) .< new_timestamps[3] + expected_count = count(!ismissing, values(cl[:Close][idx])) + @test expected_count == values(cl_new[:Close][2])[1] + + # Test count with symbol + cl_new_sym = retime(cl, new_timestamps; downsample=:count) + @test values(cl_new) == values(cl_new_sym) + end + + @testset "First and Last aggregation" begin + new_timestamps = collect(Dates.Date(2000):Dates.Week(1):Dates.Date(2001)) + + cl_first = retime(cl, new_timestamps; downsample=TimeSeries.First()) + cl_last = retime(cl, new_timestamps; downsample=TimeSeries.Last()) + + @test timestamp(cl_first) == new_timestamps + @test timestamp(cl_last) == new_timestamps + + # Verify first and last are correct + idx = new_timestamps[2] .<= timestamp(cl) .< new_timestamps[3] + @test first(values(cl[:Close][idx])) == values(cl_first[:Close][2])[1] + @test last(values(cl[:Close][idx])) == values(cl_last[:Close][2])[1] + end + + @testset "Median aggregation" begin + new_timestamps = collect(Dates.Date(2000):Dates.Week(1):Dates.Date(2001)) + + cl_new = retime(cl, new_timestamps; downsample=:median) + @test timestamp(cl_new) == new_timestamps + + # Verify median is correct + idx = new_timestamps[2] .<= timestamp(cl) .< new_timestamps[3] + @test median(values(cl[:Close][idx])) == values(cl_new[:Close][2])[1] + end end