The Hedgehog Language

A language reference for writing Custom Metrics in Hedgehog.

1. Introduction

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.

// 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.

2. Types

Numeric types

TypeDescription
Integer32-bit signed integer
Long64-bit signed integer
Float32-bit floating-point number
Double64-bit floating-point number
NumberGeneric numeric type (accepts any of the above)

Core types

TypeDescription
Booleantrue or false
StringText
TimeSeriesA sequence of date/value pairs
DateA calendar date (also called TimePoint)
DateSetA set of dates
InstrumentA tradeable entity (stock, ETF, currency pair, etc.)
CurrencyA currency
FrequencyA time frequency (e.g., daily, monthly)
MetricA reference to a metric (can be called as a function)
ObjectThe most general type; accepts any value
NoneNo value (used for metrics with no input)

Builder and iteration types

TypeDescription
TimeSeriesBuilderMutable builder for constructing a TimeSeries point by point
TickA single date/value pair from a TimeSeries
EnumEnumerated constants (e.g., Enum.DAYS)

Collection types

TypeDescription
ArrayAn ordered list of values (created with [...] syntax)
CollectionA generic collection of values
MapA key-value mapping
ExplorerGroupA named set of instruments or values

3. Literals

Numbers

42          // integer
3.14        // decimal
.5          // leading dot is allowed
-7          // negative (unary minus operator)

Strings

"hello"
"line one\nline two"     // escape sequences: \n \t \\ \" \' \uXXXX

Dates

Date literals use single quotes:

'2024-01-15'
'2024-12-31'

Booleans and null

true
false
null

Arrays

[1, 2, 3]
["a", "b", "c"]
[]              // empty array

Enum constants

Enum.DAYS
Enum.MONTHS

4. Variables

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;

5. Operators

Arithmetic

OperatorDescriptionExample
+Addition (also string concatenation)3 + 4
-Subtraction10 - 3
*Multiplication6 * 7
/Division10 / 3

Update-assign

x += 5;     // same as x = x + 5
x -= 1;
x *= 2;
x /= 3;

Increment / Decrement

++x;    // prefix increment
x++;    // postfix increment
--x;
x--;

Relational

OperatorDescription
==Equal to
!=Not equal to
<Less than
>Greater than
<=Less than or equal to
>=Greater than or equal to

Logical

OperatorDescriptionExample
&&Logical ANDtrue && false
||Logical ORtrue || false
!Logical NOT (prefix)!true

Ternary

// condition ? value_if_true : value_if_false
String label = (x > 0) ? "positive" : "not positive";

Elvis (null-coalescing)

// Returns the left side unless it is null, then returns the right side
null ?: "fallback"       // returns "fallback"
1 + 1 ?: "fallback"     // returns 2

Safe navigation

// 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

Casting

Object x = 42;
Integer n = (Integer) x;

Operator precedence (highest to lowest)

PrecedenceOperators
1 (highest).   ?. (member access)
2!   - (unary)   ++   --   (Type) cast
3*   /
4+   -
5<   >   <=   >=   ==   !=
6&&
7||
8?: (elvis)   ? : (ternary)
9 (lowest)=   +=   -=   *=   /=

6. Control Flow

if / else

if (price > 100) {
    print("expensive");
} else if (price > 50) {
    print("moderate");
} else {
    print("cheap");
}

for loop (C-style)

for (Integer i = 0; i < 10; ++i) {
    print(i);
}

foreach loop

for (Instrument stock : INDU.members()) {
    print(stock.toString());
}

while loop

Integer count = 0;
while (count < 5) {
    print(count);
    ++count;
}

try / catch / finally

try {
    TimeSeries ts = this.close();
    return ts;
} catch (Object e) {
    print("Error: " + e.toString());
    return null;
} finally {
    print("done");
}

return

return 42;
return;    // return with no value

7. Working with Time Series

Broadcasting (arithmetic with time series)

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);

Building a time series from scratch

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);

Iterating over ticks

TimeSeries prices = this.close();
for (Tick t : prices.ticks()) {
    print(t.date() + ": " + t.value());
}

8. Anonymous Metrics

Anonymous metrics are inline, reusable functions. They are declared with the metric keyword and can have parameters, an input type, and an output type.

Basic syntax

Metric myMetric = metric (parameters) : OutputType {
    // body
};

With input and output types

// Input: String, Output: String
Metric greet = metric : String -> String {
    return "Hello, " + this;
};

String msg = "World".greet();   // "Hello, World"

With parameters and defaults

// 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

Single-expression body

If the body is a single expression, no return is needed:

Metric square = metric (Number n) : Number { n * n };

Calling anonymous metrics

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();

9. Built-in Functions

General

FunctionDescription
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.

Collection and date functions

FunctionCalled onReturnsDescription
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.

Math functions

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);
FunctionDescription
absAbsolute value
sqrtSquare root
cbrtCube root
lnNatural logarithm
log10Base-10 logarithm
floorRound down
ceilRound up
signSign of the value (-1, 0, or 1)
sin, cos, tanTrigonometric functions
asin, acos, atanInverse trigonometric functions
sinh, cosh, tanhHyperbolic functions
toDegreesRadians to degrees
toRadiansDegrees to radians

10. Comments

// This is a line comment

/* This is a
   block comment */

11. Reserved Keywords

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

12. Common Patterns

Daily returns

// Input: Instrument, Output: TimeSeries
TimeSeries prices = this.close();
TimeSeries prev = prices.lag(1);
return (prices - prev) / prev;

Conditional output based on instrument type

// Input: Instrument, Output: TimeSeries
String typeName = this.getType().toString();
if (typeName == "Currency") {
    return this.close();
} else {
    return this.adjustedClose();
}

Sum values across a group

// Input: None, Output: Number
Number total = 0;
for (Instrument stock : INDU.members()) {
    total += stock.valueOn(today());
}
return total;

Build a custom time series from dates

// 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);

Safe metric chaining

// Use ?. to safely handle nulls in a chain
TimeSeries result = this?.close()?.sqrt();

// Use ?: to provide a fallback
Number val = this.valueOn(today()) ?: 0;

Using an anonymous metric as a parameter

// 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);