From 900919aed80304b8569869cf8c5b21a4d2035b4d Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:10:20 -0400 Subject: [PATCH] Update code for ruff 16 --- bt/algos.py | 185 +++++++++++++++++++++----------------------- bt/backtest.py | 14 ++-- bt/core.py | 111 ++++++++++++-------------- docs/source/conf.py | 8 +- pyproject.toml | 2 +- 5 files changed, 148 insertions(+), 172 deletions(-) diff --git a/bt/algos.py b/bt/algos.py index 4eae62d3..d66eb27f 100644 --- a/bt/algos.py +++ b/bt/algos.py @@ -3,9 +3,9 @@ """ import abc +import math import random import re -import math import numpy as np import pandas as pd @@ -14,6 +14,9 @@ import bt from bt.core import Algo, AlgoStack, SecurityBase, is_zero +_DEFAULT_LOOKBACK = pd.DateOffset(months=3) +_DEFAULT_LAG = pd.DateOffset(days=0) + def run_always(f): """ @@ -50,7 +53,7 @@ class PrintTempData(Algo): """ def __init__(self, fmt_string=None): - super(PrintTempData, self).__init__() + super().__init__() self.fmt_string = fmt_string def __call__(self, target): @@ -81,7 +84,7 @@ class PrintInfo(Algo): """ def __init__(self, fmt_string="{name} {now}"): - super(PrintInfo, self).__init__() + super().__init__() self.fmt_string = fmt_string def __call__(self, target): @@ -98,9 +101,9 @@ class Debug(Algo): """ def __call__(self, target): - import pdb + import pdb # noqa: T100 - pdb.set_trace() + pdb.set_trace() # noqa: T100 return True @@ -117,7 +120,7 @@ class RunOnce(Algo): """ def __init__(self): - super(RunOnce, self).__init__() + super().__init__() self.has_run = False def __call__(self, target): @@ -133,7 +136,7 @@ def __call__(self, target): class RunPeriod(Algo): def __init__(self, run_on_first_date=True, run_on_end_of_period=False, run_on_last_date=False): - super(RunPeriod, self).__init__() + super().__init__() self._run_on_first_date = run_on_first_date self._run_on_end_of_period = run_on_end_of_period self._run_on_last_date = run_on_last_date @@ -203,9 +206,7 @@ class RunDaily(RunPeriod): """ def compare_dates(self, now, date_to_compare): - if now.date() != date_to_compare.date(): - return True - return False + return now.date() != date_to_compare.date() class RunWeekly(RunPeriod): @@ -225,9 +226,7 @@ class RunWeekly(RunPeriod): """ def compare_dates(self, now, date_to_compare): - if now.year != date_to_compare.year or now.week != date_to_compare.week: - return True - return False + return bool(now.year != date_to_compare.year or now.week != date_to_compare.week) class RunMonthly(RunPeriod): @@ -247,9 +246,7 @@ class RunMonthly(RunPeriod): """ def compare_dates(self, now, date_to_compare): - if now.year != date_to_compare.year or now.month != date_to_compare.month: - return True - return False + return bool(now.year != date_to_compare.year or now.month != date_to_compare.month) class RunQuarterly(RunPeriod): @@ -269,9 +266,7 @@ class RunQuarterly(RunPeriod): """ def compare_dates(self, now, date_to_compare): - if now.year != date_to_compare.year or now.quarter != date_to_compare.quarter: - return True - return False + return bool(now.year != date_to_compare.year or now.quarter != date_to_compare.quarter) class RunYearly(RunPeriod): @@ -291,9 +286,7 @@ class RunYearly(RunPeriod): """ def compare_dates(self, now, date_to_compare): - if now.year != date_to_compare.year: - return True - return False + return now.year != date_to_compare.year class RunOnDate(Algo): @@ -312,7 +305,7 @@ def __init__(self, *dates): by pandas.to_datetime so pass anything that it can parse. Typically, you will pass a string 'yyyy-mm-dd'. """ - super(RunOnDate, self).__init__() + super().__init__() # parse dates and save self.dates = [pd.to_datetime(d) for d in dates] @@ -338,7 +331,7 @@ def __init__(self, date): Args: * date: Date after which to start trading """ - super(RunAfterDate, self).__init__() + super().__init__() # parse dates and save self.date = pd.to_datetime(date) @@ -364,7 +357,7 @@ def __init__(self, days): Args: * days (int): Number of trading days to wait before starting """ - super(RunAfterDays, self).__init__() + super().__init__() self.days = days def __call__(self, target): @@ -395,7 +388,7 @@ class RunIfOutOfBounds(Algo): def __init__(self, tolerance): self.tolerance = float(tolerance) - super(RunIfOutOfBounds, self).__init__() + super().__init__() def __call__(self, target): if "weights" not in target.temp: @@ -436,7 +429,7 @@ class RunEveryNPeriods(Algo): """ def __init__(self, n, offset=0): - super(RunEveryNPeriods, self).__init__() + super().__init__() self.n = n self.offset = offset self.idx = n - offset - 1 @@ -475,7 +468,7 @@ class SelectAll(Algo): """ def __init__(self, include_no_data=False, include_negative=False): - super(SelectAll, self).__init__() + super().__init__() self.include_no_data = include_no_data self.include_negative = include_negative @@ -507,7 +500,7 @@ class SelectThese(Algo): """ def __init__(self, tickers, include_no_data=False, include_negative=False): - super(SelectThese, self).__init__() + super().__init__() self.tickers = tickers self.include_no_data = include_no_data self.include_negative = include_negative @@ -562,12 +555,12 @@ class SelectHasData(Algo): def __init__( self, - lookback=pd.DateOffset(months=3), + lookback=_DEFAULT_LOOKBACK, min_count=None, include_no_data=False, include_negative=False, ): - super(SelectHasData, self).__init__() + super().__init__() self.lookback = lookback if min_count is None: min_count = bt.ffn.get_num_days_required(lookback) @@ -619,7 +612,7 @@ class SelectN(Algo): """ def __init__(self, n, sort_descending=True, all_or_none=False, filter_selected=False): - super(SelectN, self).__init__() + super().__init__() if n < 0: raise ValueError("n cannot be negative") self.n = n @@ -679,12 +672,12 @@ class SelectMomentum(AlgoStack): def __init__( self, n, - lookback=pd.DateOffset(months=3), - lag=pd.DateOffset(days=0), + lookback=_DEFAULT_LOOKBACK, + lag=_DEFAULT_LAG, sort_descending=True, all_or_none=False, ): - super(SelectMomentum, self).__init__( + super().__init__( StatTotalReturn(lookback=lookback, lag=lag), SelectN(n=n, sort_descending=sort_descending, all_or_none=all_or_none), ) @@ -714,7 +707,7 @@ class SelectWhere(Algo): """ def __init__(self, signal, include_no_data=False, include_negative=False): - super(SelectWhere, self).__init__() + super().__init__() if isinstance(signal, pd.DataFrame): self.signal_name = None self.signal = signal @@ -736,7 +729,7 @@ def __call__(self, target): sig = signal.loc[target.now] # get tickers where True # selected = sig.index[sig] - selected = sig[sig == True].index # noqa: E712 + selected = sig[sig == True].index # save as list if not self.include_no_data: universe = target.universe.loc[target.now, list(selected)].dropna() @@ -782,7 +775,7 @@ class SelectRandomly(AlgoStack): """ def __init__(self, n=None, include_no_data=False, include_negative=False): - super(SelectRandomly, self).__init__() + super().__init__() self.n = n self.include_no_data = include_no_data self.include_negative = include_negative @@ -801,7 +794,7 @@ def __call__(self, target): sel = list(universe[universe > 0].index) if self.n is not None: - n = self.n if self.n < len(sel) else len(sel) + n = min(len(sel), self.n) sel = random.sample(sel, int(n)) target.temp["selected"] = sel @@ -824,7 +817,7 @@ class SelectRegex(Algo): """ def __init__(self, regex): - super(SelectRegex, self).__init__() + super().__init__() self.regex = re.compile(regex) def __call__(self, target): @@ -859,7 +852,7 @@ class ResolveOnTheRun(Algo): """ def __init__(self, on_the_run, include_no_data=False, include_negative=False): - super(ResolveOnTheRun, self).__init__() + super().__init__() self.on_the_run = on_the_run self.include_no_data = include_no_data self.include_negative = include_negative @@ -894,7 +887,7 @@ class SetStat(Algo): * stat """ - def __init__(self, stat, lag=pd.DateOffset(days=0)): + def __init__(self, stat, lag=_DEFAULT_LAG): self.lag = lag if isinstance(stat, pd.DataFrame): self.stat_name = None @@ -938,8 +931,8 @@ class StatTotalReturn(Algo): """ - def __init__(self, lookback=pd.DateOffset(months=3), lag=pd.DateOffset(days=0)): - super(StatTotalReturn, self).__init__() + def __init__(self, lookback=_DEFAULT_LOOKBACK, lag=_DEFAULT_LAG): + super().__init__() self.lookback = lookback self.lag = lag @@ -969,7 +962,7 @@ class WeighEqually(Algo): """ def __init__(self): - super(WeighEqually, self).__init__() + super().__init__() def __call__(self, target): selected = target.temp["selected"] @@ -999,7 +992,7 @@ class WeighSpecified(Algo): """ def __init__(self, **weights): - super(WeighSpecified, self).__init__() + super().__init__() self.weights = weights def __call__(self, target): @@ -1026,7 +1019,7 @@ class ScaleWeights(Algo): """ def __init__(self, scale): - super(ScaleWeights, self).__init__() + super().__init__() self.scale = scale def __call__(self, target): @@ -1061,7 +1054,7 @@ class WeighTarget(Algo): """ def __init__(self, weights): - super(WeighTarget, self).__init__() + super().__init__() if isinstance(weights, pd.DataFrame): self.weights_name = None self.weights = weights @@ -1107,8 +1100,8 @@ class WeighInvVol(Algo): """ - def __init__(self, lookback=pd.DateOffset(months=3), lag=pd.DateOffset(days=0)): - super(WeighInvVol, self).__init__() + def __init__(self, lookback=_DEFAULT_LOOKBACK, lag=_DEFAULT_LAG): + super().__init__() self.lookback = lookback self.lag = lag @@ -1175,16 +1168,16 @@ class WeighERC(Algo): def __init__( self, - lookback=pd.DateOffset(months=3), + lookback=_DEFAULT_LOOKBACK, initial_weights=None, risk_weights=None, covar_method="ledoit-wolf", risk_parity_method="ccd", maximum_iterations=100, tolerance=1e-8, - lag=pd.DateOffset(days=0), + lag=_DEFAULT_LAG, ): - super(WeighERC, self).__init__() + super().__init__() self.lookback = lookback self.initial_weights = initial_weights self.risk_weights = risk_weights @@ -1249,13 +1242,13 @@ class WeighMeanVar(Algo): def __init__( self, - lookback=pd.DateOffset(months=3), + lookback=_DEFAULT_LOOKBACK, bounds=(0.0, 1.0), covar_method="ledoit-wolf", rf=0.0, - lag=pd.DateOffset(days=0), + lag=_DEFAULT_LAG, ): - super(WeighMeanVar, self).__init__() + super().__init__() self.lookback = lookback self.lag = lag self.bounds = bounds @@ -1315,7 +1308,7 @@ class WeighRandomly(Algo): """ def __init__(self, bounds=(0.0, 1.0), weight_sum=1): - super(WeighRandomly, self).__init__() + super().__init__() self.bounds = bounds self.weight_sum = weight_sum @@ -1362,7 +1355,7 @@ class LimitDeltas(Algo): """ def __init__(self, limit=0.1): - super(LimitDeltas, self).__init__() + super().__init__() self.limit = limit # determine if global or specific self.global_limit = True @@ -1374,7 +1367,7 @@ def __call__(self, target): all_keys = set(list(target.children.keys()) + list(tw.keys())) for k in all_keys: - tgt = tw[k] if k in tw else 0.0 + tgt = tw.get(k, 0.0) cur = target.children[k].weight if k in target.children else 0.0 delta = tgt - cur @@ -1417,7 +1410,7 @@ class LimitWeights(Algo): """ def __init__(self, limit=0.1): - super(LimitWeights, self).__init__() + super().__init__() self.limit = limit def __call__(self, target): @@ -1462,12 +1455,12 @@ class TargetVol(Algo): def __init__( self, target_volatility, - lookback=pd.DateOffset(months=3), - lag=pd.DateOffset(days=0), + lookback=_DEFAULT_LOOKBACK, + lag=_DEFAULT_LAG, covar_method="standard", annualization_factor=252, ): - super(TargetVol, self).__init__() + super().__init__() self.target_volatility = target_volatility self.lookback = lookback self.lag = lag @@ -1499,10 +1492,10 @@ def __call__(self, target): vol = np.sqrt(np.matmul(weights.values.T, np.matmul(covar.values, weights.values)) * self.annualization_factor) if isinstance(self.target_volatility, (float, int)): - self.target_volatility = {k: self.target_volatility for k in target.temp["weights"].keys()} + self.target_volatility = {k: self.target_volatility for k in target.temp["weights"]} - for k in target.temp["weights"].keys(): - if k in self.target_volatility.keys(): + for k in target.temp["weights"]: + if k in self.target_volatility: target.temp["weights"][k] = target.temp["weights"][k] * self.target_volatility[k] / vol return True @@ -1527,12 +1520,12 @@ def __init__( self, PTE_volatility_cap, target_weights, - lookback=pd.DateOffset(months=3), - lag=pd.DateOffset(days=0), + lookback=_DEFAULT_LOOKBACK, + lag=_DEFAULT_LAG, covar_method="standard", annualization_factor=252, ): - super(PTE_Rebalance, self).__init__() + super().__init__() self.PTE_volatility_cap = PTE_volatility_cap self.target_weights = target_weights self.lookback = lookback @@ -1559,7 +1552,7 @@ def __call__(self, target): target_weights = self.target_weights.loc[target.now, :] cols = list(current_weights.index.copy()) - for c in target_weights.keys(): + for c in target_weights.keys(): # noqa: SIM118 if c not in cols: cols.append(c) @@ -1587,10 +1580,7 @@ def __call__(self, target): if pd.isnull(PTE_vol): return False # vol is too high - if PTE_vol > self.PTE_volatility_cap: - return True - else: - return False + return PTE_vol > self.PTE_volatility_cap return True @@ -1619,7 +1609,7 @@ def __init__(self, amount): Args: * amount (float): Amount to adjust by """ - super(CapitalFlow, self).__init__() + super().__init__() self.amount = float(amount) def __call__(self, target): @@ -1660,7 +1650,7 @@ class CorporateActions(Algo): """ def __init__(self, dividends, splits): - super(CorporateActions, self).__init__() + super().__init__() self.dividends = dividends.fillna(0.0) self.splits = splits.fillna(1.0) @@ -1707,7 +1697,7 @@ class CloseDead(Algo): """ def __init__(self): - super(CloseDead, self).__init__() + super().__init__() def __call__(self, target): if "weights" not in target.temp: @@ -1738,7 +1728,7 @@ class SetNotional(Algo): def __init__(self, notional_value): self.notional_value = notional_value - super(SetNotional, self).__init__() + super().__init__() def __call__(self, target): notional_value = target.get_data(self.notional_value) @@ -1772,7 +1762,7 @@ class Rebalance(Algo): """ def __init__(self): - super(Rebalance, self).__init__() + super().__init__() def __call__(self, target): if "weights" not in target.temp: @@ -1857,7 +1847,7 @@ class RebalanceOverTime(Algo): """ def __init__(self, n=10): - super(RebalanceOverTime, self).__init__() + super().__init__() self.n = float(n) self._rb = Rebalance() self._weights = None @@ -1874,7 +1864,7 @@ def __call__(self, target): tgt = {} # scale delta relative to # of periods left and set that as the new # target - for cname in self._weights.keys(): + for cname in self._weights: curr = target.children[cname].weight if cname in target.children else 0.0 dlt = (self._weights[cname] - curr) / self._days_left tgt[cname] = curr + dlt @@ -1918,7 +1908,7 @@ class Require(Algo): """ def __init__(self, pred, item, if_none=False): - super(Require, self).__init__() + super().__init__() self.item = item self.pred = pred self.if_none = if_none @@ -1947,7 +1937,7 @@ class Not(Algo): """ def __init__(self, algo): - super(Not, self).__init__() + super().__init__() self._algo = algo def __call__(self, target): @@ -1974,9 +1964,8 @@ class Or(Algo): """ def __init__(self, list_of_algos): - super(Or, self).__init__() + super().__init__() self._list_of_algos = list_of_algos - return def __call__(self, target): res = False @@ -2002,7 +1991,7 @@ class SelectTypes(Algo): """ def __init__(self, include_types=(bt.core.Node,), exclude_types=()): - super(SelectTypes, self).__init__() + super().__init__() self.include_types = include_types self.exclude_types = exclude_types or (type(None),) @@ -2040,7 +2029,7 @@ class ClosePositionsAfterDates(Algo): """ def __init__(self, close_dates): - super(ClosePositionsAfterDates, self).__init__() + super().__init__() self.close_dates = close_dates def __call__(self, target): @@ -2088,7 +2077,7 @@ class RollPositionsAfterDates(Algo): """ def __init__(self, roll_data): - super(RollPositionsAfterDates, self).__init__() + super().__init__() self.roll_data = roll_data def __call__(self, target): @@ -2170,7 +2159,7 @@ class ReplayTransactions(Algo): """ def __init__(self, transactions): - super(ReplayTransactions, self).__init__() + super().__init__() self.transactions = transactions def __call__(self, target): @@ -2213,7 +2202,7 @@ class SimulateRFQTransactions(Algo): """ def __init__(self, rfqs, model): - super(SimulateRFQTransactions, self).__init__() + super().__init__() self.rfqs = rfqs self.model = model @@ -2247,7 +2236,7 @@ def _get_unit_risk(security, data, index=None): try: unit_risks = data[security] unit_risk = unit_risks.iloc[index] - except Exception: + except Exception: # noqa: BLE001 # No risk data, assume zero unit_risk = 0.0 return unit_risk @@ -2274,7 +2263,7 @@ class UpdateRisk(Algo): """ def __init__(self, measure, history=0): - super(UpdateRisk, self).__init__(name="UpdateRisk>%s" % measure) + super().__init__(name=f"UpdateRisk>{measure}") self.measure = measure self.history = history @@ -2335,7 +2324,7 @@ class PrintRisk(Algo): """ def __init__(self, fmt_string=""): - super(PrintRisk, self).__init__() + super().__init__() self.fmt_string = fmt_string def __call__(self, target): @@ -2374,7 +2363,7 @@ class HedgeRisks(Algo): """ def __init__(self, measures, pseudo=False, strategy=None, throw_nan=True): - super(HedgeRisks, self).__init__() + super().__init__() if len(measures) == 0: raise ValueError("Must pass in at least one measure to hedge") self.measures = measures @@ -2384,9 +2373,9 @@ def __init__(self, measures, pseudo=False, strategy=None, throw_nan=True): def _get_target_risk(self, target, measure): if not hasattr(target, "risk"): - raise ValueError("risk not set up on target %s" % target.name) + raise ValueError(f"risk not set up on target {target.name}") if measure not in target.risk: - raise ValueError("measure %s not set on target %s" % (measure, target.name)) + raise ValueError(f"measure {measure} not set on target {target.name}") return target.risk[measure] def __call__(self, target): @@ -2406,7 +2395,7 @@ def __call__(self, target): for m in self.measures: d = target.get_data("unit_risk").get(m) if d is None: - raise ValueError("unit_risk for %s not present in temp on %s" % (self.measure, target.name)) + raise ValueError(f"unit_risk for {self.measure} not present in temp on {target.name}") i = d.index.get_loc(target.now) data.append((i, d)) @@ -2422,7 +2411,7 @@ def __call__(self, target): # Hedge for notional, security in zip(notionals, securities): if np.isnan(notional) and self.throw_nan: - raise ValueError("%s has nan hedge notional" % security) + raise ValueError(f"{security} has nan hedge notional") target.transact(notional, security) return True diff --git a/bt/backtest.py b/bt/backtest.py index 20d908d8..8b6317a9 100644 --- a/bt/backtest.py +++ b/bt/backtest.py @@ -76,7 +76,7 @@ def benchmark_random(backtest, random_strategy, nsim=100): # create and run random backtests for i in tqdm(range(nsim)): - random_strategy.name = "random_%s" % i + random_strategy.name = f"random_{i}" rbt = bt.Backtest(random_strategy, data) rbt.run() @@ -88,7 +88,7 @@ def benchmark_random(backtest, random_strategy, nsim=100): return res -class Backtest(object): +class Backtest: """ A Backtest combines a Strategy with data to produce a Result. @@ -174,7 +174,7 @@ def __init__( ): if data.columns.duplicated().any(): cols = data.columns[data.columns.duplicated().tolist()].tolist() - raise Exception("data provided has some duplicate column names: \n%s \nPlease remove duplicates!" % cols) + raise ValueError(f"data provided has some duplicate column names: \n{cols} \nPlease remove duplicates!") # we want to reuse strategy logic - copy it! # basically strategy is a template @@ -470,7 +470,7 @@ class Result(ffn.GroupStats): def __init__(self, *backtests): tmp = [pd.DataFrame({x.name: x.strategy.prices}) for x in backtests] - super(Result, self).__init__(*tmp) + super().__init__(*tmp) self.backtest_list = backtests self.backtests = {x.name: x for x in backtests} @@ -619,7 +619,7 @@ class RandomBenchmarkResult(Result): """ def __init__(self, *backtests): - super(RandomBenchmarkResult, self).__init__(*backtests) + super().__init__(*backtests) self.base_name = backtests[0].name # seperate stats to make self.r_stats = self.stats.drop(self.base_name, axis=1) @@ -648,7 +648,7 @@ def plot_histogram(self, statistic="monthly_sharpe", figsize=(15, 5), title=None raise ValueError("Invalid statistic. Valid statisticsare the statistics in self.stats") if title is None: - title = "%s histogram" % statistic + title = f"{statistic} histogram" plt.figure(figsize=figsize) @@ -684,7 +684,7 @@ class RenormalizedFixedIncomeResult(Result): def __init__(self, normalizing_value, *backtests): for backtest in backtests: if not backtest.strategy.fixed_income: - raise ValueError("Cannot apply RenormalizedFixedIncomeResult because backtest %s is not on a fixed income strategy" % backtest.name) + raise ValueError(f"Cannot apply RenormalizedFixedIncomeResult because backtest {backtest.name} is not on a fixed income strategy") if not isinstance(normalizing_value, dict): normalizing_value = {x.name: normalizing_value for x in backtests} tmp = [pd.DataFrame({x.name: self._price(x.strategy, normalizing_value[x.name])}) for x in backtests] diff --git a/bt/core.py b/bt/core.py index f40cbd5e..de7670a4 100644 --- a/bt/core.py +++ b/bt/core.py @@ -21,7 +21,7 @@ def is_zero(x): return abs(x) < TOL -class Node(object): +class Node: """ The Node is the main building block in bt's tree structure design. Both StrategyBase and SecurityBase inherit Node. It contains the @@ -169,7 +169,7 @@ def _add_children(self, children, dc): if isinstance(c, str): if c in self._universe_tickers: - raise ValueError("Child %s already exists" % c) + raise ValueError(f"Child {c} already exists") # Create default security with lazy_add c = Security(c, lazy_add=True) @@ -178,7 +178,7 @@ def _add_children(self, children, dc): self._lazy_children[c.name] = c else: if c.name in self.children: - raise ValueError("Child %s already exists" % c) + raise ValueError(f"Child {c} already exists") c.parent = self c._set_root(self.root) @@ -312,21 +312,21 @@ def full_name(self): if self.parent == self: return self.name else: - return "%s>%s" % (self.parent.full_name, self.name) + return f"{self.parent.full_name}>{self.name}" def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, self.full_name) + return f"<{self.__class__.__name__} {self.full_name}>" def to_dot(self, root=True): """ Represent the node structure in DOT format. """ - name = lambda x: x.name or repr(self) # noqa: E731 - edges = "\n".join('\t"%s" -> "%s"' % (name(self), name(c)) for c in self.children.values()) + name = lambda x: x.name or repr(self) + edges = "\n".join(f'\t"{name(self)}" -> "{name(c)}"' for c in self.children.values()) below = "\n".join(c.to_dot(False) for c in self.children.values()) - body = "\n".join([edges, below]).rstrip() + body = f"{edges}\n{below}".rstrip() if root: - return "\n".join(["digraph {", body, "}"]) + return f"digraph {{\n{body}\n}}" return body @@ -488,7 +488,7 @@ def bidoffer_paid(self): self.root.update(self.now, None) return self._bidoffer_paid else: - raise Exception('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') + raise RuntimeError('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') @property def bidoffers_paid(self): @@ -500,7 +500,7 @@ def bidoffers_paid(self): self.root.update(self.now, None) return self._bidoffers_paid.loc[: self.now] else: - raise Exception('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') + raise RuntimeError('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') @property def universe(self): @@ -573,7 +573,7 @@ def setup(self, universe, **kwargs): # strategies as the "price" is just a reference # value and should not be used for capital allocation if self.fixed_income and not self.parent.fixed_income: - raise ValueError("Cannot have fixed income strategy child (%s) of non-fixed income strategy (%s)" % (self.name, self.parent.name)) + raise ValueError(f"Cannot have fixed income strategy child ({self.name}) of non-fixed income strategy ({self.parent.name})") # determine if needs paper trading # and setup if so @@ -745,11 +745,10 @@ def update(self, date, data=None, inow=None): self._capital += coupons val += coupons - if self.root == self: - if (val < 0) and not self.bankrupt and not self.fixed_income and not is_zero(val): - # Declare a bankruptcy - self.bankrupt = True - self.flatten() + if self.root == self and (val < 0) and not self.bankrupt and not self.fixed_income and not is_zero(val): + # Declare a bankruptcy + self.bankrupt = True + self.flatten() # update data if this value is different or # if now has changed - avoid all this if not since it @@ -778,10 +777,10 @@ def update(self, date, data=None, inow=None): ret = 0 else: raise ZeroDivisionError( - "Could not update %s on %s. Last notional value " - "was %s and pnl was %s. Therefore, " + f"Could not update {self.name} on {self.now}. Last notional value " + f"was {self._last_notl_value} and pnl was {pnl}. Therefore, " "we are dividing by zero to obtain the pnl " - "per unit notional for the period." % (self.name, self.now, self._last_notl_value, pnl) + "per unit notional for the period." ) self._price = self._last_price + ret @@ -802,18 +801,11 @@ def update(self, date, data=None, inow=None): ret = 0 else: raise ZeroDivisionError( - "Could not update %s on %s. Last value " - "was %s and net flows were %s. Current" - "value is %s. Therefore, " + f"Could not update {self.name} on {self.now}. Last value " + f"was {self._last_value} and net flows were {self._net_flows}. Current" + f"value is {self._value}. Therefore, " "we are dividing by zero to obtain the return " "for the period." - % ( - self.name, - self.now, - self._last_value, - self._net_flows, - self._value, - ) ) self._price = self._last_price * (1 + ret) @@ -1077,7 +1069,6 @@ def run(self): algorithm to execute on each date change. This method is called by backtester. """ - pass def set_commissions(self, fn): """ @@ -1317,7 +1308,7 @@ def bidoffers(self): self.update(self.root.now) return self._bidoffers.loc[: self.now] else: - raise Exception('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') + raise RuntimeError('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') @property def bidoffer_paid(self): @@ -1342,7 +1333,7 @@ def bidoffers_paid(self): self.root.update(self.root.now, None) return self._bidoffers_paid.loc[: self.now] else: - raise Exception('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') + raise RuntimeError('Bid/offer accounting not turned on: "bidoffer" argument not provided during setup') def setup(self, universe, **kwargs): """ @@ -1467,7 +1458,7 @@ def update(self, date, data=None, inow=None): if is_zero(self._position): self._value = 0 else: - raise Exception("Position is open (non-zero: %s) and latest price is NaN for security %s on %s. Cannot update node value." % (self._position, self.name, date)) + raise ValueError(f"Position is open (non-zero: {self._position}) and latest price is NaN for security {self.name} on {date}. Cannot update node value.") else: self._value = self._position * self._price * self.multiplier @@ -1522,10 +1513,10 @@ def allocate(self, amount, update=True): return if self.parent is self or self.parent is None: - raise Exception("Cannot allocate capital to a parentless security") + raise RuntimeError("Cannot allocate capital to a parentless security") if is_zero(self._price) or np.isnan(self._price): - raise Exception("Cannot allocate capital to %s because price is %s as of %s" % (self.name, self._price, self.parent.now)) + raise ValueError(f"Cannot allocate capital to {self.name} because price is {self._price} as of {self.parent.now}") # buy/sell # determine quantity - must also factor in commission @@ -1559,7 +1550,7 @@ def allocate(self, amount, update=True): # sell additional units to fund this requirement. As such, q must once # again decrease. # - if not q == -self._position: + if q != -self._position: full_outlay, _, _, _ = self.outlay(q) # if full outlay > amount, we must decrease the magnitude of `q` @@ -1602,7 +1593,7 @@ def allocate(self, amount, update=True): i = i + 1 if i > 1e4: - raise Exception( + raise RuntimeError( "Potentially infinite loop detected. This occurred " "while trying to reduce the amount of shares purchased" " to respect the outlay <= amount rule. This is most " @@ -1612,7 +1603,7 @@ def allocate(self, amount, update=True): ) if self.integer_positions and last_q == q: - raise Exception( + raise RuntimeError( "Newton Method like root search for quantity is stuck!" " q did not change in iterations so it is probably a bug" " but we are not entirely sure it is wrong! Consider " @@ -1621,7 +1612,7 @@ def allocate(self, amount, update=True): last_q = q if np.abs(full_outlay - amount) > np.abs(last_amount_short): - raise Exception( + raise RuntimeError( "The difference between what we have raised with q and" " the amount we are trying to raise has gotten bigger since" " last iteration! full_outlay should always be approaching" @@ -1729,7 +1720,6 @@ def run(self): """ Does nothing - securities have nothing to do on run. """ - pass class Security(SecurityBase): @@ -1742,8 +1732,6 @@ class Security(SecurityBase): all securities. """ - pass - class FixedIncomeSecurity(SecurityBase): """ @@ -1765,7 +1753,7 @@ def update(self, date, data=None, inow=None): else: inow = self._data.index.get_loc(date) - super(FixedIncomeSecurity, self).update(date, data, inow) + super().update(date, data, inow) # For fixed income securities (bonds, swaps), notional value is position size, not value! self._notl_value = self._position @@ -1806,7 +1794,7 @@ class CouponPayingSecurity(FixedIncomeSecurity): @cy.locals(multiplier=cy.double) def __init__(self, name, multiplier=1, fixed_income=True, lazy_add=False): - super(CouponPayingSecurity, self).__init__(name, multiplier) + super().__init__(name, multiplier) self._coupon = 0 self._holding_cost = 0 self._fixed_income = fixed_income @@ -1829,11 +1817,11 @@ def setup(self, universe, **kwargs): the strategy. In particular, often takes the form of a DataFrame of security level information (i.e. signals, risk, etc). """ - super(CouponPayingSecurity, self).setup(universe, **kwargs) + super().setup(universe, **kwargs) # Handle coupons if "coupons" not in kwargs: - raise Exception('"coupons" must be passed to setup for a CouponPayingSecurity') + raise ValueError('"coupons" must be passed to setup for a CouponPayingSecurity') try: self._coupons = kwargs["coupons"][self.name] @@ -1859,7 +1847,7 @@ def setup(self, universe, **kwargs): self._holding_costs = self.data["holding_cost"] def _sync_data(self): - super(CouponPayingSecurity, self)._sync_data() + super()._sync_data() if hasattr(self, "_holding_costs"): self._data["coupon"] = self._coupon_income self._data["holding_cost"] = self._holding_costs @@ -1877,10 +1865,10 @@ def update(self, date, data=None, inow=None): inow = self._data.index.get_loc(date) if self._coupons is None: - raise Exception("coupons have not been set for security %s" % self.name) + raise RuntimeError(f"coupons have not been set for security {self.name}") # Standard update - super(CouponPayingSecurity, self).update(date, data, inow) + super().update(date, data, inow) coupon = self._coupons.iloc[inow] # If we were to call self.parent.adjust, then all the child weights would @@ -1892,7 +1880,7 @@ def update(self, date, data=None, inow=None): if is_zero(self._position): self._coupon = 0.0 else: - raise Exception("Position is open (non-zero) and latest coupon is NaN for security %s on %s. Cannot update node value." % (self.name, date)) + raise ValueError(f"Position is open (non-zero) and latest coupon is NaN for security {self.name} on {date}. Cannot update node value.") else: self._coupon = self._position * coupon @@ -1962,7 +1950,7 @@ def update(self, date, data=None, inow=None): Update security with a given date and optionally, some data. This will update price, value, weight, etc. """ - super(HedgeSecurity, self).update(date, data, inow) + super().update(date, data, inow) self._notl_value = 0.0 self._notl_values.iloc[:] = 0.0 @@ -1983,12 +1971,12 @@ def update(self, date, data=None, inow=None): Update security with a given date and optionally, some data. This will update price, value, weight, etc. """ - super(CouponPayingHedgeSecurity, self).update(date, data, inow) + super().update(date, data, inow) self._notl_value = 0.0 self._notl_values.iloc[:] = 0.0 -class Algo(object): +class Algo: """ Algos are used to modularize strategy logic so that strategy logic becomes modular, composable, more testable and less error prone. Basically, the @@ -2020,7 +2008,7 @@ def name(self): return self._name def __call__(self, target): - raise NotImplementedError("%s not implemented!" % self.name) + raise NotImplementedError(f"{self.name} not implemented!") class AlgoStack(Algo): @@ -2037,7 +2025,7 @@ class AlgoStack(Algo): """ def __init__(self, *algos): - super(AlgoStack, self).__init__() + super().__init__() self.algos = algos self.check_run_always = any(hasattr(x, "run_always") for x in self.algos) @@ -2057,9 +2045,8 @@ def __call__(self, target): for algo in self.algos: if res: res = algo(target) - elif hasattr(algo, "run_always"): - if algo.run_always: - algo(target) + elif hasattr(algo, "run_always") and algo.run_always: + algo(target) return res @@ -2093,7 +2080,7 @@ class Strategy(StrategyBase): """ def __init__(self, name, algos=None, children=None, parent=None): - super(Strategy, self).__init__(name, children=children, parent=parent) + super().__init__(name, children=children, parent=parent) if algos is None: algos = [] self.stack = AlgoStack(*algos) @@ -2133,11 +2120,11 @@ class FixedIncomeStrategy(Strategy): """ def __init__(self, name, algos=None, children=None): - super(FixedIncomeStrategy, self).__init__(name, algos=algos, children=children) + super().__init__(name, algos=algos, children=children) self._fixed_income = True -class CostModel(object): +class CostModel: """ Nonlinear transaction cost model. diff --git a/docs/source/conf.py b/docs/source/conf.py index f9f1cd1d..c518477c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # bt documentation build configuration file, created by # sphinx-quickstart on Fri Jun 27 11:34:24 2014. @@ -11,8 +10,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -21,8 +20,9 @@ sys.path.insert(0, os.path.abspath("../../bt")) sys.path.insert(0, os.path.abspath("_themes/klink")) -import bt # noqa: E402 -import klink # noqa: E402 +import klink + +import bt klink.convert_notebooks() diff --git a/pyproject.toml b/pyproject.toml index 69c1c7e7..a0ca1a00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ dev = [ "pytest", "pytest-cov", "packaging>=24.2", - "ruff>=0.5.0,<0.16", + "ruff", "twine>=6.1", "wheel", ]