Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,19 @@ version number follows semantic versioning over the surfaces the
unchanged. The streaming guide gains a "Backtest vs live" section listing what a
backtest can see that a live run cannot.

- **Harness symbol feeds:** `docker/run_json.py --symbol-feeds` (`PINEFORGE_SYMBOL_FEEDS`)
installs other symbols' bars for `request.security` on another symbol: a JSON
index keyed by the exact symbol string the script passes and by timeframe, each
feed an OHLCV CSV plus the symbol's catalog `syminfo`, through
`strategy_set_symbol_facts` / `strategy_set_symbol_feed` (engine and codegen
1.0.0 and later). One feed per requested timeframe: nothing aggregates another
symbol's bars, so a `1` feed serves neither a `240` nor a `D` request. Unset
changes nothing; an index or feed it cannot install fails
the run (one `{"engine":"pineforge","error":...}` line, harness exit 1,
entrypoint exit 4). What was installed is recorded as
`applied_runtime.symbol_feeds`, so such a run has its own fingerprint digest.
The engine library is unchanged.

- **Native runner routing and delivery:** webhooks are optional; `--webhook-routes`
adds first-match per-action targets and payload `pineforge-native-order-action/v2`.
New `actions`, `status` and offline `redeliver` commands expose the journal and
Expand Down
82 changes: 82 additions & 0 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,88 @@ library's `strategy_set_syminfo_*` setters.
`docker/run_json.py` is vendored: pineforge-release copies it from the engine tag
at every release, so the lot-grid handling (`mincontract`) lives in this file.

### Other symbols' bars (`PINEFORGE_SYMBOL_FEEDS`)

A script that calls `request.security` on another symbol reads that symbol's
own bars, never the chart's. Without them, or when the index below lacks the
requested symbol string or timeframe, the run stops where the request's value
is read (`request.security(...) at line N: no data is pinned for this request,
and its value was read`, exit 4). `PINEFORGE_SYMBOL_FEEDS` (the
harness's `--symbol-feeds`) names a JSON index of those bars, installed through
the library's `strategy_set_symbol_facts` and `strategy_set_symbol_feed` (engine
1.0.0 and later):

```json
{"symbols": {
"BINANCE:ETHUSDT": {
"syminfo": {"tickerid": "BINANCE:ETHUSDT", "type": "crypto", "currency": "USDT",
"mintick": 0.01, "session": "24x7", "timezone": "UTC"},
"feeds": {"240": "ethusdt-240.csv", "1D": "ethusdt-1D.csv"}}}}
```

```bash
docker run --rm \
-v $(pwd)/strategy.pine:/in/strategy.pine:ro \
-v $(pwd)/btcusdt-240.csv:/in/ohlcv.csv:ro \
-v $(pwd)/symbols:/in/symbols:ro \
-e PINEFORGE_SYMBOL_FEEDS=/in/symbols/symbols.json \
pineforge > report.json
```

