trader

package module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 18, 2026 License: BSD-2-Clause Imports: 30 Imported by: 0

README

Trader

A Go FX backtesting and live paper-trading engine with OANDA integration, a REST/WebUI, and Claude MCP tools.


Install

git clone https://github.com/rustyeddy/trader
cd trader
make build          # → bin/trader
make install        # install to $GOPATH/bin

Requires Go 1.22+.


Quick Start

Backtest
# Run a pre-built config against cached historical data
trader backtest --config testdata/configs/eurusd-h1-2024-ema-cross.yml

# Run all regression configs and write reports
trader backtest regress --config testdata/configs/
Live Paper Trading
export OANDA_TOKEN=your-practice-api-token

# Dry-run: print resolved config and exit
trader live run --config testdata/configs/pulse-demo.yml --dry-run

# Single instrument against a practice account
trader live run --config testdata/configs/pulse-demo.yml

# Multi-instrument portfolio
trader live portfolio --config /path/to/portfolio.yml --dry-run
trader live portfolio --config /path/to/portfolio.yml
Daemon (REST API + UI)
trader serve --config deploy/trader.yaml.example   # REST on :9999, embedded UI, live journal
trader serve --addr :8080 --log-level debug

Open http://localhost:9999 for the dashboard.


CLI Commands

Command Description
trader analysis Parse a ChatGPT forex analysis CSV and print trade candidates and watchlist
trader backtest Run backtests against historical candles
trader backtest regress Batch regression: run all configs, write JSON + org reports
trader data sync Download ticks (Dukascopy) and build OHLC candles
trader data oanda Download candles directly from OANDA into the candle store
trader data candles Print local candles in canonical CSV format
trader data validate-candles Scan local candle months for missing expected bars and raw-source mismatches
trader data stats Print statistics for a historical candle dataset
trader data pip-value Show USD value of 1/10/100/1000 pips for each major pair
trader data position Convert between position size, USD notional value, and pip P&L
trader order account Print OANDA account balance, NAV, margin, and unrealized P/L
trader order update-stop Update stop-loss and/or take-profit on an open trade
trader live run Run a single-instrument live strategy against OANDA
trader live portfolio Run a multi-instrument live portfolio from a YAML config
trader order prices Fetch live bid/ask prices from OANDA for the major pairs
trader live journal Subscribe to OANDA transaction stream and journal closed trades
trader order Place, close, and list orders on a live OANDA account
trader serve Full daemon: REST API + live journal + embedded UI (port :9999)
trader api serve Minimal REST API only, no journal (port :8080)
trader replay Replay a dataset through the sim engine
trader mcp Expose trader as typed Claude tools over stdio (MCP protocol)

All commands accept --help.

Live journaling defaults to newline-delimited JSON files (*.jsonl) for trades and equity snapshots so the records stay easy to inspect now and easy to import into a database later.


Backtesting

Backtests are driven by YAML config files. See testdata/configs/ for a full library of examples.

# testdata/configs/eurusd-h1-2024-ema-cross.yml (excerpt)
defaults:
  capital: 10000
  risk_pct: 1.0
  data_dir: /srv/trading/data/candles

runs:
  - instrument: EUR_USD
    start_date: 2024-01-01
    end_date:   2024-12-31
    strategy:
      name: ema-cross
      fast: 9
      slow: 21

Results are printed to stdout and optionally written to reports/ as JSON + org-mode files.


Live Trading

Live trading uses OANDA's REST API. A practice account is free at oanda.com.

Authentication — set one of:

export OANDA_TOKEN=<your-token>        # env var (preferred)
echo <token> > ~/.config/oanda/pat.txt # file fallback
Single Instrument

Config (testdata/configs/pulse-demo.yml):

instrument: EUR_USD
env: practice           # practice | live
tick_interval: 60s      # how often to poll prices
max_positions: 1
risk_pct: 0.1           # % of account NAV to risk per trade
max_units: 5000         # hard unit cap
max_position_usd: 0     # hard notional cap in account currency (0 = none)

strategy:
  kind: pulse
  params:
    trade_every: 5      # open every N ticks
    hold_bars: 15       # close after N ticks
    side: long
    stop_pips: 20
    risk_pct: 0.1
trader live run --config testdata/configs/pulse-demo.yml
trader live run --config testdata/configs/pulse-demo.yml --env live --instrument GBP_USD
Multi-Instrument Portfolio

Run multiple strategies concurrently with a shared drawdown circuit breaker:

env: practice
account_id: 101-001-XXXXXXX-001   # auto-discovered if omitted
risk_pct: 1.0                     # default risk per trade (%)
drawdown_circuit_pct: 10.0        # halt new opens if equity drops this % from peak
local_warmup_bars: 5000           # bars to load from local store for indicator priming

instruments:
  - instrument: EUR_USD
    timeframe: H1
    tick_interval: 60s            # poll interval (optional, inherits global default)
    risk_pct: 0.5                 # overrides top-level default
    max_units: 10000

    strategy:
      kind: donchian-v6

    exit:
      kind: chandelier
      params:
        atr_period: 14
        multiplier: 3.0

    regime:
      kind: weekly-ema

  - instrument: GBP_USD
    timeframe: H1
    local_warmup_bars: 2000       # per-instrument override
    strategy:
      kind: ema-cross
    exit:
      kind: chandelier
      params: {atr_period: 14, multiplier: 3.0}
trader live portfolio --config portfolio.yml --dry-run
trader live portfolio --config portfolio.yml
Indicator Warmup

Before emitting live signals the adapter primes all indicators (strategy, regime filter, chandelier stop) using two phases:

  1. Local phase — reads local_warmup_bars bars from the on-disk OANDA candle store. 500 bars covers ~3 weeks of H1 data; 5000 covers ~7 months — sufficient for ATR-percentile and weekly-EMA regime filters.
  2. OANDA phase — fetches the most recent ~100 bars from OANDA to bridge any gap between the newest local bar and now.

Set local_warmup_bars: 0 to skip local warmup and use OANDA-only.

Signal Logging

All three event types — strategy signals, broker fills, and OANDA-initiated closes — flow through the same structured slog stream. With --log-level info (the default) every trading event is captured in one place.

Source Message Key fields
Strategy live: strategy signal open instrument, side, stop, reason
Strategy live: open blocked by regime filter instrument, side, reason (not trending / side not allowed)
Strategy live: open order queued instrument, side, entry_price, stop_price, stop_pips
Strategy live: strategy signal close instrument, count, reason
Strategy candle adapter: strategy returned open with no stop instrument, side, reason
Broker fill live runner: opened trade trade_id, side, units, price (OANDA confirmed fill)
Broker fill live runner: closed trade trade_id (strategy-triggered close)
Stop-out / TP live-journal trade recorded trade_id, instrument, entry, exit, pl, reason

The reason field on live-journal trade recorded contains the OANDA close reason:

  • STOP_LOSS_ORDER — stop-loss hit
  • TAKE_PROFIT_ORDER — take-profit hit
  • CLIENT_REQUEST — closed manually via the API

Configuration — add to trader.yaml or pass as flags:

log:
  level: info     # debug | info | warn | error
  format: json    # json enables structured filtering with jq
  file: /var/log/trader/trader.log   # written in addition to stdout
# Flags override the config file
trader serve --log-level info --log-format json --log-file /var/log/trader/trader.log

Filter the live log with jq (requires --log-format json):

# Tail all trading events — skip tick-level noise
tail -f /var/log/trader/trader.log | jq -c 'select(.msg | test("signal|queued|opened trade|closed trade|journal trade"))'

# Entries only — with stop price and pips
tail -f /var/log/trader/trader.log | jq -c 'select(.msg == "live: open order queued") | {time, instrument, side, entry_price, stop_price, stop_pips}'

# Fills only
tail -f /var/log/trader/trader.log | jq -c 'select(.msg == "live runner: opened trade") | {time, trade_id, side, units, price}'

# Stop-outs and closes with P/L
tail -f /var/log/trader/trader.log | jq -c 'select(.msg == "live-journal trade recorded") | {time, trade_id, instrument, entry, exit, pl, reason}'

# Everything in one clean stream
tail -f /var/log/trader/trader.log | \
  jq -c 'select(.msg | test("queued|opened trade|closed trade|journal trade")) |
         {time, msg: (.msg | split(":")[1] | ltrimstr(" ")), instrument, side,
          entry_price, stop_price, stop_pips, trade_id, price, pl, reason}'

Strategies

Strategies are referenced by their registered kind string in config files.

Kind Description Live?
pulse Mechanical open/close on fixed tick schedule — useful for pipeline testing live only
ema-cross EMA crossover (fast/slow periods configurable) backtest + live
ema-cross-adx EMA crossover filtered by ADX trend strength backtest + live
donchian Donchian channel breakout (v1) backtest + live
donchian-v2 Donchian v2 with improved exit logic backtest + live
donchian-v3 Donchian v3 backtest + live
donchian-v4 Donchian v4 backtest + live
donchian-v5 Donchian v5 backtest + live
donchian-v6 Donchian v6 — most recent, recommended backtest + live
bb-fade Bollinger Band fade (mean-reversion) backtest + live
noop Does nothing — baseline / benchmark backtest + live
fake Scripted actions for deterministic testing backtest only
lifecycle-test Exercises the full open → modify-stop → close lifecycle backtest only
template Starter template for new strategy development backtest only
Exit Strategies

Exit strategies control the trailing stop. Configured via the exit: block in portfolio YAML or used implicitly by the backtest engine.

Kind Description
chandelier ATR-based chandelier trailing stop. Params: atr_period (default 14), multiplier (default 3.0)
"" / noop No trailing stop — strategy sets its own fixed stop
Regime Filters

Regime filters suppress entries when the market is not in a favourable state.

Kind Description
"" / noop No filtering — all signals pass through
weekly-ema Allow longs only above weekly EMA, shorts only below
atr-percentile Block entries when ATR is below a percentile threshold (range-bound markets)
adx-d1 Block entries when daily ADX is below threshold (no trend)
choppiness Block entries when choppiness index signals sideways price action
choppiness-d1 Same as above using daily bars
session Allow entries only during specified trading sessions
composite Combine multiple filters (all must pass); use filters: list in config

Data Management

Historical data comes from two sources:

Dukascopy (tick data, free) — download and build candles:

trader data sync --instruments EUR_USD,GBP_USD --from 2022-01 --to 2024-12

OANDA (candles, requires token):

# Single instrument/timeframe
trader data oanda \
  --instrument EUR_USD \
  --timeframe  H1 \
  --from       2024-01-01 \
  --to         2024-12-31 \
  --env        practice

# Catch-up all instruments from last stored date through yesterday
trader data update

# First-time seed for a new timeframe (e.g. H4, which has no prior files)
trader data update --timeframes H4 --from 2005-01-03

Supported timeframes: M1, H1, H4, D. H4 is fetched natively from OANDA (not derived).

Candle data is stored under --data-dir (default /srv/trading/data/candles) in a hierarchy:

/srv/trading/data/candles/<source>/<INSTRUMENT>/<YYYY>/<MM>/

When OANDA candles are downloaded with raw preservation enabled, the bid+ask source rows are also written under the sibling raw tree:

/srv/trading/data/raw/oanda/<INSTRUMENT>/<YYYY>/<MM>/

testdata/candles/ contains small fixtures used by unit tests — do not use for real backtests.

Candle Completeness and Validation

Monthly candle files are no longer treated as complete just because the CSV exists and is non-empty. Inventory scanning reads the candle validity bits and marks a month incomplete if expected open-market slots are missing. Closed-market periods are allowed; missing bars during expected trading windows are not.

Use trader data validate-candles to scan stored months and optionally compare canonical OANDA candle coverage with preserved raw OANDA monthly files:

trader data validate-candles \
  --instruments EURUSD,USDJPY \
  --timeframe H1 \
  --from 2026-01 \
  --to 2026-03 \
  --source oanda \
  --check-raw \
  --report /tmp/candle-validation.json

What it reports:

Issue kind Meaning
missing_candle_month The canonical monthly candle CSV is missing entirely
missing_expected_candles Expected open-market bars are missing from the month
invalid_candles Present bars have invalid OHLC shape
missing_raw_source Raw OANDA monthly preservation file is missing
raw_complete_missing_canonical Raw OANDA has complete bars that are absent from canonical candles
canonical_missing_raw_complete Canonical candles contain valid bars not backed by raw OANDA complete rows

The command prints a summary to stdout and, with --report, writes a JSON report containing per-month counts, paths, and sample missing timestamps. This is the easiest way to keep an auditable record of gaps that should exist but do not.

Candle Backup

M1 data goes back to 2005 (~1 GB for 24 instruments) and cannot be fully re-downloaded from OANDA — their API retains M1 history for only a limited window. H1, H4, and D are small and re-downloadable, but are included for completeness.

Use rclone to back up incrementally to Google Drive:

One-time setup:

sudo apt install rclone
rclone config   # follow the prompts to authorise Google Drive

Run a backup (only new/changed files are transferred after the first run):

make backup-candles

This syncs /srv/trading/data/candles/oandagdrive:trader-candles/oanda with 8 parallel transfers.

Override the destination or source with make variables:

make backup-candles GDRIVE_DEST=myremote:my-bucket/candles
make backup-candles CANDLE_DIR=/path/to/other/store GDRIVE_DEST=myremote:backup

Restore:

rclone sync gdrive:trader-candles/oanda /srv/trading/data/candles/oanda --progress

After a restore, run trader data update to fill any gap between the last backup and today.

Candle CSV Export

Raw local candle reads go through Service.CandlesCSV, which streams candles from the canonical store and returns the same scaled integer CSV format used on disk. The service is shared by CLI, REST, and MCP so callers get consistent output:

# schema=v1 source=oanda instrument=EURUSD tf=h1 scale=100000
Timestamp,High,Open,Low,Close,avgspread,maxspread,ticks,flags
1704067200,110100,110000,109900,110050,10,15,60,0x0001

CLI:

trader data candles \
  --instrument EUR_USD \
  --timeframe  H1 \
  --from       2024-01-01 \
  --to         2024-01-31

--to is optional and defaults to now/latest available. Dates are inclusive at the caller boundary. Prices and spreads are emitted as fixed-point scaled integers, not floats.

Dataset Statistics

trader data stats walks a candle dataset and reports four groups of metrics:

Group What it measures
Swing High-low range per bar: count, mean, min, p25/p50/p75/p90, max (in pips)
Spread Average spread per bar: mean, p90, max (in pips; bars with zero spread are skipped)
Trend vs Consolidation Body/range ratio — |Close−Open| / (High−Low). >0.6 = trending, <0.3 = consolidating
Session Average range and bar count by UTC hour — shows which sessions are most active
# Pips only
trader data stats \
  --instrument EURUSD \
  --timeframe  H1 \
  --from       2020-01-01 \
  --to         2024-12-31

# Pips + USD value for a standard lot (100,000 units)
trader data stats --instrument EURUSD --from 2020-01-01 --to 2024-12-31 --units 100000

--units adds a USD column showing what each pip measurement is worth at the given position size. Position sizes: 1000 = micro lot, 10000 = mini lot, 100000 = standard lot. For USD-base pairs (USDJPY, USDCHF, USDCAD) approximate rates are used automatically.

Example output with --units 100000:

EURUSD H1   2020-01-01 → 2024-12-31   (USD at standard lot)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Swing (High-Low Range)
  count                      21890
  mean                       14.3 pips  ($143.00)
  min                         0.1 pips  ($1.00)
  p25                         8.1 pips  ($81.00)
  p50                        12.4 pips  ($124.00)
  p75                        18.9 pips  ($189.00)
  p90                        26.7 pips  ($267.00)
  max                       112.0 pips  ($1120.00)

Spread
  count (with spread)        21890
  mean                        0.18 pips  ($1.80)
  p90                         0.30 pips  ($3.00)
  max                         2.10 pips  ($21.00)

Trend vs Consolidation
  count                      21890
  mean body/range             0.421
  trending  (>0.6)           35.2%  (7705)
  mixed  (0.3–0.6)           34.8%  (7618)
  consolidating  (<0.3)      30.0%  (6567)

Session (by UTC hour)
  00:00 UTC                  count=1094    avg range=8.3 pips  ($83.00)
  01:00 UTC                  count=1089    avg range=7.9 pips  ($79.00)
  ...
  08:00 UTC                  count=1096    avg range=15.2 pips  ($152.00)
  09:00 UTC                  count=1098    avg range=18.4 pips  ($184.00)
  ...

