Integration Patterns

PyneCore can be embedded into any Python application. This page covers common integration patterns with real-world trading frameworks.

DataFrame Bridge (FreqTrade, Backtrader, etc.)

Many trading frameworks work with pandas DataFrames. The bridge pattern converts between DataFrames and PyneCore’s OHLCV format:

import pandas as pd
from pynecore.core.script_runner import ScriptRunner
from pynecore.core.syminfo import SymInfo
from pynecore.types.ohlcv import OHLCV


def dataframe_to_ohlcv(df: pd.DataFrame) -> list[OHLCV]:
    """Convert a pandas DataFrame to a list of OHLCV objects."""
    return [
        OHLCV(
            timestamp=int(row.Index.timestamp() * 1000),
            open=float(row.open), high=float(row.high),
            low=float(row.low), close=float(row.close),
            volume=float(row.volume),
        )
        for row in df.itertuples()
    ]


def run_indicator(df, script_path, syminfo, inputs=None):
    """Run a PyneCore indicator, return results as dict of pd.Series."""
    runner = ScriptRunner(
        script_path=script_path,
        ohlcv_iter=dataframe_to_ohlcv(df),
        syminfo=syminfo,
        inputs=inputs,
    )

    results = {}
    for _candle, plot_data in runner.run_iter():
        for key, value in plot_data.items():
            results.setdefault(key, []).append(value)

    return {
        key: pd.Series(values, index=df.index[:len(values)])
        for key, values in results.items()
    }


def run_strategy(df, script_path, syminfo, inputs=None, settings=None):
    """Run a PyneCore strategy, return its plots (dict of pd.Series) and closed trades."""
    runner = ScriptRunner(
        script_path=script_path,
        ohlcv_iter=dataframe_to_ohlcv(df),
        syminfo=syminfo,
        inputs=inputs,
        settings=settings,
    )

    results = {}
    trades = []
    for _candle, plot_data, new_trades in runner.run_iter():
        for key, value in plot_data.items():
            results.setdefault(key, []).append(value)
        trades.extend(new_trades)

    indicators = {
        key: pd.Series(values, index=df.index[:len(values)])
        for key, values in results.items()
    }
    return indicators, trades

Both helpers read plot_data and new_trades inside the loop, so they could also pass copy_results=False to run_iter() and skip the per-bar copy (see Keeping Results vs. the Fast Path).

Usage:

rsi_data = run_indicator(dataframe, Path("rsi.py"), syminfo)
dataframe["rsi"] = rsi_data["RSI"]

FreqTrade Integration

PyneCore integrates with FreqTrade in two ways:

Pattern 1: Indicators as Data Sources

Use Pine Script indicators for calculations, write entry/exit logic in Python:

class PyneIndicatorStrategy(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        pair = metadata.get("pair", "BTC/USDT")
        rsi = run_indicator(dataframe, Path("scripts/rsi.py"), syminfo)
        dataframe["rsi"] = rsi.get("RSI")
        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        dataframe.loc[dataframe["rsi"] < 30, "enter_long"] = 1
        return dataframe

Pattern 2: Strategy Signals

Let a Pine Script strategy generate buy/sell signals, FreqTrade just executes them:

class PyneStrategySignals(IStrategy):
    def populate_indicators(self, dataframe, metadata):
        _indicators, trades = run_strategy(dataframe, Path("scripts/sma_cross.py"), syminfo)

        dataframe["pyne_enter_long"] = 0
        for trade in trades:
            if trade.size > 0 and trade.entry_bar_index < len(dataframe):
                dataframe.iloc[trade.entry_bar_index,
                               dataframe.columns.get_loc("pyne_enter_long")] = 1
        return dataframe

For complete, runnable FreqTrade examples, see pynecore-examples/05-freqtrade-indicators and pynecore-examples/06-freqtrade-strategy.

Live Data Feed

Process bars as they close on an exchange. The feed yields the closed history first, then LIVE_TRANSITION, then every new candle once it has closed, and the runner runs in live mode, so each live bar’s result comes out as soon as the candle closes. CCXT’s fetch_ohlcv returns the still-forming candle as its last row, so the feed passes on only the candles whose period has ended:

import ccxt
import time
from pynecore.core.script_runner import ScriptRunner, LIVE_TRANSITION
from pynecore.types.ohlcv import OHLCV

exchange = ccxt.binance({"enableRateLimit": True})


def closed_candles(symbol, timeframe, limit):
    """The last ``limit`` closed candles; the forming candle is dropped."""
    bar_ms = exchange.parse_timeframe(timeframe) * 1000
    now = exchange.milliseconds()
    raw = exchange.fetch_ohlcv(symbol, timeframe, limit=limit + 1)
    return [
        OHLCV(timestamp=bar[0],  # CCXT and PyneCore both use milliseconds
              open=bar[1], high=bar[2], low=bar[3], close=bar[4], volume=bar[5])
        for bar in raw if bar[0] + bar_ms <= now
    ][-limit:]


def live_candles(symbol, timeframe, warmup=200):
    """Closed history, ``LIVE_TRANSITION``, then each new candle once it closes."""
    history = closed_candles(symbol, timeframe, warmup)
    yield from history
    yield LIVE_TRANSITION

    last_ts = history[-1].timestamp
    while True:
        time.sleep(10)
        for candle in closed_candles(symbol, timeframe, 5):
            if candle.timestamp > last_ts:
                last_ts = candle.timestamp
                yield candle


runner = ScriptRunner(
    script_path=Path("my_indicator.py"),
    ohlcv_iter=live_candles("BTC/USDT", "1h"),
    syminfo=syminfo,
    live=True,
)

for candle, plot_data in runner.run_iter():
    rsi = plot_data.get("RSI")
    if rsi < 30:  # NA values return False for all comparisons — no special handling needed
        print(f"RSI oversold: {rsi:.2f} at {candle.close}")

Without live=True the runner treats the feed as history and reads one bar ahead, so every result would arrive a candle late.

For a complete live CCXT example, see pynecore-examples/04-live-ccxt.

Multiple Indicators on Same Data

Run several scripts on the same data efficiently:

from pynecore.core.script_runner import ScriptRunner

data = list(ohlcv_source)  # Materialize once if it's a generator

# Run each indicator separately
rsi_runner = ScriptRunner(script_path=Path("rsi.py"), ohlcv_iter=data, syminfo=syminfo)
bb_runner = ScriptRunner(script_path=Path("bollinger.py"), ohlcv_iter=data, syminfo=syminfo)

rsi_values = [plot.get("RSI") for _, plot in rsi_runner.run_iter()]
bb_values = [(plot.get("Upper"), plot.get("Lower")) for _, plot in bb_runner.run_iter()]

Parameter Optimization

Sweep over input combinations to find optimal parameters. The inputs keys are the main() parameter names of the script (here def main(length=input.int(...), confirm=input.int(...))):

from itertools import product

results = []
for length, confirm in product(range(5, 30, 5), range(1, 4)):
    runner = ScriptRunner(
        script_path=Path("sma_crossover.py"),
        ohlcv_iter=data,
        syminfo=syminfo,
        inputs={"length": length, "confirm": confirm},
    )

    trades = []
    for _, _, new_trades in runner.run_iter():
        trades.extend(new_trades)

    if trades:
        pnl = sum(t.profit for t in trades)
        results.append({"length": length, "confirm": confirm, "pnl": pnl, "trades": len(trades)})

# Find best combination
best = max(results, key=lambda r: r["pnl"])
print(f"Best: Length={best['length']}, Confirm={best['confirm']} → P&L={best['pnl']:+.2f}")

The script’s strategy settings can be swept the same way with settings, for example to see how sensitive the result is to trading costs:

from pynecore.lib import strategy

for commission in (0.0, 0.05, 0.1, 0.2):  # percent per order
    runner = ScriptRunner(
        script_path=Path("sma_crossover.py"),
        ohlcv_iter=data,
        syminfo=syminfo,
        inputs={"length": best["length"], "confirm": best["confirm"]},
        settings={"commission_type": strategy.commission.percent, "commission_value": commission},
    )
    pnl = sum(t.profit for _, _, new_trades in runner.run_iter() for t in new_trades)
    print(f"commission {commission}% → P&L={pnl:+.2f}")

Overrides apply to their own run only and are never written into the script’s .toml, so a sweep leaves the script’s configuration as it was (see Precedence and the Script’s .toml File).

Performance Tips

  • Materialize generators: If running multiple scripts on the same data, convert the OHLCV iterator to a list first (list(reader.read_from(...))) to avoid re-reading from disk.

  • Re-run instead of caching partial results: frameworks like FreqTrade call your indicator function with the full DataFrame on every new candle. Run the script over all of it again. A script carries state from bar to bar (moving averages, var variables, the strategy’s position), so running it on the new bars alone gives different values. A full re-run of a simple indicator over 5,000 bars takes a few tens of milliseconds.

Complete Examples

For full, runnable examples covering all these patterns, see the pynecore-examples repository.