- A symbol key is the exact string the script passes at run time, exchange
prefix and suffix included: `BINANCE:ETHUSDT`, `ETHUSDT` and
`BINANCE:ETHUSDT.P` are three symbols. For `input.symbol` it is the input's
value (its default, or the `PINEFORGE_INPUTS` override). A string naming the
chart's own market is another symbol too: the harness does not set the chart's
`syminfo.tickerid`.
- One feed per timeframe the script requests, keyed in the engine's spelling:
whole minutes as a bare integer (`"240"`, never `"4h"`), else `<n>D|W|M|S`;
a bare `D`/`W`/`M`/`S` is folded to `1D`/`1W`/`1M`/`1S`. A request at
`timeframe.period` (or `""`) reads the feed at the chart's timeframe.
- A feed is a CSV like `ohlcv.csv` (`timestamp,open,high,low,close,volume`; an
empty volume is a symbol that publishes none), paths relative to the index.
Each bar's close is its open plus the timeframe (calendar months for `M`),
right for a 24x7 symbol; give a session-bound symbol a `time_close` column
(unix ms; an empty cell falls back to open plus timeframe). Bars before the
chart's first bar are delivered as history on it; bars after its last are
never read. A header-only feed installs the symbol without bars: its requests
read na on every bar (a symbol with no bars in the window).
- `syminfo` is the symbol's catalog object, flat or `{"syminfo": {...}}`. Its
`type`, `timezone`, `session`, `currency` and `mintick` are set as the
symbol's facts, which `syminfo.*` reads inside the request; `tickerid` is set
as its `canonical` fact, which no `syminfo.*` reads; other keys are ignored.
Inside the request `syminfo.tickerid` is always the key and `syminfo.ticker`
the key after its last `:`. Without `syminfo`, `syminfo.mintick` reads NaN
and those four strings read empty.
- Merge rule (TradingView's): with `lookahead` off a chart bar reads the latest
requested bar whose close is at or before the chart bar's close; with it on,
the latest that opened at or before the chart bar's open. A missing requested
bar carries the last value forward (`gaps` off) or reads na (`gaps` on).
- Limits: at most 256 symbols and 256 feeds in one index. The chart must be its
own input (`PINEFORGE_SCRIPT_TF` unset or equal to the input timeframe), else
the run fails with `request.security of another symbol needs the chart's own
bars as input; input '<i>' aggregated to chart '<s>' is not supported`. The run
is historical only. Each feed is a full pass of its request's expression over
its bars. `request.security_lower_tf` on another symbol reads no feed.
- A feed must be at the timeframe it serves. Nothing aggregates another
symbol's bars: the engine looks a feed up by the exact symbol string and
timeframe, and `strategy_set_symbol_feed` installs the bars as given. A `240`
feed does not serve a `D` request, and a `1` feed serves only a request at
`1`, so give each requested timeframe its own bars.
- An index or feed the harness cannot install (for example: not JSON, a bad
timeframe spelling, two feeds at one timeframe, a CSV without `close`,
timestamps that do not strictly increase, a close after the next bar's open,
a non-positive `mintick`, more than 256 feeds, a library without the setters,
a feed the engine refuses)
fails the run before it starts: one line
`{"engine":"pineforge","error":"--symbol-feeds: ..."}` on stdout, harness exit
1, entrypoint exit 4.
- What was installed is recorded as `applied_runtime.symbol_feeds` (each
symbol's facts, and per feed its bar count, first and last open and a hash of
its values), so the fingerprint digest differs from a run without it. Unset,
or an index naming no symbol: the key is absent and the report is what it was
before this variable existed, apart from `elapsed_seconds`.

The engine catches every error (TF mismatch, unsupported emulation
flags, unknown-input-TF, etc.) into `strategy_get_last_error()`; the
container surfaces these as `{"engine":"pineforge","error":"..."}` on
Expand Down
10 changes: 10 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
# keys are ignored. mincontract absent or null: no lot grid. Any other
# mincontract that is not a positive finite number fails the run
# (exit 4, one {"engine":"pineforge","error":...} line on stdout).
# PINEFORGE_SYMBOL_FEEDS Path to a JSON index of other symbols' bars for
# request.security on another symbol: {"symbols": {"<symbol string>":
# {"syminfo": {...}, "feeds": {"<timeframe>": "<csv path>"}}}}, the
# symbol string exactly as the script passes it, one CSV per
# timeframe the script requests (paths relative to the index). Unset:
# nothing is installed and such a request stops the run where its
# value is read. An index or feed the harness cannot install fails
# the run (exit 4, one {"engine":"pineforge","error":...} line).
#
# Exit codes:
# 0 success (JSON report, or C++ in transpile-only mode, on stdout)
Expand Down Expand Up @@ -140,12 +148,14 @@ echo "[pineforge] running backtest ..." >&2
# PINEFORGE_CHART_TZ IANA tz for date builtins
# PINEFORGE_MAGNIFIER_VOLUME_WEIGHTED 1/true → vw magnifier (needs BAR_MAGNIFIER)
# PINEFORGE_SYMINFO path to a syminfo.json (see the header)
# PINEFORGE_SYMBOL_FEEDS path to other symbols' feed index (see the header)
# PINEFORGE_BENCH (+_WARMUP/_REPEATS) 1/true → timing mode
extra=()
[[ -n "${PINEFORGE_TRADE_START_MS:-}" ]] && extra+=(--trade-start-ms "${PINEFORGE_TRADE_START_MS}")
[[ -n "${PINEFORGE_CHART_TZ:-}" ]] && extra+=(--chart-tz "${PINEFORGE_CHART_TZ}")
[[ "${PINEFORGE_MAGNIFIER_VOLUME_WEIGHTED:-}" =~ ^(1|true|yes|on)$ ]] && extra+=(--magnifier-volume-weighted)
[[ -n "${PINEFORGE_SYMINFO:-}" ]] && extra+=(--syminfo "${PINEFORGE_SYMINFO}")
[[ -n "${PINEFORGE_SYMBOL_FEEDS:-}" ]] && extra+=(--symbol-feeds "${PINEFORGE_SYMBOL_FEEDS}")
if [[ "${PINEFORGE_BENCH:-}" =~ ^(1|true|yes|on)$ ]]; then
extra+=(--bench --warmup "${PINEFORGE_WARMUP:-3}" --repeats "${PINEFORGE_REPEATS:-20}")
fi
Expand Down
Loading
Loading