--timeframe defaults to H1. All three timeframes (M1, H1, D1) are supported. --from and --to are both inclusive.

Pip Values

trader data pip-value prints the USD value of 1, 10, 100, and 1000 pips for every major pair at a given position size:

# Default: 100,000 units (1 standard lot), approximate rates for USD-base pairs
trader data pip-value

# Mini lot with live rates
trader data pip-value --units 10000 --rates USDJPY=152.50,USDCHF=0.88,USDCAD=1.38

Example output:

Pip values — 100,000 (standard lot) units  (USD per N pips)

Instrument       1 pip     10 pips    100 pips     1000 pips
──────────  ──────────  ──────────  ──────────  ────────────
EURUSD          $10.00    $100.00     $1,000    $10,000
GBPUSD          $10.00    $100.00     $1,000    $10,000
USDJPY    †    $6.6667     $66.67    $666.67     $6,667
USDCHF    †     $11.11    $111.11     $1,111    $11,111
AUDUSD          $10.00    $100.00     $1,000    $10,000
USDCAD    †    $7.3529     $73.53    $735.29     $7,353
NZDUSD          $10.00    $100.00     $1,000    $10,000

† approximate rate(s): USDJPY=150, USDCHF=0.9, USDCAD=1.36
  Override with --rates USDJPY=152.50,USDCHF=0.88,USDCAD=1.38

USD-quoted pairs (EURUSD, GBPUSD, AUDUSD, NZDUSD) are exact and need no rate. USD-base pairs (USDJPY, USDCHF, USDCAD) are marked and use approximate defaults until you supply --rates.


REST API

trader serve (port :9999) exposes the following endpoints. Most return JSON; the raw candle export returns text/csv.

Method Path Description
GET /api/v1/health Health check
GET /api/v1/account OANDA account summary (balance, NAV, margin, unrealized P/L)
GET /api/v1/prices Live bid/ask prices and spread in pips (?instruments=EURUSD,GBPUSD, default all majors)
GET /api/v1/trades Open trades
POST /api/v1/trades Place a risk-sized market order
PATCH /api/v1/trades/{id}/stop Update stop / take-profit on an open trade
DELETE /api/v1/trades/{id} Close a trade (full or partial)
GET /api/v1/transactions OANDA transaction history (?since_id=N)
GET /api/v1/candles/{instrument} Local candles as canonical CSV (from, to, timeframe, optional source)
GET /api/v1/candles/{instrument}/stats Candle dataset statistics — swing, spread, trend, session (from, to, timeframe, units)
GET /api/v1/candles/validate Validate local candle store for gaps and raw-source mismatches (instruments, from, to, timeframe)
GET /api/v1/pip-values USD pip values for major pairs (?units=100000, ?instruments=EURUSD,USDJPY)
GET /api/v1/position Position sizing table — notional, margin, pip P&L (?instrument=EURUSD&price=1.08&units=100000&pips=20)
POST /api/v1/backtests/run Run one or more backtest configs
GET /api/v1/backtests List saved backtest reports
GET /api/v1/backtests/{name} Get a single backtest report
GET /api/v1/backtests/{name}/candles OHLC bars for a saved report
POST /api/v1/replay Run a strategy replay; returns bars + signal log
POST /api/v1/analysis Parse a ChatGPT forex analysis CSV upload; returns rows split by status
GET /api/v1/stream/account SSE: account equity stream
GET /api/v1/stream/events SSE: broker event stream
GET /api/v1/stream/backtest/{id} SSE: live backtest progress

OANDA endpoints return 503 when the server starts without a token (backtest-only mode).

Example candle CSV request:

curl -s 'http://localhost:9999/api/v1/candles/EUR_USD?from=2024-01-01&to=2024-01-31&timeframe=H1'

MCP Tools

trader mcp serve exposes typed tools over stdio. Tools that read local data or perform pure calculations work without an OANDA token. Live account and trade tools require --token. Write tools (download_candles, place_order, close_trade, update_stop) also require --enable-write.

Tool Needs OANDA Write? Description
get_account_summary yes Account balance, NAV, margin, unrealized P/L
get_prices yes Live bid/ask and spread in pips for major pairs
list_open_trades yes All open positions
get_transactions yes Transaction history since a given ID
get_candles_csv no Local candles in canonical CSV
get_candle_stats no Swing, spread, trend, session statistics for a candle dataset
validate_candles no Scan stored months for gaps and raw-source mismatches
get_pip_values optional USD pip values for major pairs (live rates when OANDA available)
get_position optional Position sizing — notional, margin, pip P&L (live price when OANDA available)
run_backtest no Run backtest configs and return summaries
download_candles yes yes Download and store OANDA candles
place_order yes yes Size and submit a risk-based market order
close_trade yes yes Close an open trade fully or partially
update_stop yes yes Update stop-loss and/or take-profit on an open trade

Local config example:

{
  "mcpServers": {
    "trader": {
      "type": "stdio",
      "command": "trader",
      "args": ["mcp", "serve"]
    }
  }
}

Strategy Replay

The replay API runs any strategy against stored local candles and returns every bar plus a full signal log — without placing any orders. Use it to debug signal generation, visualise where entries and stops were placed, and tune parameters interactively.

REST API
curl -s -X POST http://localhost:9999/api/v1/replay \
  -H 'Content-Type: application/json' \
  -d '{
    "instrument":   "EURUSD",
    "timeframe":    "H1",
    "from":         "2026-01-01",
    "to":           "2026-05-29",
    "warmup_bars":  200,
    "strategy":     {"kind": "donchian-v6"},
    "exit":         {"kind": "chandelier", "params": {"atr_period": 14, "multiplier": 3.0}},
    "regime":       {"kind": "weekly-ema"}
  }'

Response includes bars[] (OHLC) and signals[]. Signal kinds:

Kind Meaning
open Strategy signalled an entry; includes stop_price and stop_pips
close Strategy signalled an exit
stop_update Chandelier trailing stop ratcheted to a new level
blocked Regime filter suppressed an open signal
no_stop Open skipped — strategy produced no stop and exit strategy not ready

Save the response and slice it with jq to analyse signals offline:

# Save replay output to file
curl -s -X POST http://localhost:9999/api/v1/replay \
  -H 'Content-Type: application/json' \
  -d '{
    "instrument": "EURUSD", "timeframe": "H1",
    "from": "2026-01-01", "to": "2026-05-29",
    "warmup_bars": 200,
    "strategy": {"kind": "donchian-v6"},
    "exit":     {"kind": "chandelier", "params": {"atr_period": 14, "multiplier": 3.0}},
    "regime":   {"kind": "weekly-ema"}
  }' > replay.json

# Signal summary
jq '.signals | group_by(.kind) | map({(.[0].kind): length}) | add' replay.json

# All entries with human-readable time and stop distance
jq '[.signals[] | select(.kind == "open")] |
    map({time: (.time | todate), side, price, stop_price, stop_pips, reason})' replay.json

# All exits
jq '[.signals[] | select(.kind == "close")] |
    map({time: (.time | todate), side, price, reason})' replay.json

# Blocked signals (regime filter)
jq '[.signals[] | select(.kind == "blocked")] |
    map({time: (.time | todate), side, reason})' replay.json

# Chronological timeline — skip stop_update noise
jq '[.signals[] | select(.kind != "stop_update")] |
    map({time: (.time | todate), kind, side, price, stop_pips, reason})' replay.json
Web UI

Open http://localhost:9999/replay. Controls: instrument, timeframe, date range, strategy, exit strategy (ATR period + multiplier), regime filter, warmup bars. Click Run Replay to render:

  • Green ▲ / Red ▼ entry markers with stop-pips label
  • Gray ● exit markers
  • Yellow ■ regime-blocked signals
  • Orange ■ no-stop-dropped signals
  • Dashed orange line — chandelier stop trail from entry to exit

The signal summary bar below the controls shows counts for each kind. The chart re-renders immediately when you change parameters and click Run again — useful for tuning the ATR multiplier or switching regime filters interactively.


Analysis

The analysis feature reads a ChatGPT-generated forex analysis CSV and classifies each pair as a trade candidate, watchlist item, or no-trade.

CSV Format

The CSV must have these nine columns (row 1 is a header):

Column Example
Group Major Pairs
Pair EUR/USD
Structure Near 1.1590; USD softer after risk-on headline…
Setup Bias Breakout continuation only after clean 4H close…
Trend Bullish EUR / bearish USD
Volatility Medium-High
Support zone 1.1570–1.1590
Resistance Zone 1.1600–1.1625
Status Tradeable watch list | Watchlist | No Trade

Support and resistance zones are price ranges separated by an en dash (), em dash (), or hyphen.

CLI
# Print watchlist and trade candidates (No Trade rows hidden by default)
trader analysis --file forex_analysis_2026-06-15.csv

# Include No Trade rows
trader analysis --file forex_analysis_2026-06-15.csv --all

Example output:

PAIR     STATUS                TREND                      VOLATILITY   SUPPORT          RESISTANCE
----     ------                -----                      ----------   -------          ----------
EUR/USD  Tradeable watch list  Bullish EUR / bearish USD  Medium-High  1.1570–1.1590    1.1600–1.1625
GBP/USD  Watchlist             Mild bullish GBP           High         1.3400–1.3410    1.3420–1.3460
AUD/USD  Tradeable watch list  Mild bullish AUD           High         0.7050–0.7070    0.7080–0.7100
EUR/CAD  Tradeable watch list  Mild bullish EUR           Medium-High  1.6150–1.6210    1.6260–1.6320
REST API

POST /api/v1/analysis accepts a multipart form upload with field name file and returns the parsed rows pre-partitioned into three slices.

curl -s -X POST http://localhost:9999/api/v1/analysis \
  -F "file=@forex_analysis_2026-06-15.csv"

Response:

{
  "total": 19,
  "tradeable": [
    {
      "group": "Major Pairs",
      "pair": "EUR/USD",
      "structure": "Near 1.1590; USD softer after risk-on headline…",
      "setup_bias": "Breakout continuation only after clean 4H close…",
      "trend": "Bullish EUR / bearish USD",
      "volatility": "Medium-High",
      "support_low": 1.157,
      "support_high": 1.159,
      "resistance_low": 1.16,
      "resistance_high": 1.1625,
      "status": "Tradeable watch list"
    }
  ],
  "watchlist": [ ... ],
  "no_trade":  [ ... ]
}

Deployment

Docker
cp deploy/env.example .env
# edit .env: OANDA_TOKEN, OANDA_ACCOUNT_ID, INSTRUMENT, STRATEGY

# Start the live bot + Postgres services
docker compose up -d live postgres

# Run a one-off backtest
docker compose run --rm backtest

# Download candles
docker compose run --rm data

# Raspberry Pi (adds memory caps + NFS candle volume)
docker compose -f docker-compose.yml -f deploy/docker-compose.pi.yml up -d live
Systemd

A ready-to-use unit file is at deploy/trader.service. It runs trader serve with the config at /etc/trader/trader.yaml. Copy the example config:

sudo cp deploy/trader.yaml.example /etc/trader/trader.yaml
sudo cp deploy/trader.service /etc/systemd/system/
sudo systemctl enable --now trader

Architecture

The core backtest loop:

Config (YAML)
  → DataManager  (loads / caches OHLC candles)
  → Backtest     (iterates candles bar by bar)
  → Strategy     (returns StrategyPlan each bar)
  → ExitStrategy (computes / updates trailing stop)
  → RegimeFilter (suppresses entries in ranging markets)
  → Broker       (fills orders, emits Events)
  → Account      (updates equity, margin, P/L)
  → Journal      (records closed trades — CSV or JSON)

Numeric types — all prices and money are fixed-point integers, never floats:

Type Scale Notes
Price (int32) 100,000 1.16177 → 116177
Money (int64) 1,000,000 avoids float rounding
Units 1 position size in micro-lots

Accounting invariants (must hold after every operation):

  • Equity = Balance + UnrealizedPL
  • FreeMargin = Equity − MarginUsed
  • BUY: open at ask, close at bid; SELL: open at bid, close at ask
  • Stop/take-profit evaluated on every bar (inclusive)
  • Forced liquidation when FreeMargin < 0

Testing

make test           # unit tests
make test-blackbox  # unit + REST API + MCP integration tests
make cover          # coverage report (stdout)
make cover-html     # coverage report (browser)

# Run a single test
go test -run TestName ./...

# Enable Dukascopy download tests (hits network)
TRADER_RUN_DUKASCOPY_TESTS=1 go test ./...

Every code change must ship with tests — see docs/CLAUDE.md for conventions.

Live Integration Smoke Test

make smoke-live runs the pulse strategy against an OANDA practice account to exercise the full broker plumbing at high frequency. Requires an active market session (London/NY overlap: 13:00–17:00 UTC recommended) and OANDA_TOKEN set in the environment.

export OANDA_TOKEN=your-practice-token

make smoke-live-dry   # parse and resolve config only — no orders placed
make smoke-live       # full run; logs to logs/smoke-live.log

# Tail trading events while running
tail -f logs/smoke-live.log | jq -c 'select(.msg | test("signal|opened trade|closed trade|journal trade"))'

Config: testdata/configs/smoke-test.yml — EUR_USD M1 pulse, trades every ~90s, 15-pip stops, session-gated to 13:00–17:00 UTC. Uncomment the GBP_USD block to test multi-instrument concurrency (phase 2).

Target Needs OANDA? What it does
make smoke No Offline CI: build, backtest, replay API
make smoke-live-dry Token only Resolve config, print plan, exit
make smoke-live Token + open session Full pulse run, JSON log

Project Layout

cmd/            CLI entry points (Cobra)
cmd/analysis/   ChatGPT forex analysis CSV parser and classifier
api/rest/       REST handlers and routing
api/mcp/        Claude MCP tool server
brokers/oanda/  OANDA REST + streaming client
service/        Business logic (orders, candle CSV export, live runner, replay, journal)
strategies/     Strategy implementations
data/           Candle loading, Dukascopy parser
ui/             Embedded SvelteKit frontend (build → ui/dist/)
deploy/         Dockerfile, docker-compose, systemd unit, example configs
testdata/       Config fixtures and candle fixtures
lots-of.go	    Trader core source code
docs/           Project notes, roadmap, service docs, and plans

Roadmap

See docs/ROADMAP.md for planned features including walk-forward testing, external/plugin strategies, and more.

Documentation

Overview

Package trader provides structured logging for the trader application using Go's standard log/slog library. It supports multiple concurrent output destinations (stdout, a log file, and syslog) and named module loggers so that log records can be filtered by subsystem (data, backtest, indicator, replay, …).

Typical usage:

// initialise once at startup (e.g. from main or cmd layer)
Setup(LogConfig{Level: "debug", Format: "text", File: "trader.log"})

// package-level helpers
Info("server started", "port", 8080)
Debug("tick received", "instrument", "EURUSD")

// module-scoped logger
logger := Module("data")
logger.Info("inventory built", "files", 42)

// or use the pre-wired module variables
Data.Info("download complete", "key", key)
Backtest.Warn("end of data reached")

Index

Constants

View Source
const (
	SourceDukascopy = "dukascopy"
	SourceOanda     = "oanda"
	SourceCandles   = "candles"
)
View Source
const (
	LotNone lotState = iota
	LotOpenRequested
	LotOpen
	LotCloseRequested
	LotClosed
)
View Source
const (
	PriceScale Scale6 = 100_000
	MoneyScale Scale7 = 1_000_000
	RateScale  Scale7 = MoneyScale
)
View Source
const (
	CloseUnknown closeCause = iota
	CloseManual
	CloseStopLoss
	CloseTakeProfit
	CloseBrokerLiquidation
)
View Source
const (
	SecondInMS  timemilli = 1_000
	MinuteInSec Timestamp = 60
	MinuteInMS  timemilli = 60_000
	HourInSec   Timestamp = 3_600
	HourInMS    timemilli = 3_600_000
)
View Source
const ErrStatMissingInstrument = "missing instrument"

ErrStatMissingInstrument is the Value field in the error Stat returned when an analyzer has no instrument configured. Use this constant instead of a bare string literal when checking whether a Stat signals a missing instrument.

View Source
const TestDataDir = "testdata"

TestDataDir is the testdata directory path relative to workspace root.

View Source
const UnitsScale int64 = 1_000_000

UnitsScale is the fixed-point scale for Units values that represent fractional multipliers (e.g. 2.5 → Units(2_500_000)).

Variables

