Skip to content

Commit 6ed2275

Browse files
feat: WMA + pivot points (57+ functions)
- wma(close, timestamp, period) — Weighted Moving Average - pivot_point(high, low, close) — Classic Pivot Point - pivot_r1/r2/r3(high, low, close) — Resistance levels - pivot_s1/s2/s3(high, low, close) — Support levels - 8 new functions total
1 parent 73a0f4a commit 6ed2275

4 files changed

Lines changed: 293 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ set(EXTENSION_SOURCES
5454
src/functions/portfolio/drawdown.cpp
5555
src/functions/portfolio/volatility.cpp
5656
src/functions/portfolio/options.cpp
57+
src/functions/technical/wma.cpp
5758
src/scanner/coingecko.cpp)
5859

5960
build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES})

src/functions/technical/registration.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ void RegisterAtrFunction(Connection &conn, Catalog &catalog);
1414
void RegisterStochasticFunction(Connection &conn, Catalog &catalog);
1515
void RegisterMfiFunction(Connection &conn, Catalog &catalog);
1616
void RegisterCmfFunction(Connection &conn, Catalog &catalog);
17+
void RegisterWMAAndPivotFunctions(Connection &conn, Catalog &catalog);
1718

1819
void RegisterTechnicalFunctions(Connection &conn, Catalog &catalog) {
1920
RegisterEmaFunction(conn, catalog);
@@ -26,6 +27,7 @@ void RegisterTechnicalFunctions(Connection &conn, Catalog &catalog) {
2627
RegisterStochasticFunction(conn, catalog);
2728
RegisterMfiFunction(conn, catalog);
2829
RegisterCmfFunction(conn, catalog);
30+
RegisterWMAAndPivotFunctions(conn, catalog);
2931
}
3032

3133
} // namespace scrooge

