fugazi 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fugazi-0.1.1/.github/workflows/ci.yml +54 -0
- fugazi-0.1.1/.github/workflows/release.yml +91 -0
- fugazi-0.1.1/.gitignore +8 -0
- fugazi-0.1.1/CLAUDE.md +89 -0
- fugazi-0.1.1/Cargo.lock +137 -0
- fugazi-0.1.1/Cargo.toml +23 -0
- fugazi-0.1.1/LICENSE +21 -0
- fugazi-0.1.1/PKG-INFO +282 -0
- fugazi-0.1.1/README.md +315 -0
- fugazi-0.1.1/examples/backtest.rs +127 -0
- fugazi-0.1.1/examples/candle_signal.rs +40 -0
- fugazi-0.1.1/examples/multi_output.rs +76 -0
- fugazi-0.1.1/examples/pairs.rs +149 -0
- fugazi-0.1.1/examples/strategy.rs +120 -0
- fugazi-0.1.1/examples/streaming.rs +39 -0
- fugazi-0.1.1/pyproject.toml +29 -0
- fugazi-0.1.1/python/.gitignore +9 -0
- fugazi-0.1.1/python/Cargo.toml +19 -0
- fugazi-0.1.1/python/README.md +263 -0
- fugazi-0.1.1/python/src/lib.rs +1885 -0
- fugazi-0.1.1/python/tests/test_fugazi.py +502 -0
- fugazi-0.1.1/python/tests/test_readme.py +86 -0
- fugazi-0.1.1/python/uv.lock +694 -0
- fugazi-0.1.1/src/.keep +0 -0
- fugazi-0.1.1/src/indicator.rs +40 -0
- fugazi-0.1.1/src/indicators/ad.rs +75 -0
- fugazi-0.1.1/src/indicators/adx.rs +133 -0
- fugazi-0.1.1/src/indicators/aroon.rs +159 -0
- fugazi-0.1.1/src/indicators/atr.rs +72 -0
- fugazi-0.1.1/src/indicators/bollinger.rs +146 -0
- fugazi-0.1.1/src/indicators/candle.rs +198 -0
- fugazi-0.1.1/src/indicators/cci.rs +93 -0
- fugazi-0.1.1/src/indicators/compare.rs +209 -0
- fugazi-0.1.1/src/indicators/component.rs +127 -0
- fugazi-0.1.1/src/indicators/dmi.rs +149 -0
- fugazi-0.1.1/src/indicators/donchian.rs +148 -0
- fugazi-0.1.1/src/indicators/ema.rs +80 -0
- fugazi-0.1.1/src/indicators/ext.rs +319 -0
- fugazi-0.1.1/src/indicators/hma.rs +88 -0
- fugazi-0.1.1/src/indicators/identity.rs +37 -0
- fugazi-0.1.1/src/indicators/keltner.rs +150 -0
- fugazi-0.1.1/src/indicators/logic.rs +179 -0
- fugazi-0.1.1/src/indicators/macd.rs +151 -0
- fugazi-0.1.1/src/indicators/mfi.rs +123 -0
- fugazi-0.1.1/src/indicators/mod.rs +90 -0
- fugazi-0.1.1/src/indicators/obv.rs +73 -0
- fugazi-0.1.1/src/indicators/ops.rs +427 -0
- fugazi-0.1.1/src/indicators/rma.rs +72 -0
- fugazi-0.1.1/src/indicators/rsi.rs +114 -0
- fugazi-0.1.1/src/indicators/sar.rs +210 -0
- fugazi-0.1.1/src/indicators/sma.rs +71 -0
- fugazi-0.1.1/src/indicators/smoothing.rs +90 -0
- fugazi-0.1.1/src/indicators/stats.rs +233 -0
- fugazi-0.1.1/src/indicators/stddev.rs +78 -0
- fugazi-0.1.1/src/indicators/stochastic.rs +96 -0
- fugazi-0.1.1/src/indicators/true_range.rs +49 -0
- fugazi-0.1.1/src/indicators/value.rs +45 -0
- fugazi-0.1.1/src/indicators/vwap.rs +75 -0
- fugazi-0.1.1/src/indicators/williams_r.rs +90 -0
- fugazi-0.1.1/src/indicators/wma.rs +74 -0
- fugazi-0.1.1/src/lib.rs +85 -0
- fugazi-0.1.1/src/signal.rs +27 -0
- fugazi-0.1.1/src/strategies/composite.rs +65 -0
- fugazi-0.1.1/src/strategies/mean_reversion.rs +144 -0
- fugazi-0.1.1/src/strategies/mod.rs +59 -0
- fugazi-0.1.1/src/strategies/momentum.rs +30 -0
- fugazi-0.1.1/src/strategies/single_asset.rs +134 -0
- fugazi-0.1.1/src/strategies/trend.rs +101 -0
- fugazi-0.1.1/src/strategies/volume.rs +37 -0
- fugazi-0.1.1/src/strategy.rs +697 -0
- fugazi-0.1.1/src/types.rs +43 -0
- fugazi-0.1.1/tests/composition.rs +60 -0
- fugazi-0.1.1/tests/data/README.md +60 -0
- fugazi-0.1.1/tests/data/aapl_monthly.csv +121 -0
- fugazi-0.1.1/tests/examples_compile.rs +43 -0
- fugazi-0.1.1/tests/strategies.rs +205 -0
- fugazi-0.1.1/tests/talib_validation.rs +335 -0
- fugazi-0.1.1/tools/environment.yml +15 -0
- fugazi-0.1.1/tools/gen_talib_fixtures.py +149 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
rust:
|
|
18
|
+
name: Rust (test + clippy)
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
23
|
+
with:
|
|
24
|
+
components: clippy
|
|
25
|
+
- uses: Swatinem/rust-cache@v2
|
|
26
|
+
# Scope to the core crate: the `fugazi-python` member links libpython via
|
|
27
|
+
# pyo3's extension-module feature and can't be `cargo test`ed standalone.
|
|
28
|
+
# Its Rust + Python surface is exercised by the `python` job below.
|
|
29
|
+
- name: Test
|
|
30
|
+
run: cargo test -p fugazi
|
|
31
|
+
- name: Clippy
|
|
32
|
+
run: cargo clippy -p fugazi --all-targets -- -D warnings
|
|
33
|
+
|
|
34
|
+
python:
|
|
35
|
+
name: Python bindings (pytest)
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
- uses: actions/setup-python@v5
|
|
40
|
+
with:
|
|
41
|
+
python-version: "3.13"
|
|
42
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
43
|
+
- uses: Swatinem/rust-cache@v2
|
|
44
|
+
- name: Build + install wheel
|
|
45
|
+
working-directory: python
|
|
46
|
+
run: |
|
|
47
|
+
pip install maturin
|
|
48
|
+
maturin build --release --out dist
|
|
49
|
+
pip install --no-index --find-links dist fugazi
|
|
50
|
+
- name: Install test deps
|
|
51
|
+
run: pip install pytest numpy pandas polars
|
|
52
|
+
- name: pytest
|
|
53
|
+
working-directory: python
|
|
54
|
+
run: pytest
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes on a version tag, e.g. `git tag v0.1.0 && git push origin v0.1.0`.
|
|
4
|
+
# Builds multi-platform wheels + sdist, then publishes to PyPI (Trusted
|
|
5
|
+
# Publishing / OIDC, no token) and the core crate to crates.io (token secret).
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- "v*"
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
wheels:
|
|
17
|
+
name: wheels (${{ matrix.platform.runner }} ${{ matrix.platform.target }})
|
|
18
|
+
runs-on: ${{ matrix.platform.runner }}
|
|
19
|
+
timeout-minutes: 30
|
|
20
|
+
strategy:
|
|
21
|
+
fail-fast: false
|
|
22
|
+
matrix:
|
|
23
|
+
platform:
|
|
24
|
+
- { runner: ubuntu-22.04, target: x86_64, manylinux: auto }
|
|
25
|
+
- { runner: ubuntu-22.04, target: aarch64, manylinux: auto }
|
|
26
|
+
# Both macOS arches build on the Apple-Silicon runner; the x86_64
|
|
27
|
+
# wheel is cross-compiled. The Intel macos-13 runner is scarce and
|
|
28
|
+
# being deprecated — it routinely queued 10+ min while this leg ran.
|
|
29
|
+
- { runner: macos-14, target: x86_64 }
|
|
30
|
+
- { runner: macos-14, target: aarch64 }
|
|
31
|
+
- { runner: windows-latest, target: x64 }
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
- name: Build wheel
|
|
35
|
+
uses: PyO3/maturin-action@v1
|
|
36
|
+
with:
|
|
37
|
+
working-directory: python
|
|
38
|
+
target: ${{ matrix.platform.target }}
|
|
39
|
+
args: --release --out dist
|
|
40
|
+
manylinux: ${{ matrix.platform.manylinux }}
|
|
41
|
+
sccache: "true"
|
|
42
|
+
- uses: actions/upload-artifact@v4
|
|
43
|
+
with:
|
|
44
|
+
name: dist-${{ matrix.platform.runner }}-${{ matrix.platform.target }}
|
|
45
|
+
path: python/dist
|
|
46
|
+
|
|
47
|
+
sdist:
|
|
48
|
+
name: sdist
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
steps:
|
|
51
|
+
- uses: actions/checkout@v4
|
|
52
|
+
- name: Build sdist
|
|
53
|
+
uses: PyO3/maturin-action@v1
|
|
54
|
+
with:
|
|
55
|
+
working-directory: python
|
|
56
|
+
command: sdist
|
|
57
|
+
args: --out dist
|
|
58
|
+
- uses: actions/upload-artifact@v4
|
|
59
|
+
with:
|
|
60
|
+
name: dist-sdist
|
|
61
|
+
path: python/dist
|
|
62
|
+
|
|
63
|
+
publish-pypi:
|
|
64
|
+
name: Publish to PyPI
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
needs: [wheels, sdist]
|
|
67
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
68
|
+
environment: pypi
|
|
69
|
+
permissions:
|
|
70
|
+
id-token: write # Trusted Publishing (OIDC)
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v4
|
|
73
|
+
with:
|
|
74
|
+
pattern: dist-*
|
|
75
|
+
path: dist
|
|
76
|
+
merge-multiple: true
|
|
77
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
78
|
+
with:
|
|
79
|
+
packages-dir: dist
|
|
80
|
+
|
|
81
|
+
publish-crates:
|
|
82
|
+
name: Publish to crates.io
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
88
|
+
- name: cargo publish
|
|
89
|
+
run: cargo publish -p fugazi
|
|
90
|
+
env:
|
|
91
|
+
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
fugazi-0.1.1/.gitignore
ADDED
fugazi-0.1.1/CLAUDE.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this is
|
|
6
|
+
|
|
7
|
+
`fugazi` is a Rust library (edition 2024, no external dependencies) of **incremental** technical-analysis primitives. Every primitive owns its internal state and is advanced one sample at a time via `update()`, carrying just enough intermediate state to produce the next output in ~O(1). The same code therefore serves both live streaming and batch backtesting.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
- Build: `cargo build`
|
|
12
|
+
- Test (unit + integration + doctests): `cargo test`
|
|
13
|
+
- Single test by name: `cargo test warms_up_then_averages`
|
|
14
|
+
- All tests in one module: `cargo test indicators::rsi`
|
|
15
|
+
- One integration-test file: `cargo test --test composition`
|
|
16
|
+
- Lint (keep clean): `cargo clippy --all-targets`
|
|
17
|
+
- API docs: `cargo doc --open`
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
Three composable layers: indicators (numeric sources), signals (boolean-valued indicators — `Indicator<Output = bool>`), and strategies (the decision layer that trades into a wallet).
|
|
22
|
+
|
|
23
|
+
### Indicators = the numeric *sources* (`src/indicator.rs`, `src/indicators/`)
|
|
24
|
+
|
|
25
|
+
`Indicator` has associated `Input`/`Output`, `update(&mut self, Input) -> Option<Output>`, `value()` (the latest output, matching the public `value` field), `is_ready()`, `reset()`. Output is `Option` because most indicators need a warm-up (`None` until ready).
|
|
26
|
+
|
|
27
|
+
The defining design choice: **price-series indicators own their input source** and are generic over it — `Ema<S>`, `Sma<S>`, `Rma<S>`, `Rsi<S>`, `Macd<S>` where `S: Indicator<Output = Real>`, with `Input = S::Input`. So composition is just nesting constructors:
|
|
28
|
+
|
|
29
|
+
```rust
|
|
30
|
+
Ema::new(Current::close(), 20) // EMA-20 of the close (Input = Candle)
|
|
31
|
+
Ema::new(Sma::new(src, 10), 20) // EMA of an SMA
|
|
32
|
+
Rsi::new(Identity::new(), 14) // RSI of a raw Real stream
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
There is **no pipe/`then`/`Chain`** — chaining *is* construction.
|
|
36
|
+
|
|
37
|
+
- **Leaf sources** terminate the chain: `Value` (constant), `Identity` (raw `Real` passthrough), and the candle accessors under `Current` (`Current::close()`, `Current::volume()`, …; built on the `Field<F>` / `CandleField` carrier in `candle.rs`).
|
|
38
|
+
- **Bar indicators** consume the whole `Candle` directly (they are not "of a price"): `Atr`, `Adx`, `TrueRange`, and the volume indicators `Obv`, `Vwap`, `Ad` (Chaikin A/D line), `Mfi` (money-flow index). These take only parameters, or none, e.g. `Atr::new(14)`, `Obv::new()`, `Mfi::new(14)`. The cumulative ones (`Obv`/`Vwap`/`Ad`) anchor at construction; `reset()` re-anchors — e.g. at a session boundary for `Vwap`.
|
|
39
|
+
- **Two-source indicators**: `Donchian<H, L>` takes a high source and a low source, e.g. `Donchian::new(Current::high(), Current::low(), 20)`.
|
|
40
|
+
- `Real = f64` and `Candle` (OHLCV) live in `src/types.rs`.
|
|
41
|
+
- Multi-output indicators (`Macd`, `Adx`, `Bollinger`, `Donchian`, `Keltner`, `Aroon`, `Dmi`) expose one named field per output and set `Output` to a small `Copy` struct (`MacdValue`, `AdxValue`, …); single-output ones expose `value: Option<Real>`. Each also has a **component accessor per output** (`macd.line()`/`.signal()`/`.histogram()`, `bands.upper()`/`.middle()`/`.lower()`, `adx.adx()`, `dmi.plus_di()`, …) returning a `Component<Self>` — a single field projected back into an `Indicator<Output = Real>`, so one output of a struct-valued indicator composes and compares like any other source: `macd.line().crosses_above(macd.signal())`, `Current::close().gt(bands.upper())`. Each accessor **clones** the source (one independently-advanced instance per component, like `crosses_above`'s operand clone). The `Component` carrier (`indicators/component.rs`) holds the source plus a `fn(Output) -> Real` selector — one generic carrier, no per-field marker types.
|
|
42
|
+
- `StochRsi<S>` is a type alias for `Stochastic<Rsi<S>>` — StochRSI is just the stochastic transform over an RSI source: `Stochastic::new(Rsi::new(src, 14), 14)`.
|
|
43
|
+
|
|
44
|
+
### Signals = boolean-valued *indicators* (`src/signal.rs`, `src/indicators/{compare,logic,ext}.rs`)
|
|
45
|
+
|
|
46
|
+
A **signal is just an `Indicator<Output = bool>`** — there is no second trait hierarchy. `Signal` is a thin marker, `trait Signal: Indicator<Input = Candle, Output = bool>` (blanket-impl'd, `?Sized`), naming "a boolean condition over a `Candle`" so a strategy can hold one as `Box<dyn Signal>`. Like any indicator a signal is `None` until warmed; read it as a plain `bool` (false until ready) with `BoolIndicatorExt::is_true` (= `value().unwrap_or(false)`). There is no `src/signals/` module — the comparison/logic/ext pieces live under `indicators/` and are imported from there.
|
|
47
|
+
|
|
48
|
+
- **Comparisons are bool-output binary ops** over two real sources: aliases `Gt`/`Lt`/`Ge`/`Le`/`Eq`/`Ne` for `Combine<L, R, GtOp>` etc. (`indicators/compare.rs`). The op is **value-carrying** — each holds an absolute `epsilon` (default `DEFAULT_EPSILON = 1e-8`); the fluent `.gt()`/`.lt()`/… builders use the default, `Gt::with_epsilon(a, b, eps)` overrides.
|
|
49
|
+
- **Boolean logic** (`indicators/logic.rs`): `And`/`Or`/`Xor` are `Combine<L, R, AndOp>` etc. over two bool sources; `Not` and `Change` are dedicated unary bool-output carriers; `Const<In>` is the constant-bool leaf (the twin of `Value`).
|
|
50
|
+
- `IndicatorExt` (blanket over every `Real`-output indicator, in `indicators/ext.rs`) is the fluent builder for **operators only** — comparisons (`gt`/`lt`/`ge`/`le`/`eq`/`ne`, `above`/`below`), arithmetic (`add`/`sub`/`mul`/`div`), lookback (`lag`/`diff`/`ratio`/`roc`), rolling extremum (`rolling_max`/`rolling_min`), and the composed `crosses_above`/`crosses_below`. Named indicators (`Sma`, `Bollinger`, …) are **not** builder methods; construct via their own `::new`. Do not add `.sma()`/`.bollinger()`-style builders.
|
|
51
|
+
- `BoolIndicatorExt` (blanket over every `Indicator<Output = bool>`, `?Sized` so it works on `Box<dyn Signal>`) adds the bool view `is_true()` and the combinators `and`/`or`/`xor`/`not` + the single edge primitive `changed` (a `Change` toggle detector).
|
|
52
|
+
- **A crossover is not a primitive**: `crosses_above(a,b)` expands to `a.gt(b).and(a.gt(b).changed())` — "comparison is true *and* it just changed". (Clones the operands, so ~2× the source work.)
|
|
53
|
+
|
|
54
|
+
### Strategies = the decision layer (`src/strategy.rs`)
|
|
55
|
+
|
|
56
|
+
Unlike the pure layers below it, a strategy **acts**, in two phases: `Strategy` has `update(&mut self, Input)` (advance its signals/indicators — touches only `&mut self`, so updates across strategies are independent and parallelizable), `trade(&self, &mut dyn Wallet<Symbol>)` (read that state and open/adjust/close positions — `&self`, *price-free*; trades against a shared wallet are serial since sizing resolves against its running state), and `reset()` (associated `Input`/`Symbol`). A driver does, each bar: feed the wallet its prices, `update` every strategy, then `trade` each. There is deliberately **no one-shot `evaluate`**. Almost every classical single-asset strategy shares one shape — a long/flat/short position driven by a few boolean signals, sized all-in — so the crate ships **`SingleAssetStrategy<Sym>`** (`strategies/single_asset.rs`): a concrete `Strategy` (Input = Candle) holding four `Box<dyn Signal>` slots (open/close long, open/close short, each defaulting to constant-false) and built with three builders — `long_on(enter, exit)`, `short_on(enter, exit)` (chainable; an opposite-side entry reverses an open position) and the `buy_and_hold(symbol)` constructor. Its `trade` runs entries first (all-in, reversal-capable) then flatten-to-flat exits. (This is the generic `(signal, action)` shape earlier designs deliberately avoided — a policy-object/`RuleStrategy`/combined-`evaluate` lineage; it was reintroduced **on request**. `SingleAssetStrategy` is still just "a concrete type implementing the trait", parameterised over its signals — not a rule engine. Don't add policy traits or a `(signal, action)` table beyond it without being asked.) The `src/strategies/` **catalogue of classical strategies** is then a set of **free-function specializations** returning a `SingleAssetStrategy` (`ma_crossover`, `rsi_reversal`, `donchian_breakout`, `keltner_breakout`, … grouped under `trend`/`mean_reversion`/`momentum`/`volume`/`composite`): long/flat = `new(sym).long_on(enter, exit)`, always-in long/short = `new(sym).long_on(up, down).short_on(down, up)`. The one strategy that doesn't fit the long/flat/short, all-in mould — `ZScoreReversion` (reads its `z` indicator directly; long/short with a flat rest) — stays its own bespoke `Strategy` type. Shared code is limited to tiny mechanical helpers (`is_long`/`is_short`, taking the position's `.amount`) in `strategies/mod.rs`. Positions size all-in via `value_frac(1.0)`, which survives a reversal (equity, unlike cash), so one `set` reverses and re-sizes all-in exactly — no `enter_all_in` helper.
|
|
57
|
+
|
|
58
|
+
All in `src/strategy.rs`:
|
|
59
|
+
- **`Wallet<Sym>` is a trait** (the portfolio interface taken as `&mut dyn`) — the single **seam** between pure fugazi and a downstream execution system. fugazi stays pure (ships only the in-memory paper impl); a downstream crate implements `Wallet` with a type whose `set_position` *publishes to an event bus / routes to a broker*. The wallet is **priced from outside**: it carries no market view; `update(symbol, price)` feeds each symbol's worth every tick (fugazi is agnostic to where prices come from), and `funds()`/`position(&Sym)`/`price(&Sym)`/`equity()` query it. The single execution primitive is `set_position(Quantity)` (drive a symbol to an absolute signed-unit target); `set` (a `Side` + `Size`, absolute target — opposite side reverses) and `close` (flat) are **default methods** over it, resolving `Size` once so only execution is per-impl. Movements return `Result<Option<Order>, WalletError>` — `Ok(None)` is "nothing to trade", and `WalletError` (`UnknownPrice`, `InvalidPrice` for a non-positive price, `InsufficientFunds` for a no-margin overdraft) flags an impossible move instead of silently no-op'ing. There is deliberately **no `trade(delta)` primitive and no additive `open`** — scale-in is `set_position(position + delta)`. NB: the trading/event-bus/market system itself is **not** in fugazi — it's a separate project that imports fugazi; keep market/IO code out of this crate.
|
|
60
|
+
- **Unit-tagged amounts** keep reference currency and instrument units from mixing: `Reference(Real)` (quote/funds currency — `funds`/`equity`/`price`) and `Quantity<Sym> { symbol, amount }` (signed instrument units — `position`, `set_position`). `Order` stays plain `Real` (its `symbol`+`side` already imply the unit).
|
|
61
|
+
- **`PaperWallet<Sym>`** is the built-in **pure** `Wallet` impl: in-memory `funds` + `HashMap<Sym,Real>` positions + a `HashMap<Sym,Real>` price map + a blotter (`Vec<Order>`); its `set_position` assumes the fill at the symbol's last fed price and books it. Caller-owned; adds inherent `new`, `is_flat`, `positions()`, `orders()`, `clear_blotter()` (`equity()` is the trait method, arg-free).
|
|
62
|
+
- **`Size`** (the magnitude vocabulary): `Units(n)` absolute, `FundsFraction(f)` (= `f·funds/price`, cash), `ValueFraction(f)` (= `f·equity/price`, all-in/target-weight; `1.0` flips cleanly on a reversal), `PositionFraction(f)` (= `f·|position|`, adjust-only). `resolve(price, position, funds, equity) -> magnitude`. Direction comes from `Side` (`Buy`/`Sell`, `.sign()`), not the size.
|
|
63
|
+
- `Order<Sym>` (`{ symbol, side, quantity }`); `Order::from_delta(symbol, delta)` builds the buy/sell for a position change (`None` within `DEFAULT_EPSILON`).
|
|
64
|
+
- There is **no `Market` trait**: the wallet holds its own fed prices, so a multi-asset input just feeds several symbols via `update` and a strategy's `trade` acts on several symbols in one call (multi-asset/pairs in the same type).
|
|
65
|
+
- Sizing/direction/short-selling/always-in-market are all just *what the strategy's code does* — no flags. Python (`python/src/lib.rs`) binds `PaperWallet`/`Order`/`Size` (sides as `"buy"`/`"sell"` strings, symbols as `str`; `update`/`set`/`set_position`/`close`, `WalletError` → `ValueError`); a Python "strategy" is plain Python code driving a `PaperWallet`.
|
|
66
|
+
|
|
67
|
+
### Generic transform ops (`src/indicators/ops.rs`)
|
|
68
|
+
|
|
69
|
+
Source-wrapping carriers, each driven by an operator type so a new operator is a trait impl, not a new type:
|
|
70
|
+
- `Combine<L, R, Op>` (binary, `BinaryOp`): **one carrier for all binary ops**, generic over the op's input/output via associated `Lhs`/`Rhs`/`Output` and holding the op **by value**. Serves arithmetic `Add`/`Sub`/`Mul`/`Div` (`Real,Real→Real`; `Div` → `None` on /0), the comparisons in `indicators/compare.rs` (`Real,Real→bool`, the op carrying its epsilon) and the boolean logic in `indicators/logic.rs` (`bool,bool→bool`). `Combine::new` needs `Op: Default`; comparison ops also get `Combine::with_epsilon`.
|
|
71
|
+
- `Lookback<I, Op>` (unary, relates a source to its value `period` steps ago, `LookbackOp`, zero-sized markers): `Lag` (past value), `Diff` (`x[t]-x[t-n]`), `Ratio` (`x[t]/x[t-n]`), `Roc`.
|
|
72
|
+
- `Extreme<S, Op>` (rolling extremum, `ExtremeOp` = `MaxOp`/`MinOp`): `RollingMax`/`RollingMin`.
|
|
73
|
+
|
|
74
|
+
### Shared cores (`pub(crate)`)
|
|
75
|
+
|
|
76
|
+
Bare `Real -> Real` math with **no source and no `Indicator` impl**, so both source-wrapping indicators and indicators smoothing values they compute *internally* share one implementation:
|
|
77
|
+
- `smoothing.rs`: `EmaState` (EMA recurrence) and `WilderState` (Wilder/RMA, mean-seed). `Ema`/`Macd` use `EmaState`; `Rma` uses `WilderState`; `Rsi` uses two (gain/loss); `Atr` = `TrueRange` + `WilderState`; `Adx` uses four.
|
|
78
|
+
- `stats.rs`: `WindowStats` (windowed sum + sum-of-squares → `mean`/`variance`/`stddev`) backs `Sma`/`StdDev`/`Bollinger`; `WindowExtreme<Op>` (monotonic-deque rolling extremum) backs `Extreme`/`RollingMax`/`RollingMin` and `Stochastic`.
|
|
79
|
+
|
|
80
|
+
## Conventions and gotchas
|
|
81
|
+
|
|
82
|
+
- **Composition is construction.** A new "X of Y" indicator takes its source `S: Indicator<Output = Real>` in `new`; don't add pipe combinators.
|
|
83
|
+
- **Use the cores, not each other's public types.** Internal smoothing of computed scalars uses `EmaState`/`WilderState` (Real recurrence). The public `Rma<S>`/`Ema<S>` wrap a *source* and can't smooth values you computed inline.
|
|
84
|
+
- **Adding an operator** (arithmetic/comparison/boolean/lookback): add an `*Op` type implementing the relevant `*Op` trait plus a type alias — never a macro. Arithmetic/boolean/lookback ops are zero-sized `Default` markers; comparison ops are value structs carrying `epsilon`. They live by their carrier — `indicators/{compare,logic}.rs` for the `Combine`-based ones, `indicators/ops.rs` for `Lookback`/`Extreme`.
|
|
85
|
+
- `Combine` (arithmetic, comparisons, and `And`/`Or`/`Xor`) feeds the *same* input to both sides, so it requires `Input: Clone`. Use `lhs`/`rhs` naming for binary operands.
|
|
86
|
+
- `Combine` holds its op **by value** (so a comparison can carry epsilon); `Lookback`/`Extreme` hold a zero-sized op as `PhantomData<fn() -> Op>`. Input-ignoring leaves (`Value`, `Const`, `Field`) use `PhantomData<fn(I)>` / `fn() -> F` to satisfy the constraint rules (avoids E0207).
|
|
87
|
+
- `Change` is a **bidirectional** toggle detector (fires on any transition); directional events come from pairing it with a comparison (see `crosses_above`).
|
|
88
|
+
- Constructors `assert!(period > 0, ...)`; document warm-up length in the type's doc comment.
|
|
89
|
+
- A comparison/edge is **`None` until** every source it depends on is warmed up (it reads `false` via `.is_true()`); a boolean op (`And`/`Or`/…) is likewise `None` until both sources are ready, so an edge that would coincide with warm-up is not detected (no spurious first-bar trade).
|
fugazi-0.1.1/Cargo.lock
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# This file is automatically @generated by Cargo.
|
|
2
|
+
# It is not intended for manual editing.
|
|
3
|
+
version = 4
|
|
4
|
+
|
|
5
|
+
[[package]]
|
|
6
|
+
name = "fugazi"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
|
|
9
|
+
[[package]]
|
|
10
|
+
name = "fugazi-python"
|
|
11
|
+
version = "0.1.1"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"fugazi",
|
|
14
|
+
"pyo3",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[[package]]
|
|
18
|
+
name = "heck"
|
|
19
|
+
version = "0.5.0"
|
|
20
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
21
|
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
|
22
|
+
|
|
23
|
+
[[package]]
|
|
24
|
+
name = "libc"
|
|
25
|
+
version = "0.2.186"
|
|
26
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
27
|
+
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
28
|
+
|
|
29
|
+
[[package]]
|
|
30
|
+
name = "once_cell"
|
|
31
|
+
version = "1.21.4"
|
|
32
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
33
|
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|
34
|
+
|
|
35
|
+
[[package]]
|
|
36
|
+
name = "portable-atomic"
|
|
37
|
+
version = "1.13.1"
|
|
38
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
39
|
+
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
40
|
+
|
|
41
|
+
[[package]]
|
|
42
|
+
name = "proc-macro2"
|
|
43
|
+
version = "1.0.106"
|
|
44
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
45
|
+
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
|
46
|
+
dependencies = [
|
|
47
|
+
"unicode-ident",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[[package]]
|
|
51
|
+
name = "pyo3"
|
|
52
|
+
version = "0.29.0"
|
|
53
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
54
|
+
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
|
55
|
+
dependencies = [
|
|
56
|
+
"libc",
|
|
57
|
+
"once_cell",
|
|
58
|
+
"portable-atomic",
|
|
59
|
+
"pyo3-build-config",
|
|
60
|
+
"pyo3-ffi",
|
|
61
|
+
"pyo3-macros",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[[package]]
|
|
65
|
+
name = "pyo3-build-config"
|
|
66
|
+
version = "0.29.0"
|
|
67
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
68
|
+
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
|
69
|
+
dependencies = [
|
|
70
|
+
"target-lexicon",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[[package]]
|
|
74
|
+
name = "pyo3-ffi"
|
|
75
|
+
version = "0.29.0"
|
|
76
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
77
|
+
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
|
78
|
+
dependencies = [
|
|
79
|
+
"libc",
|
|
80
|
+
"pyo3-build-config",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[[package]]
|
|
84
|
+
name = "pyo3-macros"
|
|
85
|
+
version = "0.29.0"
|
|
86
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
87
|
+
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
|
88
|
+
dependencies = [
|
|
89
|
+
"proc-macro2",
|
|
90
|
+
"pyo3-macros-backend",
|
|
91
|
+
"quote",
|
|
92
|
+
"syn",
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
[[package]]
|
|
96
|
+
name = "pyo3-macros-backend"
|
|
97
|
+
version = "0.29.0"
|
|
98
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
99
|
+
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
|
100
|
+
dependencies = [
|
|
101
|
+
"heck",
|
|
102
|
+
"proc-macro2",
|
|
103
|
+
"quote",
|
|
104
|
+
"syn",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
[[package]]
|
|
108
|
+
name = "quote"
|
|
109
|
+
version = "1.0.45"
|
|
110
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
111
|
+
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
|
112
|
+
dependencies = [
|
|
113
|
+
"proc-macro2",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
[[package]]
|
|
117
|
+
name = "syn"
|
|
118
|
+
version = "2.0.118"
|
|
119
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
120
|
+
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
|
121
|
+
dependencies = [
|
|
122
|
+
"proc-macro2",
|
|
123
|
+
"quote",
|
|
124
|
+
"unicode-ident",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
[[package]]
|
|
128
|
+
name = "target-lexicon"
|
|
129
|
+
version = "0.13.5"
|
|
130
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
131
|
+
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
|
132
|
+
|
|
133
|
+
[[package]]
|
|
134
|
+
name = "unicode-ident"
|
|
135
|
+
version = "1.0.24"
|
|
136
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
137
|
+
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
fugazi-0.1.1/Cargo.toml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "fugazi"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
description = "A library of incremental technical-analysis indicators, signals and strategies"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
repository = "https://github.com/acpuchades/fugazi"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["trading", "technical-analysis", "indicators", "finance", "incremental"]
|
|
10
|
+
categories = ["finance", "mathematics"]
|
|
11
|
+
|
|
12
|
+
[lib]
|
|
13
|
+
name = "fugazi"
|
|
14
|
+
path = "src/lib.rs"
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
|
|
18
|
+
# The core crate stays dependency-free. The Python bindings live in their own
|
|
19
|
+
# workspace member (`python/`), which is the only crate that pulls in PyO3 —
|
|
20
|
+
# see that crate's README for build instructions.
|
|
21
|
+
[workspace]
|
|
22
|
+
members = ["python"]
|
|
23
|
+
resolver = "2"
|
fugazi-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 acpuchades
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|