A language reference for writing Custom Metrics in Hedgehog.
The Hedgehog language (HHLang) is a small, statically-typed language for writing Custom Metrics. You write HHLang code in the Custom Metric editor inside the Hedgehog desktop client.
Every Custom Metric has an input type and an output type.
this — refers to the input value. For example, if the input type is
Instrument, then this is the instrument the metric is being
evaluated on.return — sends a value back as the metric's output. The type of the
returned value must match the declared output type.// Input: Instrument, Output: TimeSeries
TimeSeries open = this.open();
TimeSeries close = this.close();
return (open + close) / 2;
If your metric body is a single expression (no semicolons), the value of that
expression is returned automatically. Otherwise, use an explicit
return statement.
| Type | Description |
|---|---|
Integer | 32-bit signed integer |
Long | 64-bit signed integer |
Float | 32-bit floating-point number |
Double | 64-bit floating-point number |
Number | Generic numeric type (accepts any of the above) |
| Type | Description |
|---|---|
Boolean | true or false |
String | Text |
TimeSeries | A sequence of date/value pairs |
Date | A calendar date (also called TimePoint) |
DateSet | A set of dates |
Instrument | A tradeable entity (stock, ETF, currency pair, etc.) |
Currency | A currency |
Frequency | A time frequency (e.g., daily, monthly) |
Metric | A reference to a metric (can be called as a function) |
Object | The most general type; accepts any value |
None | No value (used for metrics with no input) |
| Type | Description |
|---|---|
TimeSeriesBuilder | Mutable builder for constructing a TimeSeries point by point |
Tick | A single date/value pair from a TimeSeries |
Enum | Enumerated constants (e.g., Enum.DAYS) |
| Type | Description |
|---|---|
Array | An ordered list of values (created with [...] syntax) |
Collection | A generic collection of values |
Map | A key-value mapping |
ExplorerGroup | A named set of instruments or values |
42 // integer
3.14 // decimal
.5 // leading dot is allowed
-7 // negative (unary minus operator)
"hello"
"line one\nline two" // escape sequences: \n \t \\ \" \' \uXXXX
Date literals use single quotes:
'2024-01-15'
'2024-12-31'
true
false
null
[1, 2, 3]
["a", "b", "c"]
[] // empty array
Enum.DAYS
Enum.MONTHS
Variables are declared with a type and a name. You can optionally assign a value at declaration time.
Integer x = 5;
String name = "Hedgehog";
TimeSeries prices = this.close();
Multiple variables of the same type can be declared in one statement:
Integer a = 1, b = 2, c;
Variables can be reassigned after declaration:
Integer x = 5;
x = 10;
| Operator | Description | Example |
|---|---|---|
+ | Addition (also string concatenation) | 3 + 4 |
- | Subtraction | 10 - 3 |
* | Multiplication | 6 * 7 |
/ | Division | 10 / 3 |
x += 5; // same as x = x + 5
x -= 1;
x *= 2;
x /= 3;
++x; // prefix increment
x++; // postfix increment
--x;
x--;
| Operator | Description |
|---|---|
== | Equal to |
!= | Not equal to |
< | Less than |
> | Greater than |
<= | Less than or equal to |
>= | Greater than or equal to |
| Operator | Description | Example |
|---|---|---|
&& | Logical AND | true && false |
|| | Logical OR | true || false |
! | Logical NOT (prefix) | !true |
// condition ? value_if_true : value_if_false
String label = (x > 0) ? "positive" : "not positive";
// Returns the left side unless it is null, then returns the right side
null ?: "fallback" // returns "fallback"
1 + 1 ?: "fallback" // returns 2
// If the left side is null, returns null immediately instead of
// calling the method (avoids null pointer errors)
null?.close() // returns null
AAPL?.close() // calls close() normally
Object x = 42;
Integer n = (Integer) x;
| Precedence | Operators |
|---|---|
| 1 (highest) | . ?. (member access) |
| 2 | ! - (unary) ++ -- (Type) cast |
| 3 | * / |
| 4 | + - |
| 5 | < > <= >= == != |
| 6 | && |
| 7 | || |
| 8 | ?: (elvis) ? : (ternary) |
| 9 (lowest) | = += -= *= /= |
if (price > 100) {
print("expensive");
} else if (price > 50) {
print("moderate");
} else {
print("cheap");
}
for (Integer i = 0; i < 10; ++i) {
print(i);
}
for (Instrument stock : INDU.members()) {
print(stock.toString());
}
Integer count = 0;
while (count < 5) {
print(count);
++count;
}
try {
TimeSeries ts = this.close();
return ts;
} catch (Object e) {
print("Error: " + e.toString());
return null;
} finally {
print("done");
}
return 42;
return; // return with no value
When you use arithmetic operators between a TimeSeries and a
Number, the operation is applied to every point in the series.
When you operate on two time series, matching dates are paired up.
TimeSeries prices = this.close();
// Number op TimeSeries: multiply every point by 2
TimeSeries doubled = 2 * prices;
// TimeSeries op TimeSeries: subtract point-by-point
TimeSeries spread = this.high() - this.low();
// Math functions also broadcast over time series
TimeSeries logPrices = ln(prices);
TimeSeriesBuilder builder = createTimeSeriesBuilder();
builder.addTick('2024-01-02', 100.5);
builder.addTick('2024-01-03', 101.2);
builder.addTick('2024-01-04', 99.8);
TimeSeries series = builder.toTimeSeries(Enum.DAYS);
TimeSeries prices = this.close();
for (Tick t : prices.ticks()) {
print(t.date() + ": " + t.value());
}
Anonymous metrics are inline, reusable functions. They are declared with the
metric keyword and can have parameters, an input type, and an output type.
Metric myMetric = metric (parameters) : OutputType {
// body
};
// Input: String, Output: String
Metric greet = metric : String -> String {
return "Hello, " + this;
};
String msg = "World".greet(); // "Hello, World"
// No input, takes a TimeSeries parameter and a Number with default 3
Metric scale = metric (TimeSeries t, Number factor = 3) : TimeSeries {
return factor * t;
};
TimeSeries result = scale(AAPL); // factor defaults to 3
TimeSeries result2 = scale(AAPL, 5); // factor is 5
If the body is a single expression, no return is needed:
Metric square = metric (Number n) : Number { n * n };
Anonymous metrics are called like methods. If the metric has an input type, call it on a value of that type using dot syntax. If it has no input type, call it like a function.
// No input type: call as a function
Number x = square(4);
// Has input type: call on a value
String greeting = "Alice".greet();
| Function | Description |
|---|---|
print(value) |
Prints the string representation of any value to the console. Returns the printed string. |
today() |
Returns the current evaluation date (a Date). |
createTimeSeriesBuilder() |
Creates a new TimeSeriesBuilder. Optionally pass a seed TimeSeries to copy metadata from. |
| Function | Called on | Returns | Description |
|---|---|---|---|
datesBetween(startDate, endDate) |
DateSet |
DateSet |
Returns dates in the set that fall between startDate and endDate (inclusive). |
filterEquals(obj) |
Collection |
Collection |
Returns only the elements equal to obj. |
generateTimeSeries(dateToValue) |
DateSet |
TimeSeries |
Applies a metric to each date, producing a time series. The metric must take a Date and return a Number. |
geometricMeanReturn(dates) |
TimeSeries |
Number |
Computes the geometric mean return over the given dates. Pass null to use all dates. |
getOrElse(key, orElse) |
Map |
Object |
Looks up key in the map. Returns the value if found, otherwise returns orElse. |
removeNulls() |
Collection |
Array |
Returns the collection as an array with all null values removed. |
toArray() |
Collection |
Array |
Converts the collection to an array. |
toDateSet() |
Date |
DateSet |
Wraps a single date into a DateSet. |
unique() |
Collection |
ExplorerGroup |
Returns the collection with duplicates removed. |
Each math function has three forms: called on a TimeSeries (applied
element-wise), called with a single Number, or called with a
TimeSeries argument.
// On a TimeSeries (element-wise)
TimeSeries sqrtPrices = prices.sqrt();
// On a single number
Number val = sqrt(16); // 4.0
// Passing a TimeSeries as an argument
TimeSeries result = sqrt(prices);
| Function | Description |
|---|---|
abs | Absolute value |
sqrt | Square root |
cbrt | Cube root |
ln | Natural logarithm |
log10 | Base-10 logarithm |
floor | Round down |
ceil | Round up |
sign | Sign of the value (-1, 0, or 1) |
sin, cos, tan | Trigonometric functions |
asin, acos, atan | Inverse trigonometric functions |
sinh, cosh, tanh | Hyperbolic functions |
toDegrees | Radians to degrees |
toRadians | Degrees to radians |
// This is a line comment
/* This is a
block comment */
The following words have special meaning and cannot be used as variable names:
catch
else
enum
false
finally
for
if
metric
null
return
true
try
while
// Input: Instrument, Output: TimeSeries
TimeSeries prices = this.close();
TimeSeries prev = prices.lag(1);
return (prices - prev) / prev;
// Input: Instrument, Output: TimeSeries
String typeName = this.getType().toString();
if (typeName == "Currency") {
return this.close();
} else {
return this.adjustedClose();
}
// Input: None, Output: Number
Number total = 0;
for (Instrument stock : INDU.members()) {
total += stock.valueOn(today());
}
return total;
// Input: Instrument, Output: TimeSeries
TimeSeries prices = this.close();
TimeSeriesBuilder builder = createTimeSeriesBuilder();
for (Tick t : prices.ticks()) {
if (t.value() > 100) {
builder.addTick(t.date(), t.value());
}
}
return builder.toTimeSeries(Enum.DAYS);
// Use ?. to safely handle nulls in a chain
TimeSeries result = this?.close()?.sqrt();
// Use ?: to provide a fallback
Number val = this.valueOn(today()) ?: 0;
// Input: None, Output: TimeSeries
// Generate a time series by evaluating a metric on each date
DateSet dates = '2024-01-01'.to('2024-12-31');
Metric countStocks = metric : Date -> Number {
Number count = 0;
for (Instrument s : INDU.members()) {
if (s.valueOn(this) > 100) {
++count;
}
}
return count;
};
return dates.generateTimeSeries(countStocks);