Integration Patterns
Real-world patterns for integrating PyneCore into trading systems
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, tradesBoth 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 dataframePattern 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 dataframeFor 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,
varvariables, 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.