Strategies Showcase

Real strategies shipped in the user/strategies folder of the Planar.jl repository — from a bare template to margin trading with streaming indicators. The same code runs in backtest, paper, and live mode.

BareStrat

The base template every Planar strategy extends

BitMEX
Isolated
1m
module BareStrat
using ..Strategies: Strategies as st
using .st, .st.ExchangeTypes
using .st.TimeTicks
import .st: call!
using .st.Misc: Sim, NoMargin, Paper
using .st: Buy, Sell
using .st.OrderTypes: MarketOrder, ShortMarketOrder

const DESCRIPTION = "BaseStrat"
const EXC = :bitmex
const S{M} = Strategy{M,nameof(@__MODULE__),typeof(EXCID),Isolated}

function call!(s::T, ts::DateTime, ctx) where {T<:SC}
    foreach(s.universe) do ai
        oside = rand((Buy, Sell))
        pside = rand((Long, Short))
        if isopen(ai)
            call!(s, ai, ordertp(ai, oside, pside); amount=float(ai) / 3, date)
        elseif cash(s) > ai.limits.cost.min
            call!(s, ai, ordertp(ai, oside, pside); amount=ai.limits.amount.min, date)
        end
    end
end

function call!(::Type{<:SC}, ::StrategyMarkets)
    ["BTC/USDT:USDT", "ETH/USDT:USDT", "SOL/USDT:USDT"]
end
end

SimpleStrategy

Moving-average crossover on daily BTC

Binance
NoMargin
1d
function call!(s::SC, ts::DateTime, _)
    ats = available(s.timeframe, ts)
    foreach(s.universe) do ai
        df = ohlcv(ai)
        idx = dateindex(df, ats)
        if idx > 15
            ma7d  = mean(@view df.close[(idx - 7):idx])
            ma15d = mean(@view df.close[(idx - 15):idx])
            side = ifelse(ma7d > ma15d, Buy, Sell)
            call!(s, ai, MarketOrder{side}; date=ts, amount=0.001)
        end
    end
end

const ASSETS = ["BTC/USDT"]
function call!(::Union{<:SC,Type{<:SC}}, ::StrategyMarkets)
    ASSETS
end

TwoIntervals

Dual-timeframe trend following with EMA + RSI

Binance
NoMargin
15m / 1h
@enum Trend Down = 0 Up = 1

function handler(s, ai, ats, date)
    ohlcv = ai.data[tf"1h"]
    idx = dateindex(ohlcv, ats)
    idx < 1 && return nothing
    this_trend = ifelse(ohlcv[idx, :ema15] > ohlcv[idx, :ema40], Down, Up)
    this_rsi = ai.data[tf"15m"][ats, :rsi]
    if this_trend == Up && this_rsi < 40
        price = closeat(ohlcv, ats)
        amount = freecash(s) / price
        call!(s, ai, MarketOrder{Buy}; date, amount)
    elseif this_trend == Down && this_rsi > 60
        price = closeat(ohlcv, ats)
        if !isdust(ai, price)
            call!(s, ai, CancelOrders())
            call!(s, ai, MarketOrder{Sell}; date, amount=float(ai))
        end
    end
end

BollingerBands

Mean reversion with 20-period Bollinger Bands

Phemex
Isolated
1m
function handler(s, ai, ats, ts)
    call!(bbands!, s, ai, UpdateData(); cols=(:bb_lower, :bb_upper))
    ohlcv = ai.data[s.timeframe]
    lower = ohlcv[ats, :bb_lower]
    upper = ohlcv[ats, :bb_upper]
    current_price = closeat(ohlcv, ats)

    balance_quoted = s.self.freecash(s)   # free, not in pending orders
    buy_value = float(balance_quoted) * 0.80

    has_position = isopen(ai, Long())

    if current_price < lower && !has_position
        amount = buy_value / current_price
        call!(s, ai, MarketOrder{Buy}; date=ts, amount)
    elseif current_price > upper && has_position
        call!(s, ai, Long(), ts, PositionClose())
    end
end

MarginStrat

QQE momentum trading on isolated margin

Binance
Isolated
1d
function handler(s, ai, ats, date)
    call!(qqe!, s, ai, UpdateData(); cols=(:qqe,))

    data = ohlcv(ai, tf"1d")
    v = data[ats, :qqe]
    trend = if v > 13.22
        -1
    elseif v < 8.96
        1
    else
        0
    end

    pos = position(ai)
    exposure = pos === nothing ? 0.0 : cash(pos)
    @assert iszero(exposure) ||
        islong(pos) && exposure >= 0.0 ||
        isshort(pos) && exposure <= 0.0
    # ... open / close positions from the trend signal
end