View Source
var (

	// Pre-wired module loggers.  They are initialised to the default logger
	// in init() and remain valid across Setup calls.
	L            *slog.Logger
	Data         *slog.Logger
	BacktestLog  *slog.Logger
	IndicatorLog *slog.Logger
	Strat        *slog.Logger
	Replay       *slog.Logger
)
View Source
var DefaultStrategyPlan = StrategyPlan{
	Reason: "hold",
}
View Source
var ErrKeyNotFound = errors.New("key not found")
View Source
var ErrTickNotFound = errors.New("tick not found")
View Source
var Version = "dev"

Version is the current build version. Set at build time via:

go build -ldflags="-X github.com/rustyeddy/trader.Version=v1.2.3"

Functions

func ApproximateUSDPerUnit added in v0.2.3

func ApproximateUSDPerUnit(currency string) (float64, bool)

ApproximateUSDPerUnit reports a rough USD conversion for a non-USD currency.

func AvgSpreadPips added in v0.2.3

func AvgSpreadPips(spreadSum Price, spreadOpened int, inst *Instrument) float64

AvgSpreadPips converts an accumulated Price spread into average pips.

func ClearEntries

func ClearEntries()

ClearEntries discards all entries held in the in-memory stack.

func Debug

func Debug(msg string, args ...any)

Debug logs at LevelDebug.

func Error

func Error(msg string, args ...any)

Error logs at LevelError.

func Fatal

func Fatal(msg string, args ...any)

Fatal logs at LevelError and terminates the process with os.Exit(1).

func FormatTradeOrg

func FormatTradeOrg(t TradeRecord) string

FormatTradeOrg renders a TradeRecord as an Org-mode block suitable for pasting into a journal. It purposely includes narrative placeholders (Thesis/Execution/Review) while keeping all structured facts in a PROPERTIES drawer for easy search.

func FormatTradesOrg

func FormatTradesOrg(trades []TradeRecord) string

FormatTradesOrg renders multiple trades separated by blank lines.

func GenerateSyntheticYearTestData

func GenerateSyntheticYearTestData(basedir string, instrument string, year int, timeframe Timeframe) ([]string, error)

GenerateSyntheticYearTestData generates a full year of synthetic test data.

func GetBoolParam

func GetBoolParam(m map[string]any, key string) (bool, bool, error)

GetBoolParam extracts a bool param, or returns ok=false if missing.

func GetFloat64Param

func GetFloat64Param(m map[string]any, key string) (float64, bool, error)

GetFloat64Param extracts a float64 from a params map, widening integer types as needed. Returns (0, false, nil) when the key is absent, or an error if the value is not numeric.

func GetInt32Param

func GetInt32Param(m map[string]any, key string) (int32, bool, error)

GetInt32Param extracts an int32 from a params map, accepting the numeric types produced by YAML/JSON decoding. Returns (0, false, nil) when the key is absent, or an error if the value is not numeric.

func GetIntParam added in v0.2.3

func GetIntParam(m map[string]any, key string) (int, bool, error)

GetIntParam extracts an int from a params map, accepting the numeric types produced by YAML/JSON decoding. Returns (0, false, nil) when the key is absent, or an error if the value is not numeric.

func GetStringParam added in v0.2.1

func GetStringParam(m map[string]any, key string) (string, bool, error)

GetStringParam extracts a string param, or returns ok=false if missing.

func Info

func Info(msg string, args ...any)

Info logs at LevelInfo.

func InstrumentPositions

func InstrumentPositions(lb *LotBook) map[string]Position

InstrumentPositions derives per-instrument Position from all open lots.

func IsForexMarketClosed

func IsForexMarketClosed(t time.Time) bool

IsForexMarketClosed is the exported form of isForexMarketClosed for use by sibling packages (e.g. data/dukascopy).

func JournalRecordPaths added in v0.2.3

func JournalRecordPaths(base string) (tradesPath, equityPath string)

func MajorInstruments added in v0.2.3

func MajorInstruments() []string

MajorInstruments returns the ordered list of seven major FX pairs tracked by this engine.

func Module

func Module(name string) *slog.Logger

Module returns a *slog.Logger pre-populated with the attribute "module"=name. The same logger is returned on subsequent calls with the same name.

func MustRegisterLiveStrategy added in v0.2.3

func MustRegisterLiveStrategy(ctor LiveStrategyConstructor, names ...string)

MustRegisterLiveStrategy registers a LiveStrategy and panics on error.

func MustRegisterStrategy added in v0.2.3

func MustRegisterStrategy(ctor StrategyConstructor, names ...string)

MustRegisterStrategy registers a strategy and panics on error. Intended for use in package init() registration paths so invalid registrations fail fast at startup.

func NewCSV

func NewCSV(tradesPath, equityPath string) (*csvJournal, error)

func NewDownloader

func NewDownloader() *downloader

func NewJSON added in v0.2.3

func NewJSON(tradesPath, equityPath string) (*jsonJournal, error)

func NewULID

func NewULID() string

New returns a ULID string (time-sortable identifier).

ULIDs are lexicographically sortable by generation time, which makes them ideal for journaling/trading records and database indexes.

func NormalizeInstrument

func NormalizeInstrument(sym string) string

NormalizeInstrument is an internal helper for trader type processing.

func PrintSummary

func PrintSummary(w io.Writer, s BacktestReportSummary)

PrintSummary writes a human-readable backtest report to w.

func RegisterLiveStrategy added in v0.2.3

func RegisterLiveStrategy(ctor LiveStrategyConstructor, names ...string) error

RegisterLiveStrategy registers a LiveStrategy constructor under one or more names. Typically called from a package's init() function.

func RegisterStrategy

func RegisterStrategy(ctor StrategyConstructor, names ...string) error

RegisterStrategy adds a strategy constructor under one or more names. Typically called from an implementation package's init() function. Multiple aliases are supported (e.g. "donchian", "donchian-breakout").

func RegisteredLiveStrategies added in v0.2.3

func RegisteredLiveStrategies() []string

RegisteredLiveStrategies returns the sorted list of registered live strategy names.

func RegisteredStrategies

func RegisteredStrategies() []string

RegisteredStrategies returns the sorted list of registered strategy names. Useful for help text and validation.

func RunAnalysis added in v0.2.1

func RunAnalysis(ctx context.Context, itr CandleIterator, analyzers []Analyzer) (err error)

RunAnalysis walks itr, feeding every candle to each Analyzer. It closes itr before returning.

func SetDataDir

func SetDataDir(dir string)

SetDataDir overrides the global store's base directory. Call from main before any data operations.

func Setup

func Setup(cfg LogConfig) error

Setup initialises (or re-initialises) the logging system according to cfg. It is safe to call multiple times; subsequent calls replace the active handler and close previously opened sinks.

func ShortDisplayID added in v0.2.3

func ShortDisplayID(full string) string

ShortDisplayID returns a short, human-friendly prefix for headings and logs.

func SwapStore

func SwapStore(s *Store) (restore func())

SwapStore replaces the global Store with the given one and returns a function that restores the previous Store. Useful in tests for sibling packages that need to point the global at a temp directory.

func Warn

func Warn(msg string, args ...any)

Warn logs at LevelWarn.

func WriteOrgIndex

func WriteOrgIndex(w io.Writer, summaries []BacktestReportSummary)

WriteOrgIndex writes a single comparison table across all summaries to w.

func WriteOrgReport

func WriteOrgReport(w io.Writer, s BacktestReportSummary)

WriteOrgReport writes a full per-run org-mode report to w.

Types

type ADX

type ADX struct {
	// contains filtered or unexported fields
}

ADX computes the Average Directional Index (Wilder) over candle OHLC.

Readiness / warmup: - ADX needs:

  1. N periods to build initial smoothed TR/+DM/-DM
  2. N DX values to seed the initial ADX (average of first N DX)

- Practically, that's about 2N "periods" (differences between candles), plus the first candle. - We expose Warmup() as 2N to keep it simple/consistent with your other indicators.

func NewADX

func NewADX(period int, scale Scale6) (*ADX, error)

func (*ADX) DX

func (a *ADX) DX() float64

func (*ADX) Float64

func (a *ADX) Float64() float64

func (*ADX) MinusDI

func (a *ADX) MinusDI() float64

func (*ADX) Name

func (a *ADX) Name() string

func (*ADX) Period added in v0.2.3

func (a *ADX) Period() int

func (*ADX) PlusDI

func (a *ADX) PlusDI() float64

Optional: expose DI values if you want them in strategies/debugging.

func (*ADX) Ready

func (a *ADX) Ready() bool

func (*ADX) Reset

func (a *ADX) Reset()

func (*ADX) Update

func (a *ADX) Update(c Candle)

Update consumes the next closed candle.

func (*ADX) Warmup

func (a *ADX) Warmup() int

type ATR

type ATR struct {
	// contains filtered or unexported fields
}

ATR computes the Average True Range (Wilder) over candle OHLC.

Warmup: needs N candle-to-candle periods (N+1 candles) before Ready() is true. ATR keeps fixed-point price units internally; Float64() is for display.

func NewATR

func NewATR(period int, scale Scale6) (*ATR, error)

func (*ATR) Float64

func (a *ATR) Float64() float64

func (*ATR) Name

func (a *ATR) Name() string

func (*ATR) Period

func (a *ATR) Period() int

func (*ATR) Price added in v0.2.3

func (a *ATR) Price() Price

func (*ATR) PriceSum added in v0.2.3

func (a *ATR) PriceSum() PriceSum

func (*ATR) Ready

func (a *ATR) Ready() bool

func (*ATR) Reset

func (a *ATR) Reset()

func (*ATR) Update

func (a *ATR) Update(c Candle)

func (*ATR) Warmup

func (a *ATR) Warmup() int

type ATRPercentileFilter added in v0.2.1

type ATRPercentileFilter struct {
	// contains filtered or unexported fields
}

ATRPercentileFilter gates entries based on the percentile rank of the current ATR(atrPeriod) within a rolling window of windowSize ATR readings.

Trending() returns false when the current ATR percentile is below threshold, indicating a low-volatility ranging regime where breakout entries tend to fail. AllowSide() always returns true — this is a regime gate, not directional. Ready() becomes true as soon as ATR has warmed up and the first ATR reading has been recorded into the rolling window; the window does not need to be completely full before the filter starts classifying bars.

Default params: atrPeriod=20, windowSize=200, threshold=20.0. Registered in the factory as "atr-percentile".

func NewATRPercentileFilter added in v0.2.1

func NewATRPercentileFilter(atrPeriod, windowSize int, threshold float64, scale Scale6) (*ATRPercentileFilter, error)

func (*ATRPercentileFilter) AllowSide added in v0.2.1

func (f *ATRPercentileFilter) AllowSide(_ Side) bool

func (*ATRPercentileFilter) Name added in v0.2.1

func (f *ATRPercentileFilter) Name() string

func (*ATRPercentileFilter) Percentile added in v0.2.1

func (f *ATRPercentileFilter) Percentile() float64

Percentile exposes the current ATR percentile rank for debugging. Equal ATR values share the middle of their tie bucket, so a completely flat ATR window reports the 50th percentile instead of collapsing to 0.

func (*ATRPercentileFilter) Ready added in v0.2.1

func (f *ATRPercentileFilter) Ready() bool

func (*ATRPercentileFilter) Tick added in v0.2.1

func (f *ATRPercentileFilter) Tick(ct CandleTime)

func (*ATRPercentileFilter) Trending added in v0.2.1

func (f *ATRPercentileFilter) Trending() bool

type Account

type Account struct {
	ID           string
	Name         string
	Currency     string // account denomination (e.g. "USD")
	Balance      Money  // realised cash; updated on every close
	Equity       Money  // Balance + sum of unrealised P/L across open lots
	MarginUsed   Money  // sum of margin reserved by open lots
	FreeMargin   Money  // Equity − MarginUsed
	MarginLevel  Money  // Equity / MarginUsed × MoneyScale (0 when flat)
	RiskFraction Rate   // fraction of equity risked per trade (e.g. 0.005 = 0.5 %)

	Lots   LotBook
	Trades []*Trade // closed trades, appended by CloseLot
}

Account holds the financial state for a single trading account. All monetary values are scaled integers (Money = int64 × MoneyScale). Invariants that must hold after every operation:

  • Equity = Balance + UnrealizedPL
  • FreeMargin = Equity − MarginUsed

func NewAccount

func NewAccount(name string, deposit Money) *Account

NewAccount creates an Account with the given name and opening deposit. Currency defaults to "USD"; RiskFraction defaults to 0.5 %.

func (*Account) AddLot

func (acct *Account) AddLot(lot *Lot) error

AddLot registers a newly opened lot with the account and immediately revalues all open positions at the lot's entry price.

func (*Account) CloseLot

func (acct *Account) CloseLot(lot *Lot, trade *Trade) error

CloseLot realizes P/L for the lot, appends the trade to the account's Trades history, removes the lot from the LotBook, and revalues remaining open lots at the exit price.

func (*Account) ResolveWithMarks

func (acct *Account) ResolveWithMarks(marks map[string]Price) error

ResolveWithMarks recomputes all account-level derived fields (Equity, MarginUsed, FreeMargin, MarginLevel) using the provided mark prices. If a lot's instrument has no entry in marks, the lot's EntryPrice is used. Pass nil to revalue everything at entry.

func (*Account) SizePosition

func (acct *Account) SizePosition(req *OpenRequest) error

SizePosition computes and sets req.Units as the lesser of:

  • the units allowed by the risk budget (unitsByRisk)
  • the units allowed by available margin (unitsByMargin)

Returns an error if the computed size is below the instrument's minimum trade size or if any input is invalid.

type AnalysisStatus added in v0.2.3

type AnalysisStatus string

AnalysisStatus is the action classification from a ChatGPT forex analysis row.

const (
	StatusNoTrade   AnalysisStatus = "No Trade"
	StatusWatchlist AnalysisStatus = "Watchlist"
	StatusTradeable AnalysisStatus = "Tradeable watch list"
)

type Analyzer added in v0.2.1

type Analyzer interface {
	Name() string
	Update(*CandleTime)
	Stats() []Stat
}

Analyzer accumulates statistics over a candle sequence.

type Asset

type Asset struct {
	Key        Key
	Path       string
	Range      TimeRange
	Exists     bool
	Complete   bool
	Buildable  bool
	Size       int64
	UpdatedAt  time.Time
	SourceAge  time.Time // optional: mtime of prerequisite/source
	Descriptor string
	Flags      AssetFlags

	MissingInputs int
	Reason        string
}

type AssetFlags

type AssetFlags uint32
const (
	FlagUsable AssetFlags = 1 << iota
	FlagKnownClosed
	FlagDoNotDownload
	FlagDownloadFailed
	FlagManualSkip
)

type BA

type BA struct {
	Bid Price
	Ask Price
}

BA represents a trader domain type.

func (BA) Mid added in v0.2.3

func (ba BA) Mid() Price

Mid returns the midpoint rounded half-up to the nearest scaled price unit.

func (BA) Spread added in v0.2.3

func (ba BA) Spread() Price

func (BA) Validate added in v0.2.3

func (ba BA) Validate() error

Validate is an internal helper for trader type processing.

type Backtest

type Backtest struct {
	ID        string
	RunConfig RunConfig // resolved config snapshot used for execution

	Request *BacktestRequest
	State   *BacktestRun
	Result  *BacktestResult
}

Backtest is the executable form of one backtest run. It keeps the immutable request and mutable run-state together so strategies can inspect open lots during execution, while the final result is stored in the explicit Result field rather than anonymously merged into the run.

func (*Backtest) BuildBacktestResult

func (run *Backtest) BuildBacktestResult(acct *Account) *BacktestResult

BuildBacktestResult snapshots the account state into a BacktestResult and stores it on the run's explicit Result field. It computes trade counts, returns, gross P/L, averages, risk/reward, and closed-trade drawdown from the account's closed trades. Returns nil if run or acct is nil.

func (*Backtest) Summary

func (run *Backtest) Summary() BacktestReportSummary

Summary builds a fully-populated BacktestReportSummary from the run's request and result fields. It is safe to call after BuildBacktestResult. Returns a zero-value summary if any required field is nil.

type BacktestExecutor added in v0.2.3

type BacktestExecutor interface {
	Execute(context.Context, *Backtest) error
}

BacktestExecutor runs an executable backtest using whatever runtime dependencies it needs. Service-layer code depends on this narrow contract instead of constructing Trader/Broker/Account directly.

type BacktestReportSummary

