Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ to a paid plan at:
| Macro Data Access | pandas-datareader | Local script | — |
| Macro Carry Scanner | CCXT | Local script | — |
| Carry Rebalance Bot | Blankly | Local strategy | — |
| Release-Aware EUR/USD Backtest | Julia + Fastback.jl | Local script | — |

## Distribution-first publishing loop

Expand Down Expand Up @@ -166,8 +167,12 @@ cd pandas-datareader && pip install -r requirements.txt && python example.py
# VectorBT Jupyter notebook
cd vectorbt && pip install -r requirements.txt && jupyter notebook fxmacrodata_vectorbt.ipynb

# CCXT macro scanner
cd ccxt && pip install -r requirements.txt && python example.py
# CCXT macro scanner
cd ccxt && pip install -r requirements.txt && python example.py

# Julia + Fastback.jl release-aware EUR/USD backtest
cd julia && julia --project -e 'using Pkg; Pkg.instantiate()'
julia --project fastback_release_aware_backtest.jl

# Next.js app
cd vercel && npm install && npm run dev
Expand Down
16 changes: 16 additions & 0 deletions julia/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name = "FXMacroDataFastbackExample"
uuid = "3cd9b1af-5e7b-428e-b2db-e5f38b2b5d83"
authors = ["FXMacroData <[email protected]>"]
version = "0.1.0"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Fastback = "2b92286b-cbc8-46f1-be48-a629b4baefca"
FXMacroData = "c3744e4c-d024-465f-a33d-892fc1481992"

[sources]
FXMacroData = {url = "https://github.com/fxmacrodata/FXMacroData.jl"}

[compat]
Fastback = "0.9"
julia = "1.9"
25 changes: 25 additions & 0 deletions julia/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# FXMacroData + Fastback.jl