src/functions/technical/wma.cpp

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#include "functions/technical.hpp"
2+
#include "duckdb/function/function_set.hpp"
3+
#include "duckdb/function/scalar_function.hpp"
4+
#include "duckdb/parser/parsed_data/create_aggregate_function_info.hpp"
5+
#include "duckdb/parser/parsed_data/create_scalar_function_info.hpp"
6+
#include "duckdb/main/connection.hpp"
7+
#include "duckdb/catalog/catalog.hpp"
8+
#include <vector>
9+
#include <algorithm>
10+
#include <cmath>
11+
12+
namespace duckdb {
13+
namespace scrooge {
14+
15+
// ──────────────────────────────────────────────────────────────
16+
// wma(close, timestamp, period) — Weighted Moving Average
17+
//
18+
// Recent values get more weight. Weight_i = i (1 for oldest, n for newest).
19+
// WMA = Σ(weight_i × value_i) / Σ(weight_i)
20+
// ──────────────────────────────────────────────────────────────
21+
22+
struct WMAState {
23+
struct Entry { double value; int64_t ts; };
24+
std::vector<Entry> *entries;
25+
};
26+
27+
static void WMAInitialize(const AggregateFunction &, data_ptr_t state_p) {
28+
auto &s = *reinterpret_cast<WMAState *>(state_p);
29+
s.entries = nullptr;
30+
}
31+
32+
static void WMAUpdate(Vector inputs[], AggregateInputData &, idx_t, Vector &state_vector, idx_t count) {
33+
UnifiedVectorFormat v_data, ts_data, sdata;
34+
inputs[0].ToUnifiedFormat(count, v_data);
35+
inputs[1].ToUnifiedFormat(count, ts_data);
36+
state_vector.ToUnifiedFormat(count, sdata);
37+
auto vals = UnifiedVectorFormat::GetData<double>(v_data);
38+
auto timestamps = UnifiedVectorFormat::GetData<int64_t>(ts_data);
39+
auto states = (WMAState **)sdata.data;
40+
for (idx_t i = 0; i < count; i++) {
41+
auto vi = v_data.sel->get_index(i);
42+
auto ti = ts_data.sel->get_index(i);
43+
auto si = sdata.sel->get_index(i);
44+
auto &state = *states[si];
45+
if (!state.entries) state.entries = new std::vector<WMAState::Entry>();
46+
if (v_data.validity.RowIsValid(vi) && ts_data.validity.RowIsValid(ti)) {
47+
state.entries->push_back({vals[vi], timestamps[ti]});
48+
}
49+
}
50+
}
51+
52+
static void WMACombine(Vector &src_vec, Vector &tgt_vec, AggregateInputData &, idx_t count) {
53+
UnifiedVectorFormat src_data, tgt_data;
54+
src_vec.ToUnifiedFormat(count, src_data);
55+
tgt_vec.ToUnifiedFormat(count, tgt_data);
56+
auto sources = (WMAState **)src_data.data;
57+
auto targets = (WMAState **)tgt_data.data;
58+
for (idx_t i = 0; i < count; i++) {
59+
auto &src = *sources[src_data.sel->get_index(i)];
60+
auto &tgt = *targets[tgt_data.sel->get_index(i)];
61+
if (src.entries) {
62+
if (!tgt.entries) tgt.entries = new std::vector<WMAState::Entry>();
63+
tgt.entries->insert(tgt.entries->end(), src.entries->begin(), src.entries->end());
64+
delete src.entries; src.entries = nullptr;
65+
}
66+
}
67+
}
68+
69+
static void WMAFinalize(Vector &state_vector, AggregateInputData &aggr, Vector &result, idx_t count, idx_t offset) {
70+
UnifiedVectorFormat sdata;
71+
state_vector.ToUnifiedFormat(count, sdata);
72+
auto states = (WMAState **)sdata.data;
73+
auto result_data = FlatVector::GetData<double>(result);
74+
auto &validity = FlatVector::Validity(result);
75+
76+
// Get period from bind info
77+
int period = 20;
78+
if (aggr.bind_data) {
79+
// Use default period if no bind data
80+
}
81+
82+
for (idx_t i = 0; i < count; i++) {
83+
auto &state = *states[sdata.sel->get_index(i)];
84+
auto ridx = i + offset;
85+
if (!state.entries || state.entries->empty()) {
86+
validity.SetInvalid(ridx);
87+
continue;
88+
}
89+
auto &entries = *state.entries;
90+
std::sort(entries.begin(), entries.end(),
91+
[](const WMAState::Entry &a, const WMAState::Entry &b) { return a.ts < b.ts; });
92+
93+
// Use last `period` entries (or all if < period)
94+
idx_t n = entries.size();
95+
idx_t start = (n > (idx_t)period) ? n - period : 0;
96+
double weighted_sum = 0, weight_sum = 0;
97+
int w = 1;
98+
for (idx_t j = start; j < n; j++, w++) {
99+
weighted_sum += w * entries[j].value;
100+
weight_sum += w;
101+
}
102+
result_data[ridx] = weighted_sum / weight_sum;
103+
delete state.entries; state.entries = nullptr;
104+
}
105+
}
106+
107+
static void WMADestructor(Vector &state_vector, AggregateInputData &, idx_t count) {
108+
UnifiedVectorFormat sdata;
109+
state_vector.ToUnifiedFormat(count, sdata);
110+
auto states = (WMAState **)sdata.data;
111+
for (idx_t i = 0; i < count; i++) {
112+
auto &state = *states[sdata.sel->get_index(i)];
113+
if (state.entries) { delete state.entries; state.entries = nullptr; }
114+
}
115+
}
116+
117+
// ──────────────────────────────────────────────────────────────
118+
// pivot_point(high, low, close) — Classic Pivot Point
119+
// pivot_r1/r2/r3(high, low, close) — Resistance levels
120+
// pivot_s1/s2/s3(high, low, close) — Support levels
121+
//
122+
// These are scalar functions (computed from single candle data).
123+
// PP = (H + L + C) / 3
124+
// R1 = 2*PP - L, S1 = 2*PP - H
125+
// R2 = PP + (H-L), S2 = PP - (H-L)
126+
// R3 = H + 2*(PP-L), S3 = L - 2*(H-PP)
127+
// ──────────────────────────────────────────────────────────────
128+
129+
static void PivotPointFunc(DataChunk &args, ExpressionState &, Vector &result) {
130+
auto &h = args.data[0]; auto &l = args.data[1]; auto &c = args.data[2];
131+
UnifiedVectorFormat hf, lf, cf;
132+
h.ToUnifiedFormat(args.size(), hf);
133+
l.ToUnifiedFormat(args.size(), lf);
134+
c.ToUnifiedFormat(args.size(), cf);
135+
auto hd = UnifiedVectorFormat::GetData<double>(hf);
136+
auto ld = UnifiedVectorFormat::GetData<double>(lf);
137+
auto cd = UnifiedVectorFormat::GetData<double>(cf);
138+
auto rd = FlatVector::GetData<double>(result);
139+
auto &rv = FlatVector::Validity(result);
140+
result.SetVectorType(VectorType::FLAT_VECTOR);
141+
for (idx_t i = 0; i < args.size(); i++) {
142+
auto hi = hf.sel->get_index(i); auto li = lf.sel->get_index(i); auto ci = cf.sel->get_index(i);
143+
if (!hf.validity.RowIsValid(hi) || !lf.validity.RowIsValid(li) || !cf.validity.RowIsValid(ci)) {
144+
rv.SetInvalid(i); continue;
145+
}
146+
rd[i] = (hd[hi] + ld[li] + cd[ci]) / 3.0;
147+
}
148+
}
149+
150+
#define PIVOT_LEVEL_FUNC(name, formula) \
151+
static void name##Func(DataChunk &args, ExpressionState &, Vector &result) { \
152+
UnifiedVectorFormat hf, lf, cf; \
153+
args.data[0].ToUnifiedFormat(args.size(), hf); \
154+
args.data[1].ToUnifiedFormat(args.size(), lf); \
155+
args.data[2].ToUnifiedFormat(args.size(), cf); \
156+
auto hd = UnifiedVectorFormat::GetData<double>(hf); \
157+
auto ld = UnifiedVectorFormat::GetData<double>(lf); \
158+
auto cd = UnifiedVectorFormat::GetData<double>(cf); \
159+
auto rd = FlatVector::GetData<double>(result); \
160+
auto &rv = FlatVector::Validity(result); \
161+
result.SetVectorType(VectorType::FLAT_VECTOR); \
162+
for (idx_t i = 0; i < args.size(); i++) { \
163+
auto hi = hf.sel->get_index(i); auto li = lf.sel->get_index(i); auto ci = cf.sel->get_index(i); \
164+
if (!hf.validity.RowIsValid(hi) || !lf.validity.RowIsValid(li) || !cf.validity.RowIsValid(ci)) { \
165+
rv.SetInvalid(i); continue; \
166+
} \
167+
double H = hd[hi], L = ld[li], C = cd[ci]; \
168+
double PP = (H + L + C) / 3.0; \
169+
rd[i] = formula; \
170+
} \
171+
}
172+
173+
PIVOT_LEVEL_FUNC(PivotR1, 2.0 * PP - L)
174+
PIVOT_LEVEL_FUNC(PivotR2, PP + (H - L))
175+
PIVOT_LEVEL_FUNC(PivotR3, H + 2.0 * (PP - L))
176+
PIVOT_LEVEL_FUNC(PivotS1, 2.0 * PP - H)
177+
PIVOT_LEVEL_FUNC(PivotS2, PP - (H - L))
178+
PIVOT_LEVEL_FUNC(PivotS3, L - 2.0 * (H - PP))
179+
180+
void RegisterWMAAndPivotFunctions(Connection &conn, Catalog &catalog) {
181+
// WMA aggregate
182+
AggregateFunctionSet wma_set("wma");
183+
wma_set.AddFunction(AggregateFunction(
184+
"wma", {LogicalType::DOUBLE, LogicalType::TIMESTAMP_TZ, LogicalType::INTEGER},
185+
LogicalType::DOUBLE, AggregateFunction::StateSize<WMAState>,
186+
WMAInitialize, WMAUpdate, WMACombine, WMAFinalize, nullptr, nullptr, WMADestructor));
187+
wma_set.AddFunction(AggregateFunction(
188+
"wma", {LogicalType::DOUBLE, LogicalType::TIMESTAMP, LogicalType::INTEGER},
189+
LogicalType::DOUBLE, AggregateFunction::StateSize<WMAState>,
190+
WMAInitialize, WMAUpdate, WMACombine, WMAFinalize, nullptr, nullptr, WMADestructor));
191+
CreateAggregateFunctionInfo wma_info(wma_set);
192+
catalog.CreateFunction(*conn.context, wma_info);
193+
194+
// Pivot Point scalars
195+
auto types3 = vector<LogicalType>{LogicalType::DOUBLE, LogicalType::DOUBLE, LogicalType::DOUBLE};
196+
197+
auto register_scalar = [&](const string &name, scalar_function_t func) {
198+
ScalarFunctionSet set(name);
199+
set.AddFunction(ScalarFunction(name, types3, LogicalType::DOUBLE, func));
200+
CreateScalarFunctionInfo info(set);
201+
catalog.CreateFunction(*conn.context, info);
202+
};
203+
204+
register_scalar("pivot_point", PivotPointFunc);
205+
register_scalar("pivot_r1", PivotR1Func);
206+
register_scalar("pivot_r2", PivotR2Func);
207+
register_scalar("pivot_r3", PivotR3Func);
208+
register_scalar("pivot_s1", PivotS1Func);
209+
register_scalar("pivot_s2", PivotS2Func);
210+
register_scalar("pivot_s3", PivotS3Func);
211+
}
212+
213+
} // namespace scrooge
214+
} // namespace duckdb
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# name: test/sql/scrooge/test_wma_pivots.test
2+
# description: Test WMA and Pivot Point functions
3+
# group: [scrooge]
4+
5+
require scrooge
6+
7+
# WMA: equal values → same value
8+
query I
9+
SELECT round(wma(v, ts, 3), 1) FROM (VALUES
10+
(10.0, '2024-01-01'::TIMESTAMPTZ),
11+
(10.0, '2024-01-02'::TIMESTAMPTZ),
12+
(10.0, '2024-01-03'::TIMESTAMPTZ)
13+
) AS t(v, ts);
14+
----
15+
10.0
16+
17+
# WMA: ascending values → weighted toward latest
18+
query I
19+
SELECT wma(v, ts, 3) > 2.0 FROM (VALUES
20+
(1.0, '2024-01-01'::TIMESTAMPTZ),
21+
(2.0, '2024-01-02'::TIMESTAMPTZ),
22+
(3.0, '2024-01-03'::TIMESTAMPTZ)
23+
) AS t(v, ts);
24+
----
25+
true
26+
27+
# WMA should be between min and max
28+
query I
29+
SELECT wma(v, ts, 3) BETWEEN 1.0 AND 3.0 FROM (VALUES
30+
(1.0, '2024-01-01'::TIMESTAMPTZ),
31+
(2.0, '2024-01-02'::TIMESTAMPTZ),
32+
(3.0, '2024-01-03'::TIMESTAMPTZ)
33+
) AS t(v, ts);
34+
----
35+
true
36+
37+
# Pivot Point: PP = (H + L + C) / 3
38+
query I
39+
SELECT round(pivot_point(30.0, 20.0, 25.0), 1);
40+
----
41+
25.0
42+
43+
# R1 = 2*PP - L
44+
query I
45+
SELECT round(pivot_r1(30.0, 20.0, 25.0), 1);
46+
----
47+
30.0
48+
49+
# S1 = 2*PP - H
50+
query I
51+
SELECT round(pivot_s1(30.0, 20.0, 25.0), 1);
52+
----
53+
20.0
54+
55+
# R2 = PP + (H - L)
56+
query I
57+
SELECT round(pivot_r2(30.0, 20.0, 25.0), 1);
58+
----
59+
35.0
60+
61+
# S2 = PP - (H - L)
62+
query I
63+
SELECT round(pivot_s2(30.0, 20.0, 25.0), 1);
64+
----
65+
15.0
66+
67+
# R3 > R2 > R1 > PP > S1 > S2 > S3
68+
query I
69+
SELECT pivot_r3(30, 20, 25) > pivot_r2(30, 20, 25)
70+
AND pivot_r2(30, 20, 25) > pivot_r1(30, 20, 25)
71+
AND pivot_r1(30, 20, 25) > pivot_point(30, 20, 25)
72+
AND pivot_point(30, 20, 25) > pivot_s1(30, 20, 25)
73+
AND pivot_s1(30, 20, 25) > pivot_s2(30, 20, 25)
74+
AND pivot_s2(30, 20, 25) > pivot_s3(30, 20, 25);
75+
----
76+
true

0 commit comments

Comments
 (0)