type BacktestReportSummary struct {
	Name       string `json:"name"`
	Strategy   string `json:"strategy"`
	Instrument string `json:"instrument"`
	Timeframe  string `json:"timeframe"`
	Dataset    string `json:"dataset"`
	Start      string `json:"start"`
	End        string `json:"end"`

	Trades int `json:"trades"`
	Wins   int `json:"wins"`
	Losses int `json:"losses"`

	StartBalance float64 `json:"start_balance"`
	EndBalance   float64 `json:"end_balance"`
	NetPL        float64 `json:"net_pl"`

	// Stored as human-friendly percentages, e.g. 12.34 means 12.34%
	ReturnPct float64 `json:"return_pct"`
	WinRate   float64 `json:"win_rate"`
	RiskPct   float64 `json:"risk_pct"`

	Stop      string `json:"stop"`
	Regime    string `json:"regime"`
	MaxSpread string `json:"max_spread,omitempty"`
	Slippage  string `json:"slippage,omitempty"`

	// Execution cost stats
	AvgSpreadPips  float64 `json:"avg_spread_pips"`
	SpreadFiltered int     `json:"spread_filtered"`
	RR             float64 `json:"rr"`
	MaxDrawdown    float64 `json:"max_drawdown"` // largest peak-to-trough drop in dollars (negative)
	AvgWinner      float64 `json:"avg_winner"`
	AvgLoser       float64 `json:"avg_loser"` // negative

	TradeDetails []BacktestReportTrade `json:"trade_details,omitempty"`

	// Provenance links generated reports back to their origin. Older fixtures
	// and manually constructed summaries may leave these fields empty.
	ConfigHash  string    `json:"config_hash"`  // 8-char SHA256 prefix of the run config params
	GeneratedAt string    `json:"generated_at"` // RFC3339 UTC timestamp of when the run completed
	Config      RunConfig `json:"config"`       // full config snapshot that produced this result
}

BacktestReportSummary is a normalized machine-readable summary used for committed regression baselines and generated comparison artifacts. The Config and ConfigHash fields make every report self-describing: you can open any JSON file and see exactly what params produced it.

type BacktestReportTrade

type BacktestReportTrade struct {
	ID              string  `json:"id"`
	Instrument      string  `json:"instrument"`
	Side            string  `json:"side"`
	Units           int64   `json:"units"`
	OpenPrice       float64 `json:"open_price"`
	ClosePrice      float64 `json:"close_price"`
	OpenTime        string  `json:"open_time"`
	CloseTime       string  `json:"close_time"`
	PNL             float64 `json:"pnl"`
	StopPrice       float64 `json:"stop_price,omitempty"`
	TakeProfitPrice float64 `json:"take_profit_price,omitempty"`
}

BacktestReportTrade is a JSON-serialisable record of a single closed trade used inside BacktestReportSummary.TradeDetails.

type BacktestRequest

type BacktestRequest struct {
	Name       string
	ConfigHash string // 8-char SHA256 prefix of execution-affecting config inputs

	StartingBalance Money
	RiskPct         Rate // fraction of equity risked per trade (e.g. 0.005 = 0.5 %)

	DefaultStopPips Pips // fallback stop distance when the strategy doesn't supply one
	DefaultTakePips Pips // fallback take-profit distance
	SlippagePips    Pips // extra adverse fill adjustment applied on every open/close
	MaxSpreadPips   Pips // opens are skipped when the candle spread exceeds this

	Source     string // data source identifier (e.g. "candles", "dukascopy")
	Instrument string // FX pair (e.g. "EUR_USD")
	Strategy   Strategy
	Exit       ExitStrategy
	Regime     RegimeFilter
	TimeRange  TimeRange
}

BacktestRequest holds all the static inputs needed to execute one backtest run. It is populated from Config/RunConfig before the run loop starts and is not modified during execution.

type BacktestResult

type BacktestResult struct {
	Start        Timestamp
	End          Timestamp
	StartBalance Money // starting account balance
	Balance      Money // final account balance, realised only
	Equity       Money // final equity including any open positions at run end

	Trades int // total non-nil closed trades
	Wins   int // trades with PNL > 0
	Losses int // trades with PNL < 0
	Flat   int // trades with PNL == 0

	// Derived fields populated by BuildBacktestResult.
	NetPL          Money // Balance - StartBalance
	ReturnPct      Rate  // NetPL / StartBalance, RateScale-scaled
	GrossProfit    Money // sum of winning trade PNL
	GrossLoss      Money // sum of losing trade PNL, negative
	WinRate        Rate  // Wins / Trades, RateScale-scaled
	ProfitFactor   Rate  // GrossProfit / abs(GrossLoss), RateScale-scaled
	AvgWinner      Money // average winning trade PNL
	AvgLoser       Money // average losing trade PNL, negative
	RR             Rate  // AvgWinner / abs(AvgLoser), RateScale-scaled
	MaxDrawdown    Money // largest peak-to-trough drop in cumulative PNL, negative
	MaxDrawdownPct Rate  // MaxDrawdown / StartBalance, RateScale-scaled
}

BacktestResult is a lightweight, immutable summary produced at the end of a backtest run. All derived fields are computed by Backtest.BuildBacktestResult.

type BacktestRun

type BacktestRun struct {
	Lots   *LotBook
	Trades []*Trade

	// Execution cost tracking — populated by the run loop.
	SpreadFiltered int   // opens suppressed by the max-spread filter
	SpreadOpened   int   // opens that went through (for avg spread calc)
	SpreadSum      Price // sum of candle.AvgSpread at each accepted open
}

BacktestRun holds mutable state accumulated during a single backtest execution: the current lot book, the list of closed trades, and execution-cost counters updated by the run loop.

func (*BacktestRun) GetTrades

func (run *BacktestRun) GetTrades() []*Trade

GetTrades returns the run's closed trade list, or nil if run is nil.

type BollingerBands added in v0.2.1

type BollingerBands struct {
	// contains filtered or unexported fields
}

BollingerBands computes Bollinger Bands over candle closes. Middle = SMA(n), Upper = Middle + k×σ, Lower = Middle − k×σ where σ is the population standard deviation of the last n closes.

func NewBollingerBands added in v0.2.1

func NewBollingerBands(period int, multiplier float64, scale Scale6) (*BollingerBands, error)

func (*BollingerBands) BandWidth added in v0.2.1

func (b *BollingerBands) BandWidth() float64

BandWidth returns (upper − lower) / middle — a normalised squeeze measure.

func (*BollingerBands) Lower added in v0.2.1

func (b *BollingerBands) Lower() float64

func (*BollingerBands) LowerPrice added in v0.2.1

func (b *BollingerBands) LowerPrice() Price

func (*BollingerBands) Middle added in v0.2.1

func (b *BollingerBands) Middle() float64

func (*BollingerBands) MiddlePrice added in v0.2.1

func (b *BollingerBands) MiddlePrice() Price

func (*BollingerBands) Name added in v0.2.1

func (b *BollingerBands) Name() string

func (*BollingerBands) PercentB added in v0.2.1

func (b *BollingerBands) PercentB(price float64) float64

PercentB returns where price sits relative to the bands: 0.0 = lower, 1.0 = upper, 0.5 = middle.

func (*BollingerBands) PercentBPrice added in v0.2.3

func (b *BollingerBands) PercentBPrice(price Price) float64

func (*BollingerBands) Period added in v0.2.1

func (b *BollingerBands) Period() int

func (*BollingerBands) Ready added in v0.2.1

func (b *BollingerBands) Ready() bool

func (*BollingerBands) Reset added in v0.2.1

func (b *BollingerBands) Reset()

func (*BollingerBands) StdDev added in v0.2.1

func (b *BollingerBands) StdDev() float64

func (*BollingerBands) StdDevPrice added in v0.2.3

func (b *BollingerBands) StdDevPrice() Price

func (*BollingerBands) Update added in v0.2.1

func (b *BollingerBands) Update(c Candle)

func (*BollingerBands) Upper added in v0.2.1

func (b *BollingerBands) Upper() float64

func (*BollingerBands) UpperPrice added in v0.2.1

func (b *BollingerBands) UpperPrice() Price

func (*BollingerBands) Warmup added in v0.2.3

func (b *BollingerBands) Warmup() int

type Broker

type Broker struct {
	Name    string
	Account *Account
	// contains filtered or unexported fields
}

func NewBroker

func NewBroker(name string) *Broker

func (*Broker) Events

func (b *Broker) Events() <-chan *Event

func (*Broker) SubmitClose

func (b *Broker) SubmitClose(ctx context.Context, req *CloseRequest) error

func (*Broker) SubmitOpen

func (b *Broker) SubmitOpen(ctx context.Context, req *OpenRequest) (*Lot, error)

type BuildDecision

type BuildDecision struct {
	Key
	Status   BuildStatus
	Required []Key
	Missing  []Key
	Reason   string
}

type BuildStatus

type BuildStatus int
const (
	BuildUnknown BuildStatus = iota
	BuildReady
	BuildBlocked
	BuildExistsComplete
)

type BuildTask

type BuildTask struct {
	Key
	Inputs []Key
}

BuildTask represents a single candle-aggregation job: build the candles identified by Key from the listed input Keys.

type CSVTicksFeed

type CSVTicksFeed struct {
	// contains filtered or unexported fields
}

CSVTicksFeed reads canonical tick CSV rows:

time,instrument,bid,ask[,event...]

where time is RFC3339 or RFC3339Nano.

It optionally filters ticks to [From, To) if provided. Header row ("time,...") is allowed. Empty/short rows are skipped.

func NewCSVTicksFeed

func NewCSVTicksFeed(path string, from, to Timestamp) (*CSVTicksFeed, error)

NewCSVTicksFeed opens the CSV file at path and returns a feed that yields only ticks whose timestamp falls within [from, to). Pass zero Timestamps to disable filtering.

func (*CSVTicksFeed) Close

func (f *CSVTicksFeed) Close() error

Close releases the underlying file handle.

func (*CSVTicksFeed) Next

func (f *CSVTicksFeed) Next() (Tick, bool, error)

Next advances the feed and returns the next in-range Tick. Returns (Tick{}, false, nil) at EOF and (Tick{}, false, err) on parse errors.

type Candle

type Candle struct {
	Open      Price
	High      Price
	Low       Price
	Close     Price
	AvgSpread Price
	MaxSpread Price
	Ticks     int32 // number of ticks per candle
}

Candle represents a trader domain type.

func (*Candle) FullString

func (c *Candle) FullString() string

FullString is an internal helper for trader type processing.

func (*Candle) IsZero

func (c *Candle) IsZero() bool

IsZero is an internal helper for trader type processing.

func (*Candle) String

func (c *Candle) String() string

String is an internal helper for trader type processing.

func (Candle) Validate added in v0.2.3

func (c Candle) Validate() bool

Validate reports whether the candle has a valid OHLC shape.

type CandleIndicator

type CandleIndicator interface {
	// Name returns a stable identifier like "EMA(20)" or "RSI(14)".
	Name() string

	// Period returns the configured lookback length.
	Period() int

	// Warmup returns how many updates are needed before Ready() can be true.
	// (Some indicators may become ready earlier; that's fine.)
	Warmup() int

	// Reset clears all internal state.
	Reset()

	// Update consumes the next *closed* candle and updates internal state.
	Update(c Candle)

	// Ready reports whether the indicator output is meaningful.
	Ready() bool
}

CandleIndicator computes a single streaming value from candles. It is deterministic and safe to use in live, replay, and backtests.

type CandleIterator added in v0.2.1

type CandleIterator interface {
	Next() (CandleTime, bool)
	Err() error
	Close() error
}

CandleIterator traverses a sequence of timestamped candles.

type CandleRequest

type CandleRequest struct {
	Source     string
	Instrument string
	Range      TimeRange
	Strict     bool
}

func (CandleRequest) Key

func (cr CandleRequest) Key() Key

type CandleSource added in v0.2.3

type CandleSource interface {
	Candles(context.Context, CandleRequest) (CandleIterator, error)
}

CandleSource provides candle iterators for backtest and replay execution. DataManager satisfies this interface.

type CandleTime

type CandleTime = candleTime

CandleTime represents a trader domain type.

type CandleValidationIssue added in v0.2.3

type CandleValidationIssue struct {
	Kind          string   `json:"kind"`
	Severity      string   `json:"severity"`
	Source        string   `json:"source"`
	Instrument    string   `json:"instrument"`
	Timeframe     string   `json:"timeframe"`
	Year          int      `json:"year"`
	Month         int      `json:"month"`
	Path          string   `json:"path,omitempty"`
	RawPath       string   `json:"raw_path,omitempty"`
	Expected      int      `json:"expected"`
	Present       int      `json:"present"`
	Missing       int      `json:"missing"`
	SampleMissing []string `json:"sample_missing,omitempty"`
	Message       string   `json:"message"`
}

type CandleValidationReport added in v0.2.3

type CandleValidationReport struct {
	Source        string                  `json:"source"`
	Timeframe     string                  `json:"timeframe"`
	IncludeRaw    bool                    `json:"include_raw"`
	MonthsScanned int                     `json:"months_scanned"`
	Issues        []CandleValidationIssue `json:"issues"`
}

func ValidateCandleData added in v0.2.3

func ValidateCandleData(ctx context.Context, req CandleValidationRequest) (*CandleValidationReport, error)

func (*CandleValidationReport) IssueCount added in v0.2.3

func (r *CandleValidationReport) IssueCount() int

type CandleValidationRequest added in v0.2.3

type CandleValidationRequest struct {
	Instruments []string
	Source      string
	Timeframe   Timeframe
	Start       time.Time
	End         time.Time
	IncludeRaw  bool
	RawDir      string
}

type ChandelierExit

type ChandelierExit struct {
	// contains filtered or unexported fields
}

ChandelierExit trails the stop from the highest-high (long) or lowest-low (short) seen since entry, offset by N×ATR. The stop only ever moves in the profitable direction — it never moves against the position.

Per-position extreme tracking lives on Lot.ExtremePrice so multiple concurrent lots each maintain their own watermark.

func NewChandelierExit

func NewChandelierExit(atrPeriod int, multiplier float64, scale Scale6) (*ChandelierExit, error)

func (*ChandelierExit) InitialStop

func (c *ChandelierExit) InitialStop(side Side, entry Price, candle Candle) Price

func (*ChandelierExit) Name

func (c *ChandelierExit) Name() string

func (*ChandelierExit) Ready

func (c *ChandelierExit) Ready() bool

func (*ChandelierExit) Tick

func (c *ChandelierExit) Tick(candle Candle)

func (*ChandelierExit) UpdateStop

func (c *ChandelierExit) UpdateStop(side Side, currentStop Price, _ Price, extreme Price, candle Candle) Price

type ChoppinessFilter

type ChoppinessFilter struct {
	// contains filtered or unexported fields
}

ChoppinessFilter gates entries using the Choppiness Index. When CI < threshold the market is trending; entries are allowed. When CI >= threshold the market is ranging; new opens are suppressed. The conventional threshold is 61.8. Trending() returns true before Ready() as a defensive contract, although the main callers already gate on Ready() before consulting the regime state.

func NewChoppinessFilter

func NewChoppinessFilter(period int, threshold float64, scale Scale6) (*ChoppinessFilter, error)

func (*ChoppinessFilter) AllowSide added in v0.2.1

func (f *ChoppinessFilter) AllowSide(_ Side) bool

func (*ChoppinessFilter) Choppiness added in v0.2.3

func (f *ChoppinessFilter) Choppiness() float64

Choppiness exposes the raw CI value for logging/debugging.

func (*ChoppinessFilter) Name

func (f *ChoppinessFilter) Name() string

func (*ChoppinessFilter) Ready

func (f *ChoppinessFilter) Ready() bool

func (*ChoppinessFilter) Tick

func (f *ChoppinessFilter) Tick(ct CandleTime)

func (*ChoppinessFilter) Trending

func (f *ChoppinessFilter) Trending() bool

func (*ChoppinessFilter) Value

func (f *ChoppinessFilter) Value() float64

Value exposes the raw CI value for logging/debugging.

type ChoppinessIndex

type ChoppinessIndex struct {
	// contains filtered or unexported fields
}

ChoppinessIndex measures whether price action is trending or ranging.

Formula: 100 × log10(Σ TR(1,N) / (HH(N) − LL(N))) / log10(N)

Values near 100 = choppy/consolidating; near 0 = strongly trending. Conventional threshold: 61.8 (trending below, ranging above).

func NewChoppinessIndex

func NewChoppinessIndex(period int, scale Scale6) (*ChoppinessIndex, error)

func (*ChoppinessIndex) Float64 added in v0.2.3

