Examples¶
The examples use finance-datagen so the inputs look like real financial
fixtures: generated price paths, OHLCV bars, signal panels, position panels,
and transaction logs. All generators are deterministic when seed is set, so
the snippets are suitable for documentation, tests, and notebooks.
Price Path to Return and Risk Metrics¶
Use generate_prices for a realistic close series, then derive returns and risk
metrics with expression calls.
import polars as pl
from finance_datagen import generate_prices
import finance_calcs as fc
prices = generate_prices(symbol="ACME", seed=7)
metrics = prices.with_columns(
pl.col("price").fcalcs.simple_returns().alias("ret"),
).select(
fc.returns(pl.col("ret")).alias("total_return"),
fc.annualized_return(pl.col("ret")).alias("ann_return"),
fc.volatility(pl.col("ret")).alias("ann_vol"),
fc.sharpe(pl.col("ret")).alias("sharpe"),
fc.max_drawdown(pl.col("ret")).alias("max_drawdown"),
)
The direct function style and namespace style are equivalent for expression metrics. Prefer the namespace when it improves pipeline readability, and use the top-level functions when several input columns are involved.
Calendar and Custom Period Slices¶
period= creates per-bucket metrics while preserving row-level output. Use
finance_enums.Frequency or a Polars duration string when the bucket can be
derived from a timestamp column.
import polars as pl
from finance_datagen import generate_prices
from finance_enums import Frequency
import finance_calcs as fc
prices = generate_prices(symbol="ACME", seed=11)
returns = prices.with_columns(
pl.col("price").fcalcs.simple_returns().alias("ret"),
)
monthly = returns.with_columns(
fc.period_bucket(pl.col("timestamp"), Frequency.Month).alias("month"),
pl.col("ret").fcalcs.returns(period=Frequency.Month, date=pl.col("timestamp")).alias("month_return"),
pl.col("ret").fcalcs.sharpe(period="1q", date=pl.col("timestamp")).alias("quarter_sharpe"),
)
For fiscal calendars, strategy regimes, or exchange-calendar grids generated
upstream, pass the bucket expression directly. In that case date= is not
required.
regime_metrics = returns.with_columns(
pl.when(pl.col("price") > pl.col("price").rolling_mean(63))
.then(pl.lit("above_trend"))
.otherwise(pl.lit("below_trend"))
.alias("regime"),
).with_columns(
fc.tail_ratio(pl.col("ret"), period=pl.col("regime")).alias("regime_tail_ratio"),
)
OHLCV Indicators¶
Use ohlc_from_close to turn any generated close path into bars. That gives the
high, low, open, close, and volume inputs needed by overlap, momentum,
volatility, and volume indicators.
import polars as pl
from finance_datagen import generate_prices, ohlc_from_close
prices = generate_prices(symbol="ACME", seed=5)
bars = ohlc_from_close(prices["price"], symbol="ACME", seed=5)
features = bars.with_columns(
pl.col("close").fcalcs.sma(20).alias("sma_20"),
pl.col("close").fcalcs.ema(20).alias("ema_20"),
pl.col("close").fcalcs.rsi(14).alias("rsi_14"),
pl.col("close").fcalcs.macd_line().alias("macd"),
fc.atr(pl.col("high"), pl.col("low"), pl.col("close"), period=14).alias("atr_14"),
fc.natr(pl.col("high"), pl.col("low"), pl.col("close"), period=14).alias("natr_14"),
fc.obv(pl.col("close"), pl.col("volume")).alias("obv"),
fc.adosc(pl.col("high"), pl.col("low"), pl.col("close"), pl.col("volume")).alias("adosc"),
)
For OHLC volatility estimators, use the same bars:
vol_features = bars.with_columns(
fc.parkinson_vol(pl.col("high"), pl.col("low"), period=20).alias("parkinson"),
fc.garman_klass_vol(pl.col("open"), pl.col("high"), pl.col("low"), pl.col("close"), period=20).alias("gk"),
fc.yang_zhang_vol(pl.col("open"), pl.col("high"), pl.col("low"), pl.col("close"), period=20).alias("yz"),
)
Cross-Sectional Alpha and Quantiles¶
generate_signal produces a long-form date, symbol, signal, fwd_returns panel
with a controlled information coefficient. Use over("date") for per-date
signal transforms and group_by("date") for per-date IC or spread metrics.
from datetime import date
import polars as pl
from finance_datagen import generate_signal
import finance_calcs as fc
signals = generate_signal(n_dates=60, n_assets=100, ic=0.06, start=date(2024, 1, 2), seed=3)
ranked = signals.with_columns(
fc.zscore(pl.col("signal")).over("date").alias("signal_z"),
fc.rank_normalize(pl.col("signal")).over("date").alias("signal_rank"),
fc.assign_quantile(pl.col("signal"), n_quantiles=5).over("date").alias("quantile"),
)
ic = ranked.group_by("date").agg(
fc.spearman_ic(pl.col("signal"), pl.col("fwd_returns")).alias("ic"),
fc.conditional_ic(pl.col("signal"), pl.col("fwd_returns"), pl.col("signal") > 0).alias("positive_signal_ic"),
fc.hit_rate(pl.col("signal"), pl.col("fwd_returns")).alias("hit_rate"),
fc.long_short_spread(pl.col("fwd_returns"), pl.col("quantile"), upper=4, lower=0).alias("q5_q1_spread"),
)
ic_summary = fc.ic_summary_stats(ic["ic"])
quantile_returns = ranked.group_by("date").agg(
*fc.mean_return_by_quantile(pl.col("fwd_returns"), pl.col("quantile"), n_quantiles=5),
)
horizon_panel = ranked.with_columns(
(pl.col("fwd_returns") * 0.8).alias("fwd_1"),
(pl.col("fwd_returns") * 0.5).alias("fwd_5"),
)
horizon_ic = horizon_panel.group_by("date").agg(
*fc.ic_decay(pl.col("signal"), {1: pl.col("fwd_1"), 5: pl.col("fwd_5")}),
)
To monitor quantile turnover, evaluate changes within each symbol and aggregate by date.
turnover = ranked.with_columns(
fc.quantile_changed(pl.col("quantile")).over("symbol").fill_null(False).alias("changed"),
).group_by("date").agg(
fc.quantile_turnover(pl.col("changed")).alias("quantile_turnover"),
)
Benchmark and Factor Metrics¶
generate_multi_asset_gbm gives a correlated multi-asset panel. A simple market
benchmark can be built as the cross-sectional average return for each timestamp.
Then factor metrics can be computed by symbol.
import polars as pl
from finance_datagen import generate_multi_asset_gbm
import finance_calcs as fc
panel = generate_multi_asset_gbm(
n_steps=252,
n_assets=5,
symbols=["AAA", "BBB", "CCC", "DDD", "EEE"],
rho=0.35,
seed=9,
).with_columns(
pl.col("price").fcalcs.simple_returns().over("symbol").alias("ret"),
).with_columns(
pl.col("ret").mean().over("timestamp").alias("benchmark"),
)
factor_metrics = panel.group_by("symbol").agg(
fc.beta(pl.col("ret"), pl.col("benchmark")).alias("beta"),
fc.alpha(pl.col("ret"), pl.col("benchmark")).alias("alpha"),
fc.tracking_error(pl.col("ret"), pl.col("benchmark")).alias("tracking_error"),
fc.information_ratio(pl.col("ret"), pl.col("benchmark")).alias("information_ratio"),
fc.up_capture(pl.col("ret"), pl.col("benchmark")).alias("up_capture"),
fc.down_capture(pl.col("ret"), pl.col("benchmark")).alias("down_capture"),
)
The same functions can be run with window= for rolling estimates or with
period= and date= for period-bucketed estimates.
Portfolio Exposures¶
generate_positions creates long-form position weights. Portfolio metrics are
intended for group_by("date") aggregations.
from datetime import date
import polars as pl
from finance_datagen import generate_positions
import finance_calcs as fc
positions = generate_positions(
n_dates=20,
n_assets=40,
gross_exposure=1.25,
start=date(2024, 1, 2),
exchange="XNYS",
currency="USD",
seed=4,
)
exposures = positions.group_by("date").agg(
fc.gross_leverage(pl.col("weight")).alias("gross"),
fc.net_exposure(pl.col("weight")).alias("net"),
fc.long_exposure(pl.col("weight")).alias("long"),
fc.short_exposure(pl.col("weight")).alias("short"),
fc.concentration(pl.col("weight")).alias("hhi"),
fc.top_n_concentration(pl.col("weight"), n=5).alias("top_5"),
)
To compute active share, join benchmark weights aligned by date and symbol, then aggregate.
with_benchmark = positions.with_columns(
(1.0 / pl.len().over("date")).alias("benchmark_weight"),
)
active = with_benchmark.group_by("date").agg(
fc.active_share(pl.col("weight"), pl.col("benchmark_weight")).alias("active_share"),
)
Post-Trade Costs and Turnover¶
generate_transactions produces side-aware transaction logs with prices,
quantities, commissions, fees, and slippage bps. Use row-wise expressions for
costs, then aggregate by date, symbol, or venue.
from datetime import date
import polars as pl
from finance_datagen import generate_transactions
import finance_calcs as fc
transactions = generate_transactions(
n_dates=10,
n_assets=20,
trades_per_day=30,
start=date(2024, 1, 2),
exchange="XNYS",
currency="USD",
seed=6,
)
costed = transactions.with_columns(
pl.col("timestamp").dt.date().alias("date"),
fc.transaction_notional(pl.col("amount"), pl.col("price")).alias("notional"),
fc.transaction_volume(pl.col("amount"), pl.col("price"), period="day", date=pl.col("timestamp")).alias("daily_notional"),
fc.transaction_cost(
pl.col("amount"),
pl.col("price"),
commission=pl.col("commission"),
fees=pl.col("fees"),
bps=pl.col("bps"),
).alias("cost"),
)
daily_costs = costed.group_by("date").agg(
pl.col("notional").sum().alias("gross_notional"),
pl.col("cost").sum().alias("total_cost"),
)
cost_breakdown = fc.cost_attribution(transactions)
Turnover starts from a position panel because it measures changes in weights:
turnover = positions.sort(["symbol", "date"]).with_columns(
fc.turnover(pl.col("weight")).over("symbol").alias("turnover_contribution"),
).group_by("date").agg(
pl.col("turnover_contribution").sum().alias("turnover"),
)
Round-trip and execution-quality helpers operate on concrete Polars frames because they need ordered trade sequences.
manual_transactions = pl.DataFrame(
{
"timestamp": [date(2024, 1, 2), date(2024, 1, 3), date(2024, 1, 4), date(2024, 1, 5)],
"symbol": ["ACME", "ACME", "ACME", "BETA"],
"amount": [100.0, -40.0, -60.0, -50.0],
"price": [100.0, 110.0, 95.0, 42.0],
}
)
round_trips = fc.extract_round_trips(manual_transactions)
trade_stats = fc.round_trip_stats(round_trips)
price_path = pl.DataFrame(
{
"timestamp": [date(2024, 1, 2), date(2024, 1, 3), date(2024, 1, 4)],
"symbol": ["ACME", "ACME", "ACME"],
"price": [100.0, 92.0, 112.0],
}
)
excursions = fc.mae_mfe(round_trips.filter(pl.col("symbol") == "ACME"), price_path)
execution_quality = pl.DataFrame(
{"side": ["Buy", "Sell"], "exec": [101.0, 99.0], "decision": [100.0, 100.0], "vwap": [100.5, 99.5]}
).select(
fc.implementation_shortfall(pl.col("exec"), pl.col("decision"), side=pl.col("side")).alias("is_bps"),
fc.vwap_slippage(pl.col("exec"), pl.col("vwap"), side=pl.col("side")).alias("vwap_bps"),
)
Series-Level Statistics¶
Some statistical helpers consume an eager pl.Series. Use generated return
series from the same price fixtures.
import polars as pl
from finance_datagen import generate_prices
import finance_calcs as fc
prices = generate_prices(n_steps=756, sigma=0.25, seed=12)
returns = prices.select(pl.col("price").fcalcs.simple_returns().alias("ret"))["ret"].drop_nulls()
psr = fc.probabilistic_sharpe(returns, benchmark_sr=0.0)
ds = fc.deflated_sharpe(returns, n_trials=20)
min_obs = fc.minimum_track_record_length(returns, benchmark_sr=0.5)
sharpe, lower, upper = fc.sharpe_ci_bootstrap(returns, seed=12)
gpd_var = fc.gpd_var(returns, var_p=0.01)
gpd_cvar = fc.gpd_cvar(returns, var_p=0.01)