This example runs a release-aware EUR/USD backtest with [Fastback.jl](https://github.com/rbeeli/Fastback.jl) and the FXMacroData Julia client. It uses the source timestamp of every economic release and revision, so a value is eligible for a simulated decision only after it was published.

The strategy is deliberately simple: it changes a simulated EUR/USD position when a newly available USD policy-rate observation differs from the preceding one. It is a research example, not investment advice or a live-trading system.

## Run it

Install Julia 1.9 or later, then run:

```bash
cd julia
julia --project -e 'using Pkg; Pkg.instantiate()'
julia --project fastback_release_aware_backtest.jl
```

Set `FXMACRODATA_API_KEY` (or `FXMD_API_KEY`) in your shell before running the example. The client reads the key at runtime and sends it only as an `api_key` query parameter; do not put keys in `Project.toml`, scripts, or notebooks.

## How it avoids look-ahead bias

- The client requests `revisions=all`, preserving the publication time of each initial result and later revision.
- The loop applies a release only when its timestamp is strictly earlier than the daily valuation timestamp, so same-day daily bars cannot use a result published later that day.
- It consumes FXMacroData's daily reference rates and processes orders only inside Fastback's simulated account.

For the API client source and endpoint coverage, see [FXMacroData.jl](https://github.com/fxmacrodata/FXMacroData.jl). Explore the [FXMacroData API documentation](https://fxmacrodata.com/documentation) or [subscribe for protected datasets](https://fxmacrodata.com/subscribe).
139 changes: 139 additions & 0 deletions julia/fastback_release_aware_backtest.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
using Dates
using Fastback
using FXMacroData

const START_DATE = Date(2024, 1, 1)
const END_DATE = Date(2025, 12, 31)
const POSITION_SIZE_EUR = 10_000.0

"""Return the first non-empty value for any of `keys` in an API row."""
function row_value(row, keys)
for key in keys
value = get(row, key, nothing)
if value !== nothing && value != ""
return value
end
end
return nothing
end

"""Parse a daily observation date from an FXMacroData row."""
function observation_date(row)
value = row_value(row, ("date", "observation_date", "timestamp"))
value === nothing && throw(ArgumentError("FX row does not contain a date"))
return Date(first(split(String(value), 'T')))
end

"""Parse an announcement or revision timestamp from an FXMacroData row."""
function publication_time(row)
value = row_value(row, ("announcement_datetime", "published_at", "release_datetime"))
value === nothing && throw(ArgumentError("Release row does not contain a publication timestamp"))
return DateTime(replace(String(value), r"Z$" => ""))
end

"""Parse a numeric field while preserving API values represented as JSON strings."""
function numeric_value(value)
value isa Number && return Float64(value)
return parse(Float64, String(value))
end

"""Normalise API release rows into timestamp-ordered values, including revisions."""
function policy_rate_events(rows)
events = NamedTuple{(:released_at, :value),Tuple{DateTime,Float64}}[]
for row in rows
released_at = row_value(row, ("announcement_datetime", "published_at", "release_datetime"))
actual = row_value(row, ("actual", "value", "val"))
released_at === nothing && continue
actual === nothing && continue
push!(events, (released_at=publication_time(row), value=numeric_value(actual)))
end
sort!(events; by=event -> event.released_at)
return events
end

"""Normalise daily FX rows into date-ordered EUR/USD valuation bars."""
function fx_bars(rows)
bars = NamedTuple{(:date, :price),Tuple{Date,Float64}}[]
for row in rows
price = row_value(row, ("val", "value", "rate", "close"))
price === nothing && continue
push!(bars, (date=observation_date(row), price=numeric_value(price)))
end
sort!(bars; by=bar -> bar.date)
return bars
end

"""Run the release-aware EUR/USD policy-rate demonstration in a Fastback account."""
function run_backtest(; start_date=START_DATE, end_date=END_DATE)
client = Client()
release_rows = announcements(
client,
"usd",
"policy_rate";
start_date=start_date,
end_date=end_date,
revisions="all",
)
price_rows = forex(client, "eur", "usd"; start_date=start_date, end_date=end_date)

events = policy_rate_events(release_rows)
bars = fx_bars(price_rows)
isempty(events) && throw(ArgumentError("No policy-rate releases returned for the selected period"))
isempty(bars) && throw(ArgumentError("No EUR/USD observations returned for the selected period"))

account = Account(
;
funding=AccountFunding.Margined,
base_currency=CashSpec(:USD),
broker=FlatFeeBroker(; pct=0.0002),
)
usd = cash_asset(account, :USD)
deposit!(account, :USD, 100_000.0)
eurusd = register_instrument!(account, spot_instrument(Symbol("EUR/USD"), :EUR, :USD))
collect_equity, equity_history = periodic_collector(Float64, Day(1))

event_index = 1
active_policy_rate = nothing
preceding_policy_rate = nothing
held_quantity = 0.0

for bar in bars
valuation_time = DateTime(bar.date)
while event_index <= length(events) && events[event_index].released_at < valuation_time
preceding_policy_rate = active_policy_rate
active_policy_rate = events[event_index].value
event_index += 1
end

if active_policy_rate !== nothing && preceding_policy_rate !== nothing
desired_quantity = active_policy_rate > preceding_policy_rate ? POSITION_SIZE_EUR : -POSITION_SIZE_EUR
if desired_quantity != held_quantity
delta = desired_quantity - held_quantity
order = Order(oid!(account), eurusd, valuation_time, bar.price, delta)
fill_order!(
account,
order;
dt=valuation_time,
fill_price=bar.price,
bid=bar.price,
ask=bar.price,
last=bar.price,
)
held_quantity = desired_quantity
end
end

update_marks!(account, eurusd, valuation_time, bar.price, bar.price, bar.price)
if should_collect(equity_history, valuation_time)
collect_equity(valuation_time, equity(account, usd))
end
end

return (account=account, equity_history=equity_history, releases_processed=event_index - 1)
end

if abspath(PROGRAM_FILE) == @__FILE__
result = run_backtest()
println("Processed $(result.releases_processed) policy-rate releases.")
println("Final simulated equity: $(equity(result.account, cash_asset(result.account, :USD))) USD")
end