func (c *ChoppinessIndex) Float64() float64

func (*ChoppinessIndex) Name

func (c *ChoppinessIndex) Name() string

func (*ChoppinessIndex) Period added in v0.2.3

func (c *ChoppinessIndex) Period() int

func (*ChoppinessIndex) Ready

func (c *ChoppinessIndex) Ready() bool

func (*ChoppinessIndex) Reset

func (c *ChoppinessIndex) Reset()

func (*ChoppinessIndex) Update

func (c *ChoppinessIndex) Update(candle Candle)

func (*ChoppinessIndex) Warmup

func (c *ChoppinessIndex) Warmup() int

type CloseMatcher

type CloseMatcher interface {
	Match(lots []*Lot, units Units) ([]LotMatch, error)
}

type CloseRequest

type CloseRequest struct {
	Request
	*Lot
	CloseCause closeCause
}

CloseRequest represents a trader domain type.

func (*CloseRequest) Validate added in v0.2.3

func (r *CloseRequest) Validate() error

Validate is an internal helper for trader type processing.

type CompiledBacktest added in v0.2.3

type CompiledBacktest struct {
	ID        string
	RunConfig RunConfig
	Request   BacktestRequest
}

CompiledBacktest is the construction-phase output for one backtest run. It is immutable and contains the resolved config snapshot plus the validated request used to instantiate an executable Backtest later.

func CompileBacktests added in v0.2.3

func CompileBacktests(cfg *Config) ([]CompiledBacktest, error)

CompileBacktests converts a loaded Config into validated, immutable backtest definitions. Defaults are applied during construction so execution only deals with already-compiled requests.

func (CompiledBacktest) NewRun added in v0.2.3

func (c CompiledBacktest) NewRun() Backtest

NewRun instantiates a fresh executable Backtest from a compiled definition.

type CompositeRegimeFilter added in v0.2.1

type CompositeRegimeFilter struct {
	// contains filtered or unexported fields
}

CompositeRegimeFilter ANDs multiple RegimeFilters: Trending() returns true only when every sub-filter returns true. Tick() is called on all sub-filters regardless of their individual state so each indicator stays current.

func NewCompositeRegimeFilter added in v0.2.1

func NewCompositeRegimeFilter(filters []RegimeFilter) *CompositeRegimeFilter

func (*CompositeRegimeFilter) AllowSide added in v0.2.1

func (c *CompositeRegimeFilter) AllowSide(side Side) bool

func (*CompositeRegimeFilter) Name added in v0.2.1

func (c *CompositeRegimeFilter) Name() string

func (*CompositeRegimeFilter) Ready added in v0.2.1

func (c *CompositeRegimeFilter) Ready() bool

func (*CompositeRegimeFilter) Tick added in v0.2.1

func (c *CompositeRegimeFilter) Tick(ct CandleTime)

func (*CompositeRegimeFilter) Trending added in v0.2.1

func (c *CompositeRegimeFilter) Trending() bool

type Config

type Config struct {
	Version  int         `json:"version" yaml:"version"`
	Defaults RunDefaults `json:"defaults" yaml:"defaults"`
	Runs     []RunConfig `json:"runs" yaml:"runs"`
}

Config is the top-level structure parsed from a YAML or JSON config file. It carries a set of defaults that are merged into each RunConfig before the run is executed.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and parses a YAML or JSON config file from path. The file extension determines the parser (.yaml/.yml → YAML; .json → JSON). Returns an error if the file is missing, unparseable, or contains no runs.

type D1ADXFilter added in v0.2.1

type D1ADXFilter struct {
	// contains filtered or unexported fields
}

D1ADXFilter is a regime filter that applies ADX at the daily timeframe while being fed sub-daily bars (e.g. H1). It aggregates intraday bars into daily OHLC and updates the ADX only when a day closes.

Trending() returns true when D1 ADX >= threshold, meaning the daily timeframe confirms a broad enough trend to allow new entries. AllowSide() always returns true because this is a regime gate, not a directional filter. Trending() returns true before Ready() as a defensive contract, although the main callers already gate on Ready() before consulting the regime state.

Registered in the factory as "adx-d1".

func NewD1ADXFilter added in v0.2.1

func NewD1ADXFilter(period int, threshold float64, scale Scale6) (*D1ADXFilter, error)

func (*D1ADXFilter) ADX added in v0.2.3

func (f *D1ADXFilter) ADX() float64

ADX exposes the raw ADX value for debugging.

func (*D1ADXFilter) ADXValue added in v0.2.1

func (f *D1ADXFilter) ADXValue() float64

ADXValue exposes the raw ADX value for debugging.

func (*D1ADXFilter) AllowSide added in v0.2.1

func (f *D1ADXFilter) AllowSide(_ Side) bool

func (*D1ADXFilter) Name added in v0.2.1

func (f *D1ADXFilter) Name() string

func (*D1ADXFilter) Ready added in v0.2.1

func (f *D1ADXFilter) Ready() bool

func (*D1ADXFilter) Tick added in v0.2.1

func (f *D1ADXFilter) Tick(ct CandleTime)

func (*D1ADXFilter) Trending added in v0.2.1

func (f *D1ADXFilter) Trending() bool

type D1ChoppinessFilter added in v0.2.1

type D1ChoppinessFilter struct {
	// contains filtered or unexported fields
}

D1ChoppinessFilter is a regime filter that applies the Choppiness Index at the daily timeframe while being fed sub-daily bars (e.g. H1). It aggregates intraday bars into daily OHLC and updates the CI only when a day closes.

This avoids the correlation problem that arises when using same-timeframe CI with Donchian breakouts: a breakout bar will always look "trending" at the moment of entry when measured on its own timeframe. The daily CI captures whether the broader market context is trending over multiple days, which is independent of any individual H1 breakout signal. AllowSide() always returns true because this is a regime gate, not a directional filter. Trending() returns true before Ready() as a defensive contract, although the main callers already gate on Ready() before consulting the regime state.

Registered in the factory as "choppiness-d1".

func NewD1ChoppinessFilter added in v0.2.1

func NewD1ChoppinessFilter(period int, threshold float64, scale Scale6) (*D1ChoppinessFilter, error)

func (*D1ChoppinessFilter) AllowSide added in v0.2.1

func (f *D1ChoppinessFilter) AllowSide(_ Side) bool

func (*D1ChoppinessFilter) Choppiness added in v0.2.3

func (f *D1ChoppinessFilter) Choppiness() float64

Choppiness exposes the raw CI value for debugging.

func (*D1ChoppinessFilter) Name added in v0.2.1

func (f *D1ChoppinessFilter) Name() string

func (*D1ChoppinessFilter) Ready added in v0.2.1

func (f *D1ChoppinessFilter) Ready() bool

func (*D1ChoppinessFilter) Tick added in v0.2.1

func (f *D1ChoppinessFilter) Tick(ct CandleTime)

func (*D1ChoppinessFilter) Trending added in v0.2.1

func (f *D1ChoppinessFilter) Trending() bool

func (*D1ChoppinessFilter) Value added in v0.2.1

func (f *D1ChoppinessFilter) Value() float64

Value exposes the raw CI value for debugging.

type DataConfig

type DataConfig struct {
	Source     string `json:"source" yaml:"source"`
	Instrument string `json:"instrument" yaml:"instrument"`
	Timeframe  string `json:"timeframe" yaml:"timeframe"`
	From       string `json:"from" yaml:"from"`
	To         string `json:"to" yaml:"to"`
	Strict     *bool  `json:"strict" yaml:"strict"`
}

DataConfig specifies the data source, instrument, timeframe, and date range for a run.

type DataKind

type DataKind uint8
const (
	KindUnknown DataKind = iota
	KindTick
	KindCandle
)

func (DataKind) String

func (k DataKind) String() string

type DataManager

type DataManager struct {
	Start       time.Time
	End         time.Time
	Instruments []string
	// contains filtered or unexported fields
}

DataManager is responsible for identifing data files that are missing accross all instruments. For missing datasets, ensure they are downloaded, for datasets that are downloaded, make sure they are made into candles.

func GetDataManager

func GetDataManager() *DataManager

func NewDataManager

func NewDataManager(instruments []string, start, end time.Time) *DataManager

NewDataManager constructs a DataManager for the given instruments and time range.

func (*DataManager) BuildWantList

func (dm *DataManager) BuildWantList(ctx context.Context) (*Wantlist, error)

func (*DataManager) Candles

func (dm *DataManager) Candles(ctx context.Context, req CandleRequest) (CandleIterator, error)

func (*DataManager) ExecuteDownloads

func (dm *DataManager) ExecuteDownloads(ctx context.Context) error

func (*DataManager) Init

func (dm *DataManager) Init()

Init will get DataManager ready to go.

func (*DataManager) Plan

func (dm *DataManager) Plan(ctx context.Context) (*Plan, error)

func (*DataManager) Sync

func (dm *DataManager) Sync(ctx context.Context, download, build bool) error

type EMA

type EMA struct {
	// contains filtered or unexported fields
}

EMA computes an Exponential Moving Average over candle closes.

Pricing note:

  • trader.Candle prices are scaled integers.
  • EMA stores scaled price units internally; Float64 is only for display.

func NewEMA

func NewEMA(period int, scale Scale6) (*EMA, error)

func (*EMA) Float64

func (e *EMA) Float64() float64

func (*EMA) Name

func (e *EMA) Name() string

func (*EMA) Period

func (e *EMA) Period() int

func (*EMA) Price added in v0.2.3

func (e *EMA) Price() Price

func (*EMA) PriceSum added in v0.2.3

func (e *EMA) PriceSum() PriceSum

func (*EMA) Ready

func (e *EMA) Ready() bool

func (*EMA) Reset

func (e *EMA) Reset()

func (*EMA) Update

func (e *EMA) Update(c Candle)

func (*EMA) Warmup

func (e *EMA) Warmup() int

type EquitySnapshot

type EquitySnapshot struct {
	Timestamp   Timestamp
	Balance     Money
	Equity      Money
	MarginUsed  Money
	FreeMargin  Money
	MarginLevel Money
}

EquitySnapshot captures account state at a point in time for journal backends that persist balance/equity history alongside completed trades.

type Event

type Event struct {
	Type  EventType
	Trade *Trade
	Lot   *Lot
}

type EventType

type EventType int
const (
	EventOrderFilled EventType = iota + 1
	EventPositionClosed
)

func (EventType) String

func (e EventType) String() string

type ExitConfig

type ExitConfig struct {
	Kind   string         `json:"kind"   yaml:"kind"`
	Params map[string]any `json:"params" yaml:"params"`
}

ExitConfig mirrors the exit: section of a YAML backtest config.

type ExitStrategy

type ExitStrategy interface {
	// Name returns a human-readable description for reports.
	Name() string

	// Ready reports whether the exit strategy has enough history to place stops.
	Ready() bool

	// Tick updates internal indicators. Called every bar before strategy.Update().
	Tick(c Candle)

	// InitialStop returns the stop price at the moment a position is opened.
	InitialStop(side Side, entry Price, c Candle) Price

	// UpdateStop returns the new stop price for an open lot each bar.
	// extreme is the lot's ExtremePrice (highest high for longs, lowest low for shorts).
	// The implementation must never move the stop against the position.
	UpdateStop(side Side, currentStop Price, entry Price, extreme Price, c Candle) Price
}

ExitStrategy manages stop placement after a position is open. It is called every bar regardless of position state (to warm up indicators), and is consulted to set/update the stop price on open lots.

func GetExitStrategy

func GetExitStrategy(cfg ExitConfig, scale Scale6) (ExitStrategy, error)

GetExitStrategy constructs an ExitStrategy from cfg. If cfg.Kind is empty, NoopExit is returned (pass-through).

type FIFOMatcher

type FIFOMatcher struct{}

FIFOMatcher closes the oldest open lots first.

func (FIFOMatcher) Match

func (FIFOMatcher) Match(lots []*Lot, units Units) ([]LotMatch, error)

type Float64Indicator added in v0.2.3

type Float64Indicator interface {
	// Float64 returns the current indicator value. If !Ready(), it should return 0
	// (or the last computed value) — callers should always check Ready().
	Float64() float64
}

type ForexAnalysis added in v0.2.3

type ForexAnalysis struct {
	Group          string         `json:"-"`
	Pair           string         `json:"-"`
	Structure      string         `json:"-"`
	SetupBias      string         `json:"-"`
	Trend          string         `json:"-"`
	Volatility     string         `json:"-"`
	SupportLow     Price          `json:"-"`
	SupportHigh    Price          `json:"-"`
	ResistanceLow  Price          `json:"-"`
	ResistanceHigh Price          `json:"-"`
	Status         AnalysisStatus `json:"-"`
}

ForexAnalysis holds one row from a ChatGPT forex analysis CSV. Price fields are stored as scaled int32 (Price) matching the rest of the engine; JSON output converts them back to decimal via Float64().

func (ForexAnalysis) IsTradeable added in v0.2.3

func (f ForexAnalysis) IsTradeable() bool

IsTradeable reports whether the row is an active trade candidate.

func (ForexAnalysis) IsWatched added in v0.2.3

func (f ForexAnalysis) IsWatched() bool

IsWatched reports whether the row belongs on any watchlist (both Watchlist and Tradeable rows qualify).

func (ForexAnalysis) MarshalJSON added in v0.2.3

func (f ForexAnalysis) MarshalJSON() ([]byte, error)

MarshalJSON emits price fields as decimal floats.

type GlobalConfig added in v0.2.1

type GlobalConfig struct {
	Log   GlobalLogConfig   `yaml:"log"`
	Data  GlobalDataConfig  `yaml:"data"`
	OANDA GlobalOANDAConfig `yaml:"oanda"`
	DB    string            `yaml:"db"`
}

