Powerlanguage python conversion
Skill KasperChenGH/multicharts-powerlanguage/skills/powerlanguage-python-conversion
Use when converting or porting code between MultiCharts PowerLanguage and Python in either direction — Strategy ABC scaffold, concept mapping tables, semantic differences, pandas-ta/TA-Lib indicators, conversion checklists.From its SKILL.md
npx -y skills add KasperChenGH/multicharts-powerlanguage --skill powerlanguage-python-conversionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 9 stars9 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
37.8 KB, as published. Nobody here has run it
PowerLanguage ↔ Python Conversion
How to use
This skill covers the structural and semantic differences between MultiCharts PowerLanguage and Python for algorithmic trading. It does not duplicate the syntax rules of PowerLanguage — for declarations, bar references, control flow, and built-in function signatures use the powerlanguage-syntax skill. When performing a conversion in either direction, work through Part 0 (structural template) to set up the boilerplate, then Part 1 (concept mapping) for line-by-line translation, then review Part 2 (semantic differences and gotchas), and finally run through the relevant checklist in Part 3 before calling the output complete.
Part 0: Structural Template
PowerLanguage runs the entire script top-to-bottom on each bar close automatically. Python has no implicit bar loop, no built-in OHLCV type, and no order submission mechanism. Every PL-to-Python conversion targets the framework-agnostic scaffold below.
Bar dataclass
from dataclasses import dataclass
@dataclass
class Bar:
open: float
high: float
low: float
close: float
volume: float
time: int # Unix epoch seconds
bar_number: int # 1-based to match PL convention
Order types
from enum import Enum, auto
from dataclasses import dataclass
class Side(Enum):
LONG = auto()
SHORT = auto()
class OrderType(Enum):
MARKET = auto()
LIMIT = auto()
STOP = auto()
class Action(Enum):
ENTRY = auto()
EXIT = auto()
STOP_LOSS = auto()
PROFIT_TARGET = auto()
BREAK_EVEN = auto()
@dataclass
class Order:
label: str
action: Action
side: Side
order_type: OrderType
price: float = 0.0 # 0.0 for Market
qty: float = 1.0
Strategy ABC
from abc import ABC, abstractmethod
class Strategy(ABC):
@abstractmethod
def on_bar(self, bars: list[Bar], orders: list[Order]) -> None:
...
Strategy subclass skeleton
import pandas as pd
import pandas_ta as ta
class MyStrategy(Strategy):
def __init__(self, length: int = 20, threshold: float = 0.5):
# Inputs (PL: Inputs: Length(20), Threshold(0.5))
self.length = length
self.threshold = threshold
# Variables (PL: Variables: my_var(0))
self.my_var = 0.0
# Position state (PL: MarketPosition, EntryPrice, etc.)
self.position = 0 # -1, 0, +1
self.entry_price = 0.0
self.bars_since_entry = 0
self.current_contracts = 0.0
def on_bar(self, bars: list[Bar], orders: list[Order]) -> None:
if len(bars) < self.length:
return
closes = pd.Series([b.close for b in bars])
# pandas-ta: compute indicator on the full series
sma = ta.sma(closes, length=self.length)
current_sma = sma.iloc[-1]
# ... strategy logic, push to orders list ...
Alternative: TA-Lib indicator pattern
import talib
import numpy as np
class MyStrategy(Strategy):
def on_bar(self, bars: list[Bar], orders: list[Order]) -> None:
closes = np.array([b.close for b in bars])
# TA-Lib: batch array API (NumPy in, NumPy out)
sma = talib.SMA(closes, timeperiod=self.length)
current_sma = sma[-1]
pandas-ta is recommended as the primary library (pure Python, zero install friction, pip-installable). TA-Lib is the performance alternative (C-based, requires compiling the C library). The concept mapping tables in Part 1 show both.
Main loop
def main():
bars: list[Bar] = load_bars("data.csv") # user-provided
strategy = MyStrategy(length=20, threshold=0.5)
for i in range(len(bars)):
orders: list[Order] = []
strategy.on_bar(bars[: i + 1], orders)
# execute orders against simulated book...
The slice bars[:i+1] gives the strategy access to full history; bars[-1] is the current bar. This mirrors PL's implicit bar-by-bar execution.
Part 1: Concept Mapping
Table 1 — Declarations
| PowerLanguage | Python | Notes |
|---|---|---|
Inputs: Length(20) | __init__ parameter with default: def __init__(self, length: int = 20) | Stored as self.length; treat as read-only after construction |
Variables: myVar(0) | self.my_var = 0.0 in __init__ | Mutable instance attributes |
Value1 .. Value99 | Named attribute (e.g., self.rsi_val) | Must rename to meaningful identifiers; Python has no pre-declared variables |
Condition1 .. Condition99 | Named attribute (e.g., self.is_oversold) | Same: rename to typed bool attributes |
Arrays: buf[10](0) | self.buf = [0.0] * 11 | PL [10] means last index is 10 (11 elements); use a Python list |
IntraBarPersist myVar(0) | self.my_var = 0.0 (same as Variables) | Python has no bar-close vs intra-bar distinction; all instance attributes persist |
Table 2 — Data Access
| PowerLanguage | Python | Notes |
|---|---|---|
Open, High, Low, Close, Volume | bars[-1].open, .high, .low, .close, .volume | bars[-1] for current bar |
Close[1] | bars[-2].close | Lookback N: bars[-1 - n].close. Guard with len(bars) > n. |
Close of Data2 | separate bars2: list[Bar] parameter | PL Data2 is a second chart feed; pass a second list to on_bar() |
Date | datetime.fromtimestamp(bars[-1].time, tz=timezone.utc) | PL Date is YYYMMDD integer; Python uses datetime module |
Time | dt.hour * 100 + dt.minute (from datetime object) | PL Time is HHMM integer |
BarNumber / CurrentBar | bars[-1].bar_number or len(bars) | PL is 1-based |
Table 3 — Technical Indicators (dual-library)
| PowerLanguage | pandas-ta (primary) | TA-Lib (alternative) | Notes |
|---|---|---|---|
Average(Close, N) | ta.sma(closes, length=N) | talib.SMA(closes, timeperiod=N) | Current value: .iloc[-1] / [-1] |
XAverage(Close, N) | ta.ema(closes, length=N) | talib.EMA(closes, timeperiod=N) | |
RSI(Close, N) | ta.rsi(closes, length=N) | talib.RSI(closes, timeperiod=N) | Returns 0–100 |
BollingerBand(Close, N, 2) | ta.bbands(closes, length=N, std=2) | talib.BBANDS(closes, timeperiod=N, nbdevup=2, nbdevdn=2) | Returns upper/mid/lower |
MACD(Close, 12, 26) | ta.macd(closes, fast=12, slow=26, signal=9) | talib.MACD(closes, fastperiod=12, slowperiod=26, signalperiod=9) | Returns MACD/signal/histogram |
Stochastic(...) | ta.stoch(highs, lows, closes, k=14, d=3) | talib.STOCH(highs, lows, closes, fastk_period=14, slowk_period=3, slowd_period=3) | Returns slowK/slowD |
ADX(N) | ta.adx(highs, lows, closes, length=N) | talib.ADX(highs, lows, closes, timeperiod=N) | |
CCI(N) | ta.cci(highs, lows, closes, length=N) | talib.CCI(highs, lows, closes, timeperiod=N) | |
AvgTrueRange(N) | ta.atr(highs, lows, closes, length=N, mamode='sma') or rolling mean of ta.true_range(...) | talib.SMA(talib.TRANGE(highs, lows, closes), timeperiod=N) | PL AvgTrueRange is a SIMPLE average of TrueRange; pandas-ta/TA-Lib ATR default to Wilder smoothing — the default Wilder ATR matches PL SmoothedAverage(TrueRange, Len) instead |
Momentum(Close, N) | ta.mom(closes, length=N) | talib.MOM(closes, timeperiod=N) | |
Highest(High, N) | highs.rolling(N).max() | talib.MAX(highs, timeperiod=N) | |
Lowest(Low, N) | lows.rolling(N).min() | talib.MIN(lows, timeperiod=N) | |
DMIPlus(N) / DMIMinus(N) | ta.dm(highs, lows, length=N) | talib.PLUS_DI(...) / talib.MINUS_DI(...) | |
KeltnerChannel(Close, N, mult) | ta.kc(highs, lows, closes, length=N, scalar=mult) | Manual: talib.EMA(closes, N) ± mult * talib.ATR(highs, lows, closes, N) | |
Parabolic(step) | ta.psar(highs, lows, af0=step, max_af=0.2) | talib.SAR(highs, lows, acceleration=step, maximum=0.2) | |
PercentR(N) | ta.willr(highs, lows, closes, length=N) + 100 | talib.WILLR(highs, lows, closes, timeperiod=N) + 100 | PL PercentR is POSITIVE 0..100 (= Williams %R + 100); library WILLR returns −100..0 — add 100, and thresholds become 80/20 on the positive scale |
RateOfChange(Close, N) | ta.roc(closes, length=N) | talib.ROC(closes, timeperiod=N) | |
Volatility(N) | ta.true_range(highs, lows, closes).ewm(span=N).mean() (or ta.atr(..., mamode='ema')) | talib.EMA(talib.TRANGE(highs, lows, closes), timeperiod=N) | PL Volatility is a smoothed average of TrueRange weighted toward the most recent bar — NOT a stdev-based measure (StandardDev is stdev of closes; VolatilityStdDev is annualized stdev of log returns) |
StandardDev(Close, N, 1) | ta.stdev(closes, length=N) | talib.STDDEV(closes, timeperiod=N, nbdev=1) | |
TrueRange | ta.true_range(highs, lows, closes) | talib.TRANGE(highs, lows, closes) | Per-bar, no period |
MoneyFlow(N) | ta.mfi(highs, lows, closes, volumes, length=N) | talib.MFI(highs, lows, closes, volumes, timeperiod=N) | Requires HLCV |
AccumDist | ta.ad(highs, lows, closes, volumes) | talib.AD(highs, lows, closes, volumes) | Cumulative, no period |
LinearRegValue(Close, N, 0) | ta.linreg(closes, length=N) | talib.LINEARREG(closes, timeperiod=N) | |
LinearRegSlope(Close, N) | ta.linreg(closes, length=N, slope=True) | talib.LINEARREG_SLOPE(closes, timeperiod=N) | |
Summation(Close, N) | closes.rolling(N).sum() | talib.SUM(closes, timeperiod=N) | |
Cum(Volume) | volumes.cumsum() | np.cumsum(volumes) | Running total |
WAverage(Close, N) | ta.wma(closes, length=N) | talib.WMA(closes, timeperiod=N) | |
MidPoint(Close, N) | ta.midpoint(closes, length=N) | talib.MIDPOINT(closes, timeperiod=N) | |
TSI(Close, LongLen, ShortLen) | ta.tsi(closes, fast=ShortLen, slow=LongLen) | Manual: double-EMA of momentum / double-EMA of abs(momentum) | pandas-ta fast=short, slow=long (reversed naming vs PL) |
SwingHigh(Occur, Price, Strength, Length) | Manual: scan for pivot high | Manual: scan for pivot high | Occurrence is the FIRST PL arg; returns −1 when no swing found; Length must exceed Strength |
SwingLow(Occur, Price, Strength, Length) | Manual: scan for pivot low | Manual: scan for pivot low | Same: Occurrence first, −1 when none found, Length > Strength |
HighestBar(High, N) | highs[-N:].idxmax() → compute bars ago | np.argmax(highs[-N:]) | Returns bars ago |
LowestBar(Low, N) | lows[-N:].idxmin() → compute bars ago | np.argmin(lows[-N:]) | Returns bars ago |
CountIF(cond, N) | cond_series.rolling(N).sum() | Manual: sum(1 for ...) | Count True values |
Crosses Over | prev_a <= prev_b and a > b | Same | No built-in; store previous values |
Crosses Under | prev_a >= prev_b and a < b | Same | Same |
AverageFC(Close, N) | ta.sma(closes, length=N) | talib.SMA(closes, timeperiod=N) | FC = "fast calculation": AverageFC = SummationFC/Len (running-sum SMA), numerically identical to Average — use SMA |
AdaptiveMovAvg(Close, N) | ta.kama(closes, length=N) | talib.KAMA(closes, timeperiod=N) | Kaufman Adaptive Moving Average |
UltimateOscillator(7,14,28) | ta.uo(highs, lows, closes, fast=7, medium=14, slow=28) | talib.ULTOSC(highs, lows, closes, timeperiod1=7, timeperiod2=14, timeperiod3=28) | Requires HLC |
ChaikinOsc(3, 10) | ta.adosc(highs, lows, closes, volumes, fast=3, slow=10) | talib.ADOSC(highs, lows, closes, volumes, fastperiod=3, slowperiod=10) | Requires HLCV |
PriceOscillator(Fast, Slow) | ta.apo(closes, fast=Fast, slow=Slow) | talib.APO(closes, fastperiod=Fast, slowperiod=Slow) | Absolute Price Oscillator |
DirMovement(N, ...) | ta.dm(highs, lows, length=N) + ta.adx(...) | talib.PLUS_DI(...) + talib.MINUS_DI(...) + talib.ADX(...) | Multi-output: +DI, −DI, ADX |
Extremes(High, Low, N, oHH, oLL, oHHBar, oLLBar) | Manual: rolling max/min + argmax/argmin | Manual: rolling max/min + argmax/argmin | Returns highest, lowest, and their bar offsets |
TrueHigh | np.maximum(highs, closes.shift(1)) | Same | max(High, Close[1]) |
TrueLow | np.minimum(lows, closes.shift(1)) | Same | min(Low, Close[1]) |
Range | highs - lows | Same | Per-bar range; no period |
NthHighest(N, Close, Len) | Manual: closes.rolling(Len).apply(lambda x: np.sort(x)[-N]) | Manual: sort rolling window | Nth largest value in window |
NthLowest(N, Close, Len) | Manual: closes.rolling(Len).apply(lambda x: np.sort(x)[N-1]) | Manual: sort rolling window | Nth smallest value in window |
NthHighestBar(N, Close, Len) | Manual: find bar offset of Nth highest | Manual: find bar offset of Nth highest | Returns bars ago of Nth highest |
NthLowestBar(N, Close, Len) | Manual: find bar offset of Nth lowest | Manual: find bar offset of Nth lowest | Returns bars ago of Nth lowest |
SwingHighBar(Occur, Price, Strength, Length) | Manual: pivot detection with bar offset | Manual: pivot detection with bar offset | Bars since Nth swing high; same arg order as SwingHigh |
SwingLowBar(Occur, Price, Strength, Length) | Manual: pivot detection with bar offset | Manual: pivot detection with bar offset | Bars since Nth swing low |
LinearRegAngle(Close, N) | np.degrees(np.arctan(ta.linreg(closes, length=N, slope=True))) | talib.LINEARREG_ANGLE(closes, timeperiod=N) | Slope converted to degrees |
Correlation(Close, Volume, N) | ta.correlation(closes, volumes, length=N) | talib.CORREL(closes, volumes, timeperiod=N) | Pearson correlation |
RSquared(Close, N) | ta.correlation(closes, ..., length=N) ** 2 | talib.CORREL(closes, ..., timeperiod=N) ** 2 | Square of correlation |
StdError(Close, N) | Manual: fit np.polyfit(np.arange(N), closes[-N:], 1), then np.sqrt(((y - yhat) ** 2).sum() / (N - 2)) | Manual: same residual computation on the window | Standard error of the linear-regression residuals — NOT stdev/sqrt(N) (that is SE of the mean) |
Median(Close, N) | ta.median(closes, length=N) | closes.rolling(N).median() | Rolling median |
ELDate(dt) | Manual: (y - 1900) * 10000 + m * 100 + d | Same | EasyLanguage YYYMMDD date format |
MinutesToTime(mins) | Manual: (mins // 60) * 100 + mins % 60 | Same | Minutes since midnight to HHMM |
TimeToMinutes(hhmm) | Manual: (hhmm // 100) * 60 + hhmm % 100 | Same | HHMM to minutes since midnight |
AvgPrice | (opens + highs + lows + closes) / 4 | talib.AVGPRICE(opens, highs, lows, closes) | Average of OHLC |
MedianPrice | (highs + lows) / 2 | talib.MEDPRICE(highs, lows) | Median of HL |
TypicalPrice | (highs + lows + closes) / 3 | talib.TYPPRICE(highs, lows, closes) | Typical price HLC |
WeightedClose | (highs + lows + 2 * closes) / 4 | talib.WCLPRICE(highs, lows, closes) | Weighted close HLCC |
MRO(cond, N, 1) | Manual: find Nth True in cond_series[-N:] | Manual: iterate lookback | Most Recent Occurrence; returns bars ago |
IFF(cond, trueVal, falseVal) | trueVal if cond else falseVal | Same | Python ternary; or np.where(cond, trueVal, falseVal) for Series |
TriAverage(Close, N) | ta.trima(closes, length=N) | talib.TRIMA(closes, timeperiod=N) | Triangular MA |
FastK(N) | ta.stoch(highs, lows, closes, k=N, d=1, smooth_k=1)['STOCHk_N_1_1'] | talib.STOCHF(highs, lows, closes, fastk_period=N) | Raw Fast %K |
FastD(N) | ta.stoch(highs, lows, closes, k=N, d=3, smooth_k=1)['STOCHd_N_3_1'] | talib.STOCHF(highs, lows, closes, fastk_period=N, fastd_period=3) | Smoothed Fast %D — set smooth_k=1 so %D is the SMA(3) of RAW %K, not of slowed %K |
SlowK(N) | ta.stoch(highs, lows, closes, k=N, d=3)['STOCHk_N_3_3'] | talib.STOCH(highs, lows, closes, fastk_period=N, slowk_period=3) | Slow %K |
SlowD(N) | ta.stoch(highs, lows, closes, k=N, d=3)['STOCHd_N_3_3'] | talib.STOCH(highs, lows, closes, fastk_period=N, slowk_period=3, slowd_period=3) | Slow %D |
FastKCustom(H, L, C, N) | ta.stoch(H, L, C, k=N, d=1, smooth_k=1)['STOCHk_...'] | talib.STOCHF(H, L, C, fastk_period=N) | Custom prices |
FastDCustom(H, L, C, N) | ta.stoch(H, L, C, k=N, d=3)['STOCHd_...'] | talib.STOCHF(H, L, C, fastk_period=N, fastd_period=3) | Custom prices |
SlowKCustom(H, L, C, N) | ta.stoch(H, L, C, k=N, d=3)['STOCHk_...'] | talib.STOCH(H, L, C, fastk_period=N, slowk_period=3) | Custom prices |
SlowDCustom(H, L, C, N) | ta.stoch(H, L, C, k=N, d=3)["STOCHd_N_3_3"] | talib.STOCH(H, L, C, fastk_period=N)[2] | Slow %D with custom prices |
StochasticExp(H, L, C, N, S1, S2, ...) | Manual: compute FastK then apply EMA smoothing | Manual: compute with talib.STOCHF then talib.EMA | Exponential smoothing variant |
ADXR(N) | ta.adx(highs, lows, closes, length=N) then (adx + adx.shift(N)) / 2 | talib.ADXR(highs, lows, closes, timeperiod=N) | TA-Lib has direct ADXR |
ADXCustom(H, L, C, N) | ta.adx(H, L, C, length=N) | talib.ADX(H, L, C, timeperiod=N) | Pass custom price Series |
DMI(N) | ta.adx(highs, lows, closes, length=N) | talib.ADX(highs, lows, closes, timeperiod=N) | Wrapper; same as ADX |
DMIPlusCustom(H, L, C, N) | ta.dm(H, L, length=N)['DMP_N'] | talib.PLUS_DI(H, L, C, timeperiod=N) | +DI with custom prices |
DMIMinusCustom(H, L, C, N) | ta.dm(H, L, length=N)['DMN_N'] | talib.MINUS_DI(H, L, C, timeperiod=N) | −DI with custom prices |
ParabolicCustom(step, limit) | ta.psar(highs, lows, af0=step, max_af=limit) | talib.SAR(highs, lows, acceleration=step, maximum=limit) | Parabolic SAR with custom limit |
TRIX(Close, N) | ta.trix(closes, length=N) | talib.TRIX(closes, timeperiod=N) | Triple EMA ROC |
MassIndex(SmoothLen, SumLen) | ta.massi(highs, lows, fast=SmoothLen, slow=SumLen) | Manual: EMA ratio sum | Mass Index |
EaseOfMovement | ta.eom(highs, lows, closes, volumes) | Manual: distance moved / box ratio | Requires HLCV |
SwingIndex | Manual: Wilder swing index formula | Manual: same | No library built-in |
AccumSwingIndex | Manual: cumulative sum of SwingIndex | Manual: same | No library built-in |
Detrend(Close, N) | Manual: closes - closes.rolling(N).mean().shift(N // 2 + 1) | Manual: offset SMA | Detrended price |
PercentChange(Close, N) | closes.pct_change(N) * 100 | talib.ROC(closes, timeperiod=N) | Percent change |
UlcerIndex(Close, N) | ta.ui(closes, length=N) | Manual: RMS of drawdown pct | Downside volatility |
ParabolicSAR(step, limit, ...) | ta.psar(highs, lows, af0=step, max_af=limit) | talib.SAR(highs, lows, acceleration=step, maximum=limit) | Multi-output; extract position from PSARl/PSARs columns |
LinearReg(Close, N, TgtBar, ...) | ta.linreg(closes, length=N) + manual slope/angle | talib.LINEARREG(closes, timeperiod=N) + talib.LINEARREG_SLOPE/ANGLE/INTERCEPT | Multi-output; combine four TA-Lib calls |
TrueRangeCustom(H, L, C) | ta.true_range(H, L, C) | talib.TRANGE(H, L, C) | Custom prices |
VolatilityStdDev(NumDays) | np.log(closes / closes.shift(1)).rolling(NumDays).std() * np.sqrt(252) | Manual: annualized stdev of log returns | Historical volatility |
StandardDevAnnual(Close, N, DataType) | ta.stdev(closes, length=N) * np.sqrt(252) | talib.STDDEV(closes, timeperiod=N) * np.sqrt(252) | Annualized stdev |
HighestFC(Close, N) | closes.rolling(N).max() | talib.MAX(closes, timeperiod=N) | Same as Highest |
LowestFC(Close, N) | closes.rolling(N).min() | talib.MIN(closes, timeperiod=N) | Same as Lowest |
PivotHighVS(Inst, Price, LStr, RStr, Len) | Manual: scan for pivot high with L/R strength | Manual: same | Asymmetric left/right strength |
PivotLowVS(Inst, Price, LStr, RStr, Len) | Manual: scan for pivot low with L/R strength | Manual: same | Same |
PivotHighVSBar(Inst, Price, LStr, RStr, Len) | Manual: bars ago of pivot high | Manual: same | Returns offset |
PivotLowVSBar(Inst, Price, LStr, RStr, Len) | Manual: bars ago of pivot low | Manual: same | Returns offset |
Divergence(P1, P2, Str, Len, HiLo) | Manual: compare pivot highs/lows of two series | Manual: same | No library built-in |
TimeSeriesForecast(Close, N) | ta.tsf(closes, length=N) | talib.TSF(closes, timeperiod=N) | Time Series Forecast |
SummationFC(Close, N) | closes.rolling(N).sum() | talib.SUM(closes, timeperiod=N) | Same as Summation |
OpenD(N) | df.resample('D').first()['open'].iloc[-1-N] | Manual: resample to daily | Daily open |
HighD(N) | df.resample('D').max()['high'].iloc[-1-N] | Manual: resample | Daily high |
LowD(N) | df.resample('D').min()['low'].iloc[-1-N] | Manual: resample | Daily low |
CloseD(N) | df.resample('D').last()['close'].iloc[-1-N] | Manual: resample | Daily close |
OpenW(N) | df.resample('W').first()['open'].iloc[-1-N] | Manual: resample | Weekly open |
HighW(N) | df.resample('W').max()['high'].iloc[-1-N] | Manual: resample | Weekly high |
LowW(N) | df.resample('W').min()['low'].iloc[-1-N] | Manual: resample | Weekly low |
CloseW(N) | df.resample('W').last()['close'].iloc[-1-N] | Manual: resample | Weekly close |
OpenM(N) | df.resample('ME').first()['open'].iloc[-1-N] | Manual: resample | Monthly open |
HighM(N) | df.resample('ME').max()['high'].iloc[-1-N] | Manual: resample | Monthly high |
LowM(N) | df.resample('ME').min()['low'].iloc[-1-N] | Manual: resample | Monthly low |
CloseM(N) | df.resample('ME').last()['close'].iloc[-1-N] | Manual: resample | Monthly close |
OpenY(N) | df.resample('YE').first()['open'].iloc[-1-N] | Manual: resample | Yearly open |
HighY(N) | df.resample('YE').max()['high'].iloc[-1-N] | Manual: resample | Yearly high |
LowY(N) | df.resample('YE').min()['low'].iloc[-1-N] | Manual: resample | Yearly low |
CloseY(N) | df.resample('YE').last()['close'].iloc[-1-N] | Manual: resample | Yearly close |
LRO(cond, N, Inst) | Manual: cond_series[-N:].iloc[::-1] find Nth True from end | Manual: same | Least Recent Occurrence |
SummationIf(cond, Price, N) | (Price * cond).rolling(N).sum() | Manual: multiply then sum | Conditional rolling sum |
IFFString(cond, trueStr, falseStr) | trueStr if cond else falseStr | Same | String ternary; or np.where |
OBV | ta.obv(closes, volumes) | talib.OBV(closes, volumes) | On Balance Volume |
VolumeROC(N) | ta.roc(volumes, length=N) | talib.ROC(volumes, timeperiod=N) | Volume rate of change |
VolumeOsc(ShortLen, LongLen) | ta.sma(volumes, length=ShortLen) - ta.sma(volumes, length=LongLen) | talib.SMA(volumes, ShortLen) - talib.SMA(volumes, LongLen) | Volume oscillator |
PriceVolTrend | ta.pvt(closes, volumes) | Manual: cumulative pct_change * volume | Price Volume Trend |
LWAccDis | Manual: ((closes - opens) / (highs - lows) * volumes).cumsum() | Manual: same | Larry Williams A/D |
Fisher(Price) | Manual: normalize, then 0.5 * np.log((1 + norm) / (1 - norm)) | Manual: same | Fisher transformation |
FisherINV(Price) | Manual: (np.exp(2 * Price) - 1) / (np.exp(2 * Price) + 1) | Manual: same | Inverse Fisher |
C_Doji(Pct) | Manual: abs(close - open) <= (high - low) * Pct / 100 | talib.CDLDOJI(opens, highs, lows, closes) | TA-Lib has CDL* family |
C_Hammer_HangingMan(Len, Factor, ...) | Manual: body/shadow ratios | talib.CDLHAMMER(...) / talib.CDLHANGINGMAN(...) | Separate TA-Lib functions |
C_BullEng_BearEng(Len, ...) | Manual: engulfing detection | talib.CDLENGULFING(opens, highs, lows, closes) | +100=bullish, -100=bearish |
C_BullHar_BearHar(Len, ...) | Manual: harami detection | talib.CDLHARAMI(opens, highs, lows, closes) | +100=bullish, -100=bearish |
C_MornDoji_EveDoji(Len, Pct, ...) | Manual: 3-bar doji star | talib.CDLMORNINGDOJISTAR(...) / talib.CDLEVENINGDOJISTAR(...) | Separate functions |
C_MornStar_EveStar(Len, ...) | Manual: 3-bar star | talib.CDLMORNINGSTAR(...) / talib.CDLEVENINGSTAR(...) | Separate functions |
C_PierceLine_DkCloud(Len, ...) | Manual: piercing/cloud | talib.CDLPIERCING(...) / talib.CDLDARKCLOUDCOVER(...) | Separate functions |
C_ShootingStar(Len, Factor) | Manual: shooting star | talib.CDLSHOOTINGSTAR(opens, highs, lows, closes) | TA-Lib direct |
C_3WhSolds_3BlkCrows(Len, Factor, ...) | Manual: 3-bar trend | talib.CDL3WHITESOLDIERS(...) / talib.CDL3BLACKCROWS(...) | Separate functions |
| Statistical extended | |||
AvgDeviation(Close, N) | ta.mad(closes, length=N) | Manual: (closes - closes.rolling(N).mean()).abs().rolling(N).mean() | Mean absolute deviation |
Variance(Close, N) | ta.variance(closes, length=N) | talib.VAR(closes, timeperiod=N) | Population variance |
Kurtosis(Close, N) | closes.rolling(N).kurt() | Manual: 4th moment | Excess kurtosis |
Skew(Close, N) | closes.rolling(N).skew() | Manual: 3rd moment | Skewness |
PercentRank(ValToRank, Price, N) | closes.rolling(N).apply(lambda w: stats.percentileofscore(w, w.iloc[-1])) | Manual | Percent rank |
Covariance(P1, P2, N) | P1.rolling(N).cov(P2, ddof=0) | Manual | Covariance — pandas defaults to SAMPLE covariance (ddof=1); pass ddof=0 for the population form PL uses |
Quartile(Close, N, Q) | closes.rolling(N).quantile(Q*0.25) | Manual | Quartile value |
TrimMean(Close, N, Pct) | closes.rolling(N).apply(lambda w: stats.trim_mean(w, Pct/100)) | Manual | Trimmed mean |
Mode(Close, N, Type) | closes.rolling(N).apply(lambda w: w.mode().iloc[0]) | Manual | Modal value |
HarmonicMean(Close, N) | closes.rolling(N).apply(stats.hmean) | Manual | Harmonic mean |
| Moving averages extended | |||
SmoothedAverage(Close, N) | ta.rma(closes, length=N) | Manual: Wilder smoothing | Wilder/RMA |
| Miscellaneous | |||
BarAnnualization | Manual: compute from bar frequency | Manual | Bars-per-year factor |
LastBarOnChart | idx == len(df) - 1 | Manual | True on last bar |
| Custom functions | |||
StochRSI(Close, N, M) | pandas_ta.stochrsi(df["close"], length=N, rsi_length=N, k=M) | pandas_ta.stochrsi() | Stochastic RSI |
supertrend(N, Mult) | pandas_ta.supertrend(df["high"], df["low"], df["close"], length=N, multiplier=Mult) | pandas_ta.supertrend() | Supertrend |
NVI(Start) | Manual: accumulate on volume-down bars | Manual | Negative Volume Index |
PVI(Start) | Manual: accumulate on volume-up bars | Manual | Positive Volume Index |
Coppo(N1, N2, N3) | pandas_ta.coppock(df["close"], length=N3, fast=N2, slow=N1) | pandas_ta.coppock() | Coppock Curve |
LWTI(Close, P, N) | Manual: (sma(close-close.shift(P), N) / sma(high-low, N)) * 50 + 50 | Manual | Larry Williams TI |
TVI(Close, Vol, Tick) | Manual: cumulative directional volume | Manual | Trade Volume Index |
SharpeRatio(Period, Rate, Calc, Cap) | Manual: (returns.mean() - rf) / returns.std() | Manual | Portfolio Sharpe |
WRSI(N, Close) | pandas_ta.rsi(df["close"], length=N) | pandas_ta.rsi() | Wilder RSI (default) |
NewMA(Close, N) | Manual: Heikin-Ashi + TEMA hybrid | Manual | Hybrid MA |
Table 4 — Strategy Orders
| PowerLanguage | Python | Notes |
|---|---|---|
Buy("label") next bar market | orders.append(Order(label="label", action=Action.ENTRY, side=Side.LONG, order_type=OrderType.MARKET)) | Append to orders list |
SellShort("label") next bar market | Order(action=Action.ENTRY, side=Side.SHORT, order_type=OrderType.MARKET) | Side.SHORT for short entry |
Sell("label") next bar market | Close long: Order(action=Action.EXIT, side=Side.LONG, order_type=OrderType.MARKET) | WARNING: PL Sell exits a long — do NOT map to Side.SHORT |
BuyToCover("label") next bar market | Close short: Order(action=Action.EXIT, side=Side.SHORT, order_type=OrderType.MARKET) | PL BuyToCover exits an existing short |
Buy("label") next bar at price limit | Order(order_type=OrderType.LIMIT, price=price) | Limit order |
Buy("label") next bar at price stop | Order(order_type=OrderType.STOP, price=price) | Stop order |
SetStopLoss(dollars) | stop_price = entry_price - dollars / (qty * point_value) | PL takes a dollar amount; Python must convert to price level |
SetProfitTarget(dollars) | target_price = entry_price + dollars / (qty * point_value) | Same dollar-to-price conversion |
Table 5 — Plotting
| PowerLanguage | Python | Notes |
|---|---|---|
Plot1(value, "label") | print(f"{label}: {value}") or log to file | No chart in Python; output to stdout, logging module, or results list |
SetPlotColor(1, Red) | N/A | No visual output; skip or store as metadata |
SetPlotWidth(1, 2) | N/A | Skip |
NoPlot(1) | Skip output for that bar | Conditional: don't append to results |
Table 6 — Control Flow
| PowerLanguage | Python | Notes |
|---|---|---|
If cond Then Begin ... End; | if cond: | Direct mapping |
If cond Then ... Else ... | if cond: ... else: ... | Direct mapping |
For idx = 1 to n Begin ... End; | for i in range(1, n + 1): | PL For is inclusive; Python range excludes upper bound. PL side uses idx because i is reserved (alias for OpenInterest) |
For idx = n DownTo 1 Begin ... End; | for i in range(n, 0, -1): | PL DownTo; Python reversed range |
While cond Begin ... End; | while cond: | Direct mapping |
Switch (expr) Begin Case 1: ... End; | match expr: with case 1: ... | Python 3.10+ match/case; no fallthrough (matches PL); PL empty case body is a compile error — use Value1 = Value1; as no-op |
Once Begin ... End; | if not self._once_done: ... self._once_done = True | Use a bool attribute; PL Once runs on first bar only |
Table 7 — Other Built-ins and Features
| PowerLanguage | Python | Notes |
|---|---|---|
MarketPosition | self.position (int: −1/0/+1) | Track manually; update on order fills |
EntryPrice | self.entry_price (float) | Track manually; set when position opens |
CurrentContracts | self.current_contracts (float) | Track manually; absolute contract count |
BarsSinceEntry | self.bars_since_entry (int) | Increment each bar while position != 0; reset on new entry |
Print("text") | print("text") | Direct mapping |
#BeginCmtry ... #EndCmtry | No equivalent | PL expert commentary; skip or log |
Alert("msg") | print("ALERT: msg", file=sys.stderr) | No built-in alert; write to stderr or use callback |
Part 2: Semantic Differences and Gotchas
Order fill timing — the #1 conversion bug
PL Buy next bar at market fills at the NEXT bar's OPEN, and next bar at X Stop / Limit fills intrabar on the next bar at the stop/limit price (or at the open if the bar gaps through it). A converted simulator that flips position on the signal bar (same-bar close fill) is one bar early and uses the wrong price.
The executor must queue orders produced on bar N and fill them against bar N+1:
for i in range(len(bars) - 1):
orders: list[Order] = []
strategy.on_bar(bars[: i + 1], orders) # orders queued on bar i...
nxt = bars[i + 1]
for o in orders: # ...filled on bar i+1
if o.order_type is OrderType.MARKET:
fill(o, price=nxt.open) # next bar's OPEN
elif o.order_type is OrderType.STOP and nxt.high >= o.price: # buy stop
fill(o, price=max(o.price, nxt.open)) # intrabar at the stop price
# sell stop: nxt.low <= o.price; limit orders mirror with reversed comparisons
EMA seeding/warmup gotcha: PL XAverage seeds recursively from the first bar's price. pandas-ta ema defaults to an SMA seed (sma=True) — pass sma=False to match PL, or document the difference. TA-Lib TA_EMA seeds with the SMA of the first Length values. Early-history signals therefore differ across targets — discard roughly the first 3×Length bars before comparing backtests.
-
Selldoes not mean "go short". In PowerLanguage,Sellexits an existing long position. The Python equivalent is closing the position (settingself.position = 0). Do not create a new short entry as a translation ofSell. -
PL is implicitly bar-driven; Python requires an explicit loop. PowerLanguage executes the entire script top-to-bottom on each bar close automatically. In Python, you must write the
for i in range(len(bars))loop yourself and callon_bar()on each iteration. Forgetting the loop wrapper means the strategy logic runs only once. -
Negative indexing is convenient but still needs bounds guarding.
bars[-2].closeis Pythonic for "previous bar" but raisesIndexErrorwhenlen(bars) < 2. Guard withif len(bars) > nbefore any lookback access. PL handles this automatically via MaxBarsBack; Python does not. -
pandas-ta operates on full Series, not streaming. Each
on_bar()call recomputes the indicator on the entire series up to the current bar. This is correct but O(n²) over a backtest. For performance, pre-compute indicators outside the loop or cache results. -
NaN values in indicator output. pandas-ta returns
NaNfor the firstlength - 1bars where the indicator has insufficient history. Always checkpd.notna(value)ornot math.isnan(value)before using the result. PL silently returns 0 for insufficient history; Python raises errors or produces silent NaN propagation. -
Mutable default arguments are a classic Python trap. Never write
def __init__(self, buf=[])— useNoneand assign inside:self.buf = buf if buf is not None else []. This doesn't affect PL conversion directly, but catches new Python users. -
Python
matchdoes not fall through. Unlike C++switch, Python 3.10+match/casehas no fallthrough. This matches PL'sSwitch/Casebehavior, so the mapping is direct. -
Float equality. Use
math.isclose(a, b)orabs(a - b) < 1e-10instead of==. PL uses tolerance-based comparison for=on float values; Python==on floats is exact bitwise comparison. -
No
Value1..Value99orCondition1..Condition99. These PL pre-declared variables must be replaced with named instance attributes. Use descriptive identifiers likeself.rsi_valueandself.is_oversold. -
Position state is not tracked automatically. PL provides
MarketPosition,EntryPrice,CurrentContracts, andBarsSinceEntryas built-in read-only variables updated by the engine. In Python, you must maintain these as instance attributes and update them manually whenever the execution engine fills an order. -
Dollar amounts versus price levels for stops.
SetStopLossandSetProfitTargetin PL accept a dollar (currency) amount. Python has no implicit conversion — you must compute the price level manually:stop_price = entry_price - stop_dollars / (qty * point_value). -
Multi-data series requires explicit design. PL
Close of Data2accesses a second feed bound to the chart. In Python, you must pass a second bar list (bars2: list[Bar]) toon_bar()or store it as an attribute. There is no implicit secondary-feed mechanism. -
No Portfolio Money Management (PMM) equivalent. PL's PMM allows a single script to govern position sizing across multiple instruments. Python has no built-in portfolio-level abstraction. You must implement cross-instrument logic as a separate orchestrator that calls individual strategy instances.
-
datetime.utcfromtimestampis deprecated in Python 3.12+. Usedatetime.fromtimestamp(ts, tz=timezone.utc)instead to avoid deprecation warnings.
Part 3: Conversion Checklists
PL → Python Pre-Conversion
- List every
Inputs:declaration →__init__parameter with default - List every
Variables:declaration → instance attribute in__init__ - Identify
Value1..99/Condition1..99→ plan meaningful replacement names - Identify all indicator calls → confirm pandas-ta / TA-Lib equivalent exists
- Locate
Data2/Data3references → plan multi-feed architecture - Locate
SetStopLoss/SetProfitTarget→ note point value for dollar→price conversion - Check
IntraBarPersist→ decide if tick-level behavior matters - Identify
.elffunction calls → plan Python function equivalents
PL → Python Post-Conversion
- Every
Sellmaps to closing the long, not entering short - Every
BuyToCovermaps to closing the short, not entering long - All bar lookback accesses guarded with
len(bars) > n - All indicator results checked for
NaNbefore use -
Crosses Over/Crosses Underimplemented as two-bar comparisons -
MarketPosition,EntryPrice,CurrentContracts,BarsSinceEntrytracked as instance attributes - Dollar-based stop/target amounts converted to price levels
-
Value1..99andCondition1..99renamed to descriptive typed attributes - Code runs with
python -Wall script.pywith zero errors and zero warnings - Strategy back-tested on same instrument/date range; trade count and P&L in same order of magnitude
Python → PL Pre-Conversion
- List all
__init__attributes → categorize: inputs (immutable) vs variables (mutable) vs indicator state - Identify all pandas-ta / TA-Lib calls → map each to PL built-in
- Check for
matchstatements with guard clauses → plan PLSwitchorIf/Else - Identify any libraries beyond pandas-ta/TA-Lib → plan PL equivalents
- Check for explicit position-tracking attributes → confirm PL built-ins suffice
Python → PL Post-Conversion
- Every short entry maps to
SellShort, notSell - Every long close maps to
Sell, notSellShort - All
bars[-1-n].closelookbacks converted toClose[n] - All manual crossover comparisons converted to PL
Crosses Over/Crosses Under - All attributes categorized: immutable →
Inputs:, mutable →Variables: - All pandas-ta/TA-Lib calls replaced with PL function calls
- NaN handling removed (PL handles lookback alignment internally)
- Price-level stops/targets converted to dollar amounts for
SetStopLoss/SetProfitTarget - Script compiled in MultiCharts PowerEditor with zero errors