GlobalConfig holds settings that apply across all trader commands. It is populated by merging YAML files from the standard search path in order:

  1. /etc/trader/*.yml — system-wide defaults
  2. ~/.config/trader/*.yml — user overrides
  3. explicit path — passed via root --config flag

Within each directory, files are merged alphabetically. Later files override earlier ones for any non-empty field.

func LoadGlobalConfig added in v0.2.1

func LoadGlobalConfig(explicitPath string) (*GlobalConfig, error)

LoadGlobalConfig merges global config files from the standard search path plus an optional explicit file. Missing directories and files are silently skipped; a parse error in any file is returned immediately.

type GlobalDataConfig added in v0.2.1

type GlobalDataConfig struct {
	Dir string `yaml:"dir"`
}

GlobalDataConfig holds data directory settings.

type GlobalLogConfig added in v0.2.1

type GlobalLogConfig struct {
	Level  string `yaml:"level"`
	File   string `yaml:"file"`
	Format string `yaml:"format"`
}

GlobalLogConfig holds log-related global settings.

type GlobalOANDAConfig added in v0.2.1

type GlobalOANDAConfig struct {
	Token     string `yaml:"token"`
	AccountID string `yaml:"account_id"`
	Env       string `yaml:"env"`
}

GlobalOANDAConfig holds OANDA broker credentials.

type Instrument

type Instrument struct {
	Name                string
	BaseCurrency        string
	QuoteCurrency       string
	PipLocation         int
	TradeUnitsPrecision int
	MinimumTradeSize    Units
	MarginRate          Rate
}

Instrument represents a trader domain type.

func GetInstrument

func GetInstrument(symbol string) *Instrument

GetInstrument is an internal helper for trader type processing.

func LookupInstrument added in v0.2.3

func LookupInstrument(symbol string) (Instrument, bool)

LookupInstrument returns a copy of the instrument metadata and whether it exists.

func (*Instrument) AddPips

func (inst *Instrument) AddPips(px Price, pips Pips) Price

AddPips is an internal helper for trader type processing.

func (*Instrument) DukascopyPriceMultiplier

func (inst *Instrument) DukascopyPriceMultiplier() uint32

DukascopyPriceMultiplier returns the factor needed to convert a raw Dukascopy bi5 price integer into a Price value at the current PriceScale.

Dukascopy stores prices with (−PipLocation + 1) decimal places:

  • 5-decimal pairs (EURUSD, PipLocation=−4): native scale 100,000 → multiplier = 1
  • 3-decimal pairs (USDJPY, PipLocation=−2): native scale 1,000 → multiplier = 100

func (*Instrument) PipSize

func (inst *Instrument) PipSize() float64

PipSize is an internal helper for trader type processing.

func (*Instrument) PipValueUSD added in v0.2.1

func (inst *Instrument) PipValueUSD(rate float64, units int64, pips float64) float64

PipValueUSD returns the USD value of pips pips for a position of units units.

For USD-quoted pairs (EURUSD, GBPUSD, AUDUSD, NZDUSD) the result is exact and rate is ignored. For USD-base pairs (USDJPY, USDCHF, USDCAD) the pip value is denominated in the quote currency, so rate (the current pair price) is required to convert back to USD. Returns 0 if rate ≤ 0.

func (*Instrument) PriceDeltaFromPips

func (inst *Instrument) PriceDeltaFromPips(pips Pips) Price

PriceDeltaFromPips is an internal helper for trader type processing.

func (*Instrument) PriceUnitsPerPip

func (inst *Instrument) PriceUnitsPerPip() Price

PriceUnitsPerPip is an internal helper for trader type processing.

func (*Instrument) SubPips

func (inst *Instrument) SubPips(px Price, pips Pips) Price

SubPips is an internal helper for trader type processing.

type Inventory

type Inventory struct {
	// contains filtered or unexported fields
}

func BuildInventory

func BuildInventory(ctx context.Context) (*Inventory, error)

func NewInventory

func NewInventory() *Inventory

func (*Inventory) Delete

func (inv *Inventory) Delete(key Key)

func (*Inventory) Get

func (inv *Inventory) Get(key Key) (Asset, bool)

func (*Inventory) Has

func (inv *Inventory) Has(key Key) bool

func (*Inventory) HasComplete

func (inv *Inventory) HasComplete(key Key) bool

func (*Inventory) Keys

func (inv *Inventory) Keys() []Key

func (*Inventory) Len

func (inv *Inventory) Len() int

func (*Inventory) List

func (inv *Inventory) List() []Asset

func (*Inventory) MissingComplete

func (inv *Inventory) MissingComplete(keys []Key) []Key

func (*Inventory) Put

func (inv *Inventory) Put(a Asset)

func (*Inventory) TicksComplete

func (inv *Inventory) TicksComplete(k Key) (complete bool, required []Key, missing []Key, err error)

func (*Inventory) Update

func (inv *Inventory) Update(key Key, fn func(*Asset) error) error

func (*Inventory) WantReasonFor added in v0.2.3

func (inv *Inventory) WantReasonFor(key Key) (WantReason, bool)

type Journal

type Journal interface {
	RecordTrade(TradeRecord) error
	RecordEquity(EquitySnapshot) error
	Close() error
}

Journal is the storage contract used by live trading and replay code to persist completed trades and optional equity snapshots.

type Key

type Key struct {
	Instrument string
	Source     string
	Kind       DataKind
	TF         Timeframe
	Year       int
	Month      int
	Day        int
	Hour       int
}

func RequiredTickHoursForMonth

func RequiredTickHoursForMonth(source, instrument string, year, month int) ([]Key, error)

func (Key) IsHourlyTick

func (k Key) IsHourlyTick() bool

func (Key) IsMonthlyCandle

func (k Key) IsMonthlyCandle() bool

func (Key) Range

func (k Key) Range() (TimeRange, error)

func (Key) Time

func (ak Key) Time() time.Time

Time returns the UTC time represented by the key. Missing fields are normalized to the earliest valid value. Use Validate() first when invalid keys should be rejected instead of coerced.

Examples:

Year=2024, Month=0, Day=0, Hour=0 -> 2024-01-01 00:00:00 UTC
Year=2024, Month=5, Day=0, Hour=0 -> 2024-05-01 00:00:00 UTC
Year=2024, Month=5, Day=7, Hour=13 -> 2024-05-07 13:00:00 UTC

func (Key) Validate added in v0.2.3

func (k Key) Validate() error

type Keymap

type Keymap[V any] struct {
	// contains filtered or unexported fields
}

func NewKeymap

func NewKeymap[V any]() Keymap[V]

func (*Keymap[V]) Delete

func (km *Keymap[V]) Delete(key Key)

func (*Keymap[V]) Get

func (km *Keymap[V]) Get(key Key) (V, bool)

func (*Keymap[V]) Has

func (km *Keymap[V]) Has(key Key) bool

func (*Keymap[V]) Keys

func (km *Keymap[V]) Keys() []Key

func (*Keymap[V]) Len

func (km *Keymap[V]) Len() int

func (*Keymap[V]) List

func (km *Keymap[V]) List() []V

func (*Keymap[V]) Put

func (km *Keymap[V]) Put(key Key, v V)

func (*Keymap[V]) Range

func (km *Keymap[V]) Range(fn func(Key, V) bool)

func (*Keymap[V]) Update

func (km *Keymap[V]) Update(key Key, fn func(*V) error) error

type LinearCongruentialRandom

type LinearCongruentialRandom struct {
	// contains filtered or unexported fields
}

LinearCongruentialRandom is a simple deterministic RNG.

func NewLCRandom

func NewLCRandom(seed int64) *LinearCongruentialRandom

NewLCRandom creates a new LCR with a seed.

func (*LinearCongruentialRandom) NextGaussian

func (r *LinearCongruentialRandom) NextGaussian() float64

NextGaussian returns a pseudo-random number from a normal distribution (Box-Muller).

func (*LinearCongruentialRandom) NextUniform

func (r *LinearCongruentialRandom) NextUniform() float64

NextUniform returns a pseudo-random number in [0, 1).

type LiveJournal

type LiveJournal struct {
	// contains filtered or unexported fields
}

LiveJournal subscribes to an OANDA transaction stream and writes complete TradeRecord rows to the configured Journal as trades close.

Open ORDER_FILL events are buffered in memory (keyed by tradeID) until the matching close ORDER_FILL arrives. The close fill provides the realized P/L; we look up the buffered open to fill in entry side and open time, then RecordTrade(...) writes the complete row.

Heartbeats advance an in-memory "lastSeenTxID" cursor so callers can reconnect (or poll for gap recovery) from a known point.

func NewLiveJournal

func NewLiveJournal(client *oanda.Client, accountID string, journal Journal, log *slog.Logger) *LiveJournal

NewLiveJournal creates a journal worker. Call Run to start the subscription.

func (*LiveJournal) Backfill

func (lj *LiveJournal) Backfill(ctx context.Context, sinceID int64) error

Backfill polls GetTransactions from sinceID forward and replays them into the same handler used for streamed events. Call before Run to recover anything missed during downtime.

func (*LiveJournal) LastSeenTxID

func (lj *LiveJournal) LastSeenTxID() int64

LastSeenTxID returns the highest transaction ID we've processed (via heartbeat or actual transaction). Persist this for resume on restart.

func (*LiveJournal) Run

func (lj *LiveJournal) Run(ctx context.Context) error

Run subscribes to the transaction stream and processes events until ctx is cancelled or the stream ends. Returns the final error from the stream (nil on clean ctx-cancel exit).

func (*LiveJournal) SetBotIDLookup added in v0.2.3

func (lj *LiveJournal) SetBotIDLookup(fn func(tradeID string) string)

SetBotIDLookup provides a function the journal calls on each close to find which bot opened a given OANDA trade ID. This lets the centralized journal (one per serve process) tag TradeRecords with the correct bot.

type LiveOpenRequest

type LiveOpenRequest struct {
	Side     string  // "long" or "short"
	StopPips float64 // stop-loss distance in pips
	TakePips float64 // take-profit distance in pips (0 = none)
	RiskPct  float64 // percent of account NAV to risk
	Reason   string  // strategy signal reason, e.g. "donchian-v6-breakout-down"
}

LiveOpenRequest carries the parameters for a new live position.

type LivePlan

type LivePlan struct {
	// Open describes a new position to open. Nil means hold.
	Open *LiveOpenRequest
	// CloseIDs lists trade IDs the strategy wants to close.
	CloseIDs []string
	// Reason is a human-readable note logged by the runner.
	Reason string
}

LivePlan is what the strategy asks the runner to do this tick. At most one new position is opened per tick; zero or more are closed.

type LivePrice

type LivePrice struct {
	Instrument string
	Bid        float64
	Ask        float64
	Time       time.Time
}

LivePrice is a bid/ask snapshot from the broker.

func (LivePrice) Mid

func (p LivePrice) Mid() float64

Mid returns the mid-price.

type LiveStrategy

type LiveStrategy interface {
	Name() string

	// Tick is called once per poll interval. price is the current bid/ask snapshot.
	// openTrades lists all tracked open positions for this strategy's instrument.
	// Returns a plan (open one new position and/or close a set of existing ones).
	Tick(ctx context.Context, price LivePrice, openTrades []LiveTrade) *LivePlan
}

LiveStrategy is implemented by strategies that drive live (non-backtest) trading. Tick is called on each price poll; the runner tracks position ages and passes them in so the strategy can decide what to open or close.

func GetLiveStrategy added in v0.2.3

func GetLiveStrategy(scfg StrategyConfig) (LiveStrategy, error)

GetLiveStrategy looks up and constructs a LiveStrategy by kind.

type LiveStrategyConstructor added in v0.2.3

type LiveStrategyConstructor func(params map[string]any) (LiveStrategy, error)

LiveStrategyConstructor builds a LiveStrategy from a params map.

func LookupLiveStrategy added in v0.2.3

func LookupLiveStrategy(name string) LiveStrategyConstructor

LookupLiveStrategy returns the constructor registered under name, or nil.

type LiveTrade

type LiveTrade struct {
	ID           string
	Instrument   string
	Units        int64 // positive = long, negative = short
	EntryPrice   float64
	UnrealizedPL float64
	OpenTime     time.Time // when OANDA opened the trade
	TicksOpen    int       // estimated ticks elapsed, seeded from OpenTime on restart
}

LiveTrade describes an open position as seen by the live runner.

func (LiveTrade) Side

func (t LiveTrade) Side() string

Side returns "long" or "short".

type LogConfig

type LogConfig struct {
	// Level is the minimum log level to emit.  Accepted values (case-
	// insensitive): "debug", "info", "warn" / "warning", "error".
	// Defaults to "info" when empty or unrecognised.
	Level string

	// Format selects the handler format: "json" for JSON output, anything
	// else (or empty) for human-readable text.
	Format string

	// File is an optional path to a log file. When non-empty, log records
	// are written to both stdout and this file. When empty and no other sink
	// is configured, Setup falls back to a default log file.
	File string

	// Syslog enables forwarding of log records to the system logger.
	// Has no effect on Windows (syslog is not available there).
	Syslog bool

	// Stdout enables log output to stdout
	Stdout bool

	// Memory enables in-memory capture of log entries, accessible via
	// Entries() and ClearEntries().  Useful for testing and diagnostics.
	Memory bool
}

LogConfig holds the logging configuration that is typically populated from the application's RootConfig (RootConfig.LogLevel, etc.).

type LogEntry

type LogEntry struct {
	Time    time.Time
	Level   slog.Level
	Message string
	Attrs   []slog.Attr
}

LogEntry is a single structured log record stored in the in-memory stack.

func Entries

func Entries() []LogEntry

Entries returns a snapshot (copy) of all log entries currently held in the in-memory stack. It is safe to call from multiple goroutines.

type Lot

type Lot struct {
	*TradeCommon
	EntryPrice     Price
	EntryTime      Timestamp
	OriginalUnits  Units
	RemainingUnits Units
	State          lotState
	// ExtremePrice tracks the highest-high (long) or lowest-low (short) seen
	// since entry. Used by trailing/chandelier exit strategies.
	ExtremePrice Price
}

Lot represents a trader domain type.

func (*Lot) Clone added in v0.2.3

func (lot *Lot) Clone() *Lot

Clone is an internal helper for trader type processing.

func (*Lot) Validate added in v0.2.3

func (lot *Lot) Validate() error

Validate is an internal helper for trader type processing.

type LotBook

type LotBook struct {
	// contains filtered or unexported fields
}

LotBook represents a trader domain type.

func (*LotBook) Add

func (lb *LotBook) Add(lot *Lot) error

Add is an internal helper for trader type processing.

func (*LotBook) All

func (lb *LotBook) All() map[string]*Lot

All is an internal helper for trader type processing.

func (*LotBook) Delete

func (lb *LotBook) Delete(id string) bool

Delete is an internal helper for trader type processing.

func (*LotBook) Get added in v0.2.3

func (lb *LotBook) Get(id string) *Lot

Get is an internal helper for trader type processing.

func (*LotBook) Has added in v0.2.3

func (lb *LotBook) Has(id string) bool

Has is an internal helper for trader type processing.

func (*LotBook) Len

func (lb *LotBook) Len() int

Len is an internal helper for trader type processing.

func (*LotBook) Range

func (lb *LotBook) Range(fn func(*Lot) error) error

Range is an internal helper for trader type processing.

func (*LotBook) Slice

func (lb *LotBook) Slice() []*Lot

Slice is an internal helper for trader type processing.

type LotMatch

type LotMatch struct {
	Lot   *Lot
	Units Units
}

type Money

type Money int64

Money represents a trader domain type.

func MoneyFromFloat

func MoneyFromFloat(f float64) Money

MoneyFromFloat is an internal helper for trader type processing.

func (Money) Float64

func (m Money) Float64() float64

Float64 is an internal helper for trader type processing.

func (Money) String

func (m Money) String() string

String is an internal helper for trader type processing.

type NoopExit

type NoopExit struct{}

NoopExit is a pass-through exit strategy. It never moves stops; the entry strategy is responsible for setting an initial stop via the OpenRequest.

func (NoopExit) InitialStop

func (NoopExit) InitialStop(_ Side, _ Price, _ Candle) Price

func (NoopExit) Name

func (NoopExit) Name() string

func (NoopExit) Ready

func (NoopExit) Ready() bool

func (NoopExit) Tick

func (NoopExit) Tick(_ Candle)

func (NoopExit) UpdateStop

func (NoopExit) UpdateStop(_ Side, currentStop Price, _ Price, _ Price, _ Candle) Price

type NoopRegime

type NoopRegime struct{}

NoopRegime is a pass-through filter that always allows trading.

func (NoopRegime) AllowSide added in v0.2.1

func (NoopRegime) AllowSide(_ Side) bool

func (NoopRegime) Name

func (NoopRegime) Name() string

func (NoopRegime) Ready

func (NoopRegime) Ready() bool

func (NoopRegime) Tick

func (NoopRegime) Tick(_ CandleTime)

func (NoopRegime) Trending

func (NoopRegime) Trending() bool

type OpenRequest

type OpenRequest struct {
	Request
}

OpenRequest represents a trader domain type.

func NewOpenRequest

func NewOpenRequest(
	instr string,
	c *CandleTime,
	side Side,
	stop Price,
	take Price,
	reason string) *OpenRequest

NewOpenRequest is an internal helper for trader type processing.

func (*OpenRequest) Validate added in v0.2.3

func (r *OpenRequest) Validate() error

Validate is an internal helper for trader type processing.

type Pips

type Pips int32

Pips stores tenths of a pip (deci-pips): 1 == 0.1 pip and 20 == 2.0 pips.

func PipsFromFloat

func PipsFromFloat(v float64) Pips

PipsFromFloat converts a whole/decimal pip count into internal deci-pips.

func (Pips) Float64

func (p Pips) Float64() float64

Float64 is an internal helper for trader type processing.

type Plan

type Plan struct {
	Download []Key
	BuildM1  []BuildTask
	BuildH1  []BuildTask
	BuildD1  []BuildTask
}

Plan describes the data-preparation work that must be completed before a backtest can run: files to download and candle aggregations to build at each timeframe.

func (*Plan) BuildTasks added in v0.2.3

func (p *Plan) BuildTasks(tf Timeframe) []BuildTask

func (*Plan) Empty added in v0.2.3

func (p *Plan) Empty() bool

func (*Plan) Log

func (p *Plan) Log()

Log emits a structured summary of the plan (download and build counts) at info level.

func (*Plan) TotalBuilds added in v0.2.3

func (p *Plan) TotalBuilds() int

type Position

type Position struct {
	Instrument         string
	LongUnits          Units
	LongAvgEntryPrice  Price
	ShortUnits         Units
	ShortAvgEntryPrice Price
	NetUnits           Units
}

Position is the computed aggregate view of all open lots for one instrument. Hedged books keep separate long/short exposure and entry prices.

type Price

type Price int32

Price represents a trader domain type.

func PriceFromFloat

func PriceFromFloat(f float64) Price

PriceFromFloat is an internal helper for trader type processing.

func (Price) Float64

func (p Price) Float64() float64

Float64 is an internal helper for trader type processing.

func (Price) String

func (p Price) String() string

String is an internal helper for trader type processing.

type PriceIndicator added in v0.2.3

type PriceIndicator interface {
	Price() Price
}

type PriceSum added in v0.2.3

type PriceSum int64

PriceSum represents an accumulated sum of Price values.

type Rate

type Rate int64

Rate represents a trader domain type.

func RateFromFloat

func RateFromFloat(f float64) Rate

RateFromFloat is an internal helper for trader type processing.

func (Rate) Float64

func (r Rate) Float64() float64

Float64 is an internal helper for trader type processing.

func (Rate) String

func (r Rate) String() string

String is an internal helper for trader type processing.

type RawTick

type RawTick struct {
	Ask    Price
	Bid    Price
	AskVol float32
	BidVol float32
	// contains filtered or unexported fields
}

func (RawTick) FloorToHour

func (ms RawTick) FloorToHour() timemilli

FloorToHour is an internal helper for trader type processing.

func (RawTick) FloorToMinute

func (ms RawTick) FloorToMinute() timemilli

FloorToMinute is an internal helper for trader type processing.

func (RawTick) Mid

func (t RawTick) Mid() Price

func (RawTick) Minute

func (t RawTick) Minute() timemilli

func (RawTick) Sec

func (ms RawTick) Sec() Timestamp

Conversions

func (RawTick) Spread

func (t RawTick) Spread() Price

func (RawTick) TimeMS

func (t RawTick) TimeMS() int64

TimeMS returns the tick timestamp in milliseconds since the Unix epoch. Exported for use by sibling packages that need raw tick time.

type RegimeConfig

type RegimeConfig struct {
	Kind    string         `json:"kind"    yaml:"kind"`
	Params  map[string]any `json:"params"  yaml:"params"`
	Filters []RegimeConfig `json:"filters" yaml:"filters"` // for composite kind
}

RegimeConfig mirrors the regime: section of a YAML backtest config.

type RegimeFilter

type RegimeFilter interface {
	// Name returns a human-readable label for reports.
	Name() string

	// Ready reports whether the filter has enough history to classify.
	Ready() bool

	// Tick updates internal indicators with the current bar. The full
	// CandleTime is provided so implementations can use the timestamp
	// (e.g. to aggregate sub-daily bars into daily bars).
	Tick(ct CandleTime)

	// Trending returns true when the market is in a trending regime and
	// new entries should be allowed. Returns true while not yet ready so
	// warmup bars are not suppressed.
	Trending() bool

	// AllowSide returns true when new entries on the given side are permitted.
	// Trending() == false already blocks all opens; AllowSide provides
	// directional filtering when Trending() == true.
	AllowSide(side Side) bool
}

RegimeFilter classifies the current market as trending or ranging. The bar loop calls Tick() every bar and suppresses new position opens when Trending() returns false.

func GetRegimeFilter

func GetRegimeFilter(cfg RegimeConfig, scale Scale6) (RegimeFilter, error)

GetRegimeFilter constructs a RegimeFilter from cfg. If cfg.Kind is empty, NoopRegime is returned (no filtering).

type Request

type Request struct {
	*TradeCommon
	RequestType
	Price
	Timestamp
	Reason string
	Candle Candle
}

Request represents a trader domain type.

type RequestType

type RequestType uint8

RequestType represents a trader domain type.

const (
	RequestNone RequestType = iota
	RequestMarketOpen
	RequestLimitOpen
	RequestClose
)

func (RequestType) String added in v0.2.3

func (t RequestType) String() string

String is an internal helper for trader type processing.

type RootConfig

type RootConfig struct {
	ConfigPath string
	GlobalPath string
	DBPath     string
	ReportPath string
	DataDir    string

	LogLevel  string
	LogFile   string
	LogFormat string
	NoColor   bool

	// OANDA credentials populated from global config; individual commands
	// may override via their own --token / --account-id / --env flags.
	OANDAToken     string
	OANDAAccountID string
	OANDAEnv       string
}

type RunConfig

type RunConfig struct {
	Name     string         `json:"name"     yaml:"name"`
	Data     DataConfig     `json:"data"     yaml:"data"`
	Strategy StrategyConfig `json:"strategy" yaml:"strategy"`
	Exit     ExitConfig     `json:"exit"     yaml:"exit"`
	Regime   RegimeConfig   `json:"regime"   yaml:"regime"`
}

RunConfig describes a single backtest run: what data to load, which strategy to use, and optional exit and regime-filter overrides.

type RunDefaults

type RunDefaults struct {
	StartingBalance float64 `json:"starting-balance" yaml:"starting-balance"`
	AccountCCY      string  `json:"account-ccy" yaml:"account-ccy"`
	Scale           int64   `json:"scale" yaml:"scale"`
	Strict          bool    `json:"strict" yaml:"strict"`

	RiskPct       float64 `json:"risk-pct" yaml:"risk-pct"`
	StopPips      int32   `json:"stop-pips" yaml:"stop-pips"`
	TakePips      int32   `json:"take-pips" yaml:"take-pips"`
	RR            float64 `json:"rr" yaml:"rr"`
	Units         int32   `json:"units" yaml:"units"`
	SlippagePips  float64 `json:"slippage-pips" yaml:"slippage-pips"`
	MaxSpreadPips float64 `json:"max-spread-pips" yaml:"max-spread-pips"`

	Source string `json:"source" yaml:"source"`
}

RunDefaults holds account-level and execution-cost settings that apply to every run in the config unless overridden at the run level.

type Scale6

type Scale6 int32

Scale6 represents a trader domain type.

type Scale7

type Scale7 int64

Scale7 represents a trader domain type.

type SessionAnalyzer added in v0.2.1

type SessionAnalyzer struct {
	// contains filtered or unexported fields
}

SessionAnalyzer breaks down candle activity and average range by UTC hour. Ranges are stored as Price (scaled int) and converted to pips only at output.

func NewSessionAnalyzer added in v0.2.1

func NewSessionAnalyzer(inst *Instrument) *SessionAnalyzer

NewSessionAnalyzer creates a SessionAnalyzer for the given instrument.

func (*SessionAnalyzer) Name added in v0.2.1

func (a *SessionAnalyzer) Name() string

func (*SessionAnalyzer) Stats added in v0.2.1

func (a *SessionAnalyzer) Stats() []Stat

func (*SessionAnalyzer) Update added in v0.2.1

func (a *SessionAnalyzer) Update(ct *CandleTime)

type SessionFilter added in v0.2.1

type SessionFilter struct {
	// contains filtered or unexported fields
}

SessionFilter is a regime filter that restricts entries to a specified UTC hour window. Bars outside the window return Trending() = false so the strategy skips new opens. Session windows must stay within a single UTC day; overnight windows like 22:00-06:00 are not supported. Trending() returns true before Ready() as a defensive contract, although the main callers already gate on Ready() before consulting the regime state.

Default window: 07:00–17:00 UTC (London open through NY afternoon). Registered in the factory as "session".

func NewSessionFilter added in v0.2.1

func NewSessionFilter(start, end int) (*SessionFilter, error)

func (*SessionFilter) AllowSide added in v0.2.1

func (f *SessionFilter) AllowSide(_ Side) bool

func (*SessionFilter) Name added in v0.2.1

func (f *SessionFilter) Name() string

func (*SessionFilter) Ready added in v0.2.1

func (f *SessionFilter) Ready() bool

func (*SessionFilter) Tick added in v0.2.1

func (f *SessionFilter) Tick(ct CandleTime)

func (*SessionFilter) Trending added in v0.2.1

func (f *SessionFilter) Trending() bool

type Side

type Side int

Side represents a trader domain type.

const (
	Short Side = -1
	Long  Side = 1
)

func (Side) String

func (s Side) String() string

String is an internal helper for trader type processing.

func (Side) Valid added in v0.2.3

func (s Side) Valid() bool

Valid is an internal helper for trader type processing.

type SpreadAnalyzer added in v0.2.1

type SpreadAnalyzer struct {
	// contains filtered or unexported fields
}

SpreadAnalyzer measures the AvgSpread value of each candle. AvgSpread values are stored as Price (scaled int) and converted to pips only at output. Candles with zero AvgSpread are skipped (tick data may not carry spread).

func NewSpreadAnalyzer added in v0.2.1

func NewSpreadAnalyzer(inst *Instrument) *SpreadAnalyzer

NewSpreadAnalyzer creates a SpreadAnalyzer for the given instrument.

func (*SpreadAnalyzer) Name added in v0.2.1

func (a *SpreadAnalyzer) Name() string

func (*SpreadAnalyzer) Stats added in v0.2.1

func (a *SpreadAnalyzer) Stats() []Stat

func (*SpreadAnalyzer) Update added in v0.2.1

func (a *SpreadAnalyzer) Update(ct *CandleTime)

type Stat added in v0.2.1

type Stat struct {
	Name  string
	Value string
	Pips  float64
}

Stat is a single labeled measurement returned by an Analyzer. Pips is the raw pip count when Value is a pip measurement; zero otherwise. Callers can use Pips to convert to a currency amount without re-parsing Value.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store manages candle CSVs and raw tick files under a pair of symmetric directory trees that share a common root:

/srv/trading/data/
├── candles/<provider>/<instrument>/<year>/<month>/<filename>.csv
└── raw/<provider>/<instrument>/<year>/<month>/<day>/<hh>h_ticks.bi5

The "candles" tree is rooted at basedir; the "raw" tree is its sibling (rawRoot = filepath.Dir(basedir) + "/raw"). Providers are source names such as "oanda" or "dukascopy".

Candle filenames embed every identifying dimension so the file is self-describing without its path:

gbpusd-2026-01-h1.csv   (instrument-year-month-tf)
eurusd-2025-08-m1.csv
usdchf-2024-12-d1.csv

Raw tick files follow the Dukascopy bi5 naming convention:

/srv/trading/data/raw/dukascopy/EURUSD/2025/01/02/13h_ticks.bi5

func GetStore

func GetStore() *Store

GetStore returns the global Store. Used by sibling packages (e.g. data/dukascopy) that need direct store access.

func NewStoreAt

func NewStoreAt(basedir string) *Store

NewStoreAt returns a fresh Store rooted at basedir. Useful for tests.

func (Store) Delete

func (s Store) Delete(k Key) error

func (Store) Exists

func (s Store) Exists(key Key) (bool, error)

func (*Store) IsUsableTickFile

func (s *Store) IsUsableTickFile(k Key) bool

func (*Store) OpenTickIterator

func (s *Store) OpenTickIterator(key Key) (iterator[RawTick], error)

func (*Store) PathForAsset

func (s *Store) PathForAsset(k Key) (string, error)

func (*Store) PathForMonthlyCandle added in v0.2.3

func (s *Store) PathForMonthlyCandle(k Key) string

PathForMonthlyCandle returns the file path for a monthly candle CSV.

func (*Store) RawCandlePath added in v0.2.3

func (s *Store) RawCandlePath(k Key) (string, error)

RawCandlePath returns the path for a monthly candle CSV under the raw tree. It mirrors PathForAsset but roots in rawRoot instead of basedir.

func (*Store) ReadCSV

func (store *Store) ReadCSV(key Key) (cs *candleSet, err error)

func (*Store) RelDir

func (s *Store) RelDir(key Key) string

func (*Store) SaveFile

func (s *Store) SaveFile(key Key, r io.ReadCloser) (path string, err error)

func (*Store) WriteCSV

func (s *Store) WriteCSV(cs *candleSet) error

func (*Store) WriteMonthlyCandles

func (s *Store) WriteMonthlyCandles(source, instrument string, tf Timeframe, monthStart time.Time, candles []Candle) error

WriteMonthlyCandles writes a slice of Candle as a monthly CSV file in the canonical trader format. The candles should be dense (one slot per timeframe step within the month); zero-valued candles are treated as gaps.

Source is the data source name (e.g. "oanda", "dukascopy") and ends up in the path: <basedir>/<source>/<instrument>/<year>/<month>/<instr>-<year>-<month>-<tf>.csv

type Strategy

type Strategy interface {
	Name() string
	Reset()
	Ready() bool
	Update(context.Context, *CandleTime, *Backtest) *StrategyPlan

	// StopDescription returns a human-readable description of how this strategy
	// places stops, e.g. "ATR(14)×1.5", "25 pips", or "" if none.
	StopDescription() string
}

Strategy is the single backtest strategy interface used across the repo.

func GetStrategy

func GetStrategy(scfg StrategyConfig) (Strategy, error)

GetStrategy is the public dispatcher used by config-driven backtest setup. It looks the strategy up in the registry; implementations register themselves via init() in their own packages.

type StrategyConfig

type StrategyConfig struct {
	Kind   string         `json:"kind" yaml:"kind"`
	Params map[string]any `json:"params" yaml:"params"`
}

StrategyConfig names the strategy and carries arbitrary key/value parameters that are passed to the strategy constructor at build time.

type StrategyConstructor

type StrategyConstructor func(params map[string]any) (Strategy, error)

StrategyConstructor builds a Strategy from a config's Params map. Each implementation owns its own param parsing.

func LookupStrategy

func LookupStrategy(name string) StrategyConstructor

LookupStrategy returns the constructor registered under name, or nil.

type StrategyPlan

type StrategyPlan struct {
	Opens  []*OpenRequest
	Closes []*CloseRequest
	Cancel []string
	Reason string
}

func DefaultPlan added in v0.2.3

func DefaultPlan() *StrategyPlan

DefaultPlan returns a fresh no-op plan with the default hold reason.

func HoldPlan added in v0.2.3

func HoldPlan(reason string) *StrategyPlan

HoldPlan returns a fresh no-op plan with the provided reason. An empty reason falls back to the default hold reason.

func (*StrategyPlan) Empty added in v0.2.3

func (p *StrategyPlan) Empty() bool

Empty reports whether the plan has no actions to execute.

type SwingAnalyzer added in v0.2.1

type SwingAnalyzer struct {
	// contains filtered or unexported fields
}

SwingAnalyzer measures the high-low range of each candle. Ranges are stored as Price (scaled int) and converted to pips only at output.

func NewSwingAnalyzer added in v0.2.1

func NewSwingAnalyzer(inst *Instrument) *SwingAnalyzer

NewSwingAnalyzer creates a SwingAnalyzer for the given instrument.

func (*SwingAnalyzer) Name added in v0.2.1

func (a *SwingAnalyzer) Name() string

func (*SwingAnalyzer) Stats added in v0.2.1

func (a *SwingAnalyzer) Stats() []Stat

func (*SwingAnalyzer) Update added in v0.2.1

func (a *SwingAnalyzer) Update(ct *CandleTime)

type SyntheticCandleConfig

type SyntheticCandleConfig struct {
	Instrument  string    // e.g., "EURUSD"
	Timeframe   Timeframe // e.g., H1 (hourly)
	StartPrice  Price     // Starting price in scale units
	Volatility  float64   // Volatility as percentage (e.g., 0.005 = 0.5%)
	Trend       float64   // Trend as log return per candle (e.g., 0.0001 = +0.01%)
	Seed        int64     // Random seed for reproducibility
	TicksPerBar int32     // Number of ticks per candle
}

SyntheticCandleConfig holds parameters for generating synthetic candle data.

func DefaultSyntheticConfig

func DefaultSyntheticConfig(instrument string) SyntheticCandleConfig

DefaultSyntheticConfig returns a sensible default configuration for EUR/USD.

func (SyntheticCandleConfig) GenerateSyntheticMonthlyCandles

func (cfg SyntheticCandleConfig) GenerateSyntheticMonthlyCandles(year int, month time.Month) (*candleSet, error)

GenerateSyntheticMonthlyCandles generates a full month of synthetic OHLC data.

func (SyntheticCandleConfig) GenerateSyntheticYearlyAndWrite

func (cfg SyntheticCandleConfig) GenerateSyntheticYearlyAndWrite(store *Store, year int) ([]string, error)

GenerateSyntheticYearlyAndWrite generates a year of synthetic data and writes it to CSV files.

func (SyntheticCandleConfig) GenerateSyntheticYearlyCandles

func (cfg SyntheticCandleConfig) GenerateSyntheticYearlyCandles(year int) ([]*candleSet, error)

GenerateSyntheticYearlyCandles generates a full year of monthly candle sets.

type Tick

type Tick struct {
	Instrument string
	Timestamp  Timestamp
	BA
}

Tick represents a trader domain type.

func (Tick) Mid

func (t Tick) Mid() Price

Mid is an internal helper for trader type processing.

func (Tick) Spread

func (t Tick) Spread() Price

Spread is an internal helper for trader type processing.

func (Tick) Validate added in v0.2.3

func (t Tick) Validate() error

Validate is an internal helper for trader type processing.

type TimeRange

type TimeRange struct {
	Start Timestamp // inclusive
	End   Timestamp // exclusive
	TF    Timeframe // m1, h1, d1
}

TimeRange represents a trader domain type.

func ParseTimeRange added in v0.2.0

func ParseTimeRange(from, to, tf string) (TimeRange, error)

ParseTimeRange parses a TimeRange from "YYYY-MM-DD" from/to strings and a timeframe string ("M1", "H1", "D1"). Exported for use by sibling packages.

func (TimeRange) Contains

func (r TimeRange) Contains(ts Timestamp) bool

Contains is an internal helper for trader type processing.

func (TimeRange) Covers

func (r TimeRange) Covers(other TimeRange) bool

Covers is an internal helper for trader type processing.

func (TimeRange) MonthsInRange

func (r TimeRange) MonthsInRange() []yearMonth

MonthsInRange is an internal helper for trader type processing.

func (TimeRange) Overlaps

func (r TimeRange) Overlaps(other TimeRange) bool

Overlaps is an internal helper for trader type processing.

func (TimeRange) String

func (r TimeRange) String() string

String is an internal helper for trader type processing.

func (TimeRange) Valid

func (r TimeRange) Valid() bool

Valid is an internal helper for trader type processing.

type Timeframe

type Timeframe int64

******************************************************************** Timeframe ********************************************************************

const (
	TF0   Timeframe = 0
	Ticks Timeframe = 1
	M1    Timeframe = 60
	H1    Timeframe = 3600
	H4    Timeframe = 14400
	D1    Timeframe = 86400
)

func ParseTimeframe added in v0.2.3

func ParseTimeframe(s string) (Timeframe, error)

ParseTimeframe parses a timeframe string into its canonical Timeframe value. It accepts common aliases and returns an error for unknown values.

func (Timeframe) String

func (tf Timeframe) String() string

String is an internal helper for trader type processing.

type Timestamp

type Timestamp int64

Timestamp represents a trader domain type.

func FromString

func FromString(s string) Timestamp

FromString is an internal helper for trader type processing.

func FromTime

func FromTime(t time.Time) Timestamp

FromTime is an internal helper for trader type processing.

func ParseDateTimestamp added in v0.2.3

func ParseDateTimestamp(s string) (Timestamp, error)

ParseDateTimestamp parses a YYYY-MM-DD date string into a UTC midnight Timestamp.

func (Timestamp) Add

func (t Timestamp) Add(d time.Duration) Timestamp

Add is an internal helper for trader type processing.

func (Timestamp) After

func (t Timestamp) After(ts Timestamp) bool

After is an internal helper for trader type processing.

func (Timestamp) Before

func (t Timestamp) Before(ts Timestamp) bool

Before is an internal helper for trader type processing.

func (Timestamp) FloorToHour

func (s Timestamp) FloorToHour() Timestamp

FloorToHour is an internal helper for trader type processing.

func (Timestamp) FloorToMinute

func (s Timestamp) FloorToMinute() Timestamp

Flooring (bar opens)

func (Timestamp) Int64

func (t Timestamp) Int64() int64

Int64 is an internal helper for trader type processing.

func (Timestamp) IsZero

func (t Timestamp) IsZero() bool

IsZero is an internal helper for trader type processing.

func (Timestamp) MS

func (s Timestamp) MS() timemilli

MS is an internal helper for trader type processing.

func (Timestamp) Milli

func (t Timestamp) Milli() timemilli

Milli is an internal helper for trader type processing.

func (Timestamp) String

func (t Timestamp) String() string

String is an internal helper for trader type processing.

func (Timestamp) Time

func (t Timestamp) Time() time.Time

Time is an internal helper for trader type processing.

type Trade

type Trade struct {
	*TradeCommon
	EntryPrice Price
	EntryTime  Timestamp
	ExitPrice  Price
	ExitTime   Timestamp
	PNL        Money // account currency (best-effort)
	CloseCause closeCause
}

Trade represents a trader domain type.

func (*Trade) Clone added in v0.2.3

func (t *Trade) Clone() *Trade

Clone is an internal helper for trader type processing.

type TradeCommon

type TradeCommon struct {
	ID         string
	Instrument string
	Side       // Long or Short
	Units
	Stop Price
	Take Price
}

TradeCommon represents a trader domain type.

func (*TradeCommon) Clone added in v0.2.3

func (tc *TradeCommon) Clone() *TradeCommon

Clone is an internal helper for trader type processing.

type TradeHistory

type TradeHistory struct {
	*TradeCommon
	*OpenRequest
}

TradeHistory represents a trader domain type.

func NewTradeHistory

func NewTradeHistory(inst string) *TradeHistory

NewTradeHistory is an internal helper for trader type processing.

type TradeRecord

type TradeRecord struct {
	TradeID    string
	BotID      string // set by the bot manager; empty for backtest/journal-only runs
	Instrument string
	Units      Units
	EntryPrice Price
	ExitPrice  Price
	OpenTime   Timestamp
	CloseTime  Timestamp
	RealizedPL Money
	Reason     string
}

TradeRecord is the canonical persisted representation of a completed trade. It is shared by live journaling, replay/sim journaling, and export formats such as CSV, JSONL, and Org output.

func ReadTradesJSONL added in v0.2.3

func ReadTradesJSONL(path string) ([]TradeRecord, error)

ReadTradesJSONL reads all TradeRecords from a JSONL file. Malformed/invalid lines are silently skipped (forward-compatible with mixed journal data).

type Trader

type Trader struct {
	DataManager CandleSource
	*Broker
	*Store
}

func (*Trader) Backtest

func (t *Trader) Backtest(ctx context.Context, run *Backtest) error

type TraderBacktestExecutor added in v0.2.3

type TraderBacktestExecutor struct {
	DataManager    CandleSource
	BrokerFactory  func() *Broker
	AccountFactory func(name string, balance Money) *Account
}

TraderBacktestExecutor executes a Backtest by wiring it through Trader with factory-provided runtime dependencies.

func NewTraderBacktestExecutor added in v0.2.3

func NewTraderBacktestExecutor(dm CandleSource) *TraderBacktestExecutor

NewTraderBacktestExecutor returns a BacktestExecutor that uses Trader as the concrete execution engine.

func (*TraderBacktestExecutor) Execute added in v0.2.3

func (e *TraderBacktestExecutor) Execute(ctx context.Context, run *Backtest) error

Execute runs one backtest with freshly-constructed runtime dependencies.

type TrendAnalyzer added in v0.2.1

type TrendAnalyzer struct {
	// contains filtered or unexported fields
}

TrendAnalyzer measures the body/range ratio as a proxy for trending vs consolidating bars. ratio = |Close−Open| / (High−Low).

Thresholds: >0.6 → trending; <0.3 → consolidating.

func NewTrendAnalyzer added in v0.2.1

func NewTrendAnalyzer() *TrendAnalyzer

NewTrendAnalyzer creates a TrendAnalyzer.

func (*TrendAnalyzer) Name added in v0.2.1

func (a *TrendAnalyzer) Name() string

func (*TrendAnalyzer) Stats added in v0.2.1

func (a *TrendAnalyzer) Stats() []Stat

func (*TrendAnalyzer) Update added in v0.2.1

func (a *TrendAnalyzer) Update(ct *CandleTime)

type Units

type Units int64

Units represents a trader domain type.

func UnitsFromFloat added in v0.2.3

func UnitsFromFloat(f float64) Units

UnitsFromFloat converts a float64 multiplier to a fixed-point Units value.

func (Units) Float64 added in v0.2.3

func (u Units) Float64() float64

Float64 converts a fixed-point Units multiplier back to float64. Use only at output boundaries (display, broker API).

func (Units) Int64

func (u Units) Int64() int64

Int64 is an internal helper for trader type processing.

func (Units) String

func (u Units) String() string

String is an internal helper for trader type processing.

type Want

type Want struct {
	Key
	WantReason
}

type WantReason

type WantReason string
const (
	WantMissing    WantReason = "missing"
	WantIncomplete WantReason = "incomplete"
	WantStale      WantReason = "stale"
)

func (WantReason) Valid added in v0.2.3

func (wr WantReason) Valid() bool

type Wantlist

type Wantlist struct {
	// contains filtered or unexported fields
}

func NewWantlist

func NewWantlist() *Wantlist

func (*Wantlist) Delete

func (wl *Wantlist) Delete(key Key)

func (*Wantlist) Get

func (wl *Wantlist) Get(key Key) (Want, bool)

func (*Wantlist) Has

func (wl *Wantlist) Has(key Key) bool

func (*Wantlist) Keys

func (wl *Wantlist) Keys() []Key

func (*Wantlist) Len

func (wl *Wantlist) Len() int

func (*Wantlist) List

func (wl *Wantlist) List() []Want

func (*Wantlist) Put

func (wl *Wantlist) Put(w Want)

func (*Wantlist) PutKey added in v0.2.3

func (wl *Wantlist) PutKey(key Key, reason WantReason)

func (*Wantlist) Range added in v0.2.3

func (wl *Wantlist) Range(fn func(Key, Want) bool)

func (*Wantlist) Update

func (wl *Wantlist) Update(key Key, fn func(*Want) error) error

type WeeklyEMAFilter added in v0.2.1

type WeeklyEMAFilter struct {
	// contains filtered or unexported fields
}

WeeklyEMAFilter is a directional regime filter that aggregates sub-daily bars into ISO weekly bars and runs an EMA(period) over weekly closes.

Trending() always returns true — this is a direction-only filter. AllowSide(Long) returns true when the current week's in-progress close is above the EMA computed from completed weekly closes. AllowSide(Short) returns true when that in-progress close is below the EMA, so directional permission can change within a week as the partial weekly close moves.

During warmup (EMA not yet ready) AllowSide returns true as a defensive contract so no entries are suppressed before enough weekly data has accumulated, although the main callers already gate on Ready() before consulting directional permission.

Registered in the factory as "weekly-ema".

func NewWeeklyEMAFilter added in v0.2.1

func NewWeeklyEMAFilter(period int, scale Scale6) (*WeeklyEMAFilter, error)

func (*WeeklyEMAFilter) AllowSide added in v0.2.1

func (f *WeeklyEMAFilter) AllowSide(side Side) bool

func (*WeeklyEMAFilter) EMA added in v0.2.3

func (f *WeeklyEMAFilter) EMA() float64

EMA exposes the current weekly EMA value for debugging.

func (*WeeklyEMAFilter) EMAValue added in v0.2.1

func (f *WeeklyEMAFilter) EMAValue() float64

EMAValue exposes the current EMA value for debugging.

func (*WeeklyEMAFilter) Name added in v0.2.1

func (f *WeeklyEMAFilter) Name() string

func (*WeeklyEMAFilter) Ready added in v0.2.1

func (f *WeeklyEMAFilter) Ready() bool

func (*WeeklyEMAFilter) Tick added in v0.2.1

func (f *WeeklyEMAFilter) Tick(ct CandleTime)

func (*WeeklyEMAFilter) Trending added in v0.2.1

func (f *WeeklyEMAFilter) Trending() bool

Trending always returns true; direction is enforced via AllowSide.

Source Files

Directories

Path Synopsis
api
mcp
Package mcp implements an MCP (Model Context Protocol) server over the service layer.
Package mcp implements an MCP (Model Context Protocol) server over the service layer.
rest
Package rest is the HTTP presentation layer over the service package.
Package rest is the HTTP presentation layer over the service package.
brokers
sim
cmd
api
Package api hosts the CLI command for starting the REST API server.
Package api hosts the CLI command for starting the REST API server.
bot
Package bot hosts CLI subcommands for managing live strategy bots running inside a trader serve process.
Package bot hosts CLI subcommands for managing live strategy bots running inside a trader serve process.
gen-newsdays command
gen-newsdays generates a news-days file for use with the donchian-v5/v6 strategies (news_days_file param).
gen-newsdays generates a news-days file for use with the donchian-v5/v6 strategies (news_days_file param).
gen-testdata command
health
Package health provides CLI commands that query the trader serve REST API for health and version information.
Package health provides CLI commands that query the trader serve REST API for health and version information.
live
Package live hosts CLI commands for the live trading subsystem.
Package live hosts CLI commands for the live trading subsystem.
mcp
Package mcp hosts the CLI command for starting the MCP server.
Package mcp hosts the CLI command for starting the MCP server.
order
Package order hosts CLI subcommands for live order management.
Package order hosts CLI subcommands for live order management.
serve
Package serve implements "trader serve" — the long-running daemon mode.
Package serve implements "trader serve" — the long-running daemon mode.
Package data defines the Provider interface implemented by every market-data source (Dukascopy, OANDA, future Polygon/IBKR, etc.).
Package data defines the Provider interface implemented by every market-data source (Dukascopy, OANDA, future Polygon/IBKR, etc.).
dukascopy
Package dukascopy implements the data.Provider interface for Dukascopy historical tick files.
Package dukascopy implements the data.Provider interface for Dukascopy historical tick files.
Package service is the protocol-agnostic business-logic layer.
Package service is the protocol-agnostic business-logic layer.
strategies
bollingerfade
Package bollingerfade implements a Bollinger Band mean-reversion strategy.
Package bollingerfade implements a Bollinger Band mean-reversion strategy.
donchian
Package donchian implements the Donchian breakout strategy with close-strength confirmation.
Package donchian implements the Donchian breakout strategy with close-strength confirmation.
donchianv2
Package donchianv2 is Donchian breakout v2: adds a consecutive-close confirmation filter (confirm_bars, default 2) on top of the v1 close-strength filter.
Package donchianv2 is Donchian breakout v2: adds a consecutive-close confirmation filter (confirm_bars, default 2) on top of the v1 close-strength filter.
donchianv3
Package donchianv3 is Donchian breakout v3: adds a same-day re-entry block on top of the v2 consecutive-close confirmation filter.
Package donchianv3 is Donchian breakout v3: adds a same-day re-entry block on top of the v2 consecutive-close confirmation filter.
donchianv4
Package donchianv4 is Donchian breakout v4: adds an ADX directional-strength gate on top of the v2 consecutive-close confirmation filter.
Package donchianv4 is Donchian breakout v4: adds an ADX directional-strength gate on top of the v2 consecutive-close confirmation filter.
donchianv5
Package donchianv5 is Donchian breakout v5: adds a high-impact news-day filter on top of the v4 ADX directional-strength gate.
Package donchianv5 is Donchian breakout v5: adds a high-impact news-day filter on top of the v4 ADX directional-strength gate.
donchianv6
Package donchianv6 is Donchian breakout v6: adds a Monday/week-open entry block on top of the v5 news-day filter.
Package donchianv6 is Donchian breakout v6: adds a Monday/week-open entry block on top of the v5 news-day filter.
emacross
Package emacross implements the fast/slow EMA crossover strategy.
Package emacross implements the fast/slow EMA crossover strategy.
emacrossadx
Package emacrossadx implements the EMA-cross strategy with an ADX trend-strength gate.
Package emacrossadx implements the EMA-cross strategy with an ADX trend-strength gate.
fake
Package fake contains canned deterministic strategies used by trader's integration and lifecycle tests.
Package fake contains canned deterministic strategies used by trader's integration and lifecycle tests.
lifecycle
Package lifecycle is a deterministic canned strategy used to regression-test the full config→candles→strategy→Trader→Broker→Account→Trades→Result pipeline.
Package lifecycle is a deterministic canned strategy used to regression-test the full config→candles→strategy→Trader→Broker→Account→Trades→Result pipeline.
noop
Package noop implements a do-nothing strategy.
Package noop implements a do-nothing strategy.
pulse
Package pulse provides a mechanical live-trading strategy that opens and closes positions on a fixed schedule.
Package pulse provides a mechanical live-trading strategy that opens and closes positions on a fixed schedule.
scalper
Package scalper implements a "buy the dip" M1 scalper for live broker integration testing and incremental strategy development.
Package scalper implements a "buy the dip" M1 scalper for live broker integration testing and incremental strategy development.
stress
Package stress implements an unconditional mechanical strategy that opens a trade every N candles with no indicator warmup.
Package stress implements an unconditional mechanical strategy that opens a trade every N candles with no indicator warmup.
tmpl
Package tmpl is a strategy template / starting point for new strategy implementations.
Package tmpl is a strategy template / starting point for new strategy implementations.
Package ui exposes the compiled SvelteKit front-end as an embed.FS.
Package ui exposes the compiled SvelteKit front-end as an embed.FS.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL