Enhance backtesting functionality by adding date range parameters to load_data, improving ATR calculation, and refining trade logic with meta Supertrend signals. Update README with detailed usage instructions and requirements. Add CSV logging for trade results and performance metrics. Include ta library as a dependency in pyproject.toml.
This commit is contained in:
parent
56dca05a3e
commit
21b14d4fe4
82
README.md
82
README.md
@ -1,2 +1,82 @@
|
||||
# lowkey_backtest
|
||||
### lowkey_backtest — Supertrend Backtester
|
||||
|
||||
### Overview
|
||||
Backtest a simple, long-only strategy driven by a meta Supertrend signal on aggregated OHLCV data. The script:
|
||||
- Loads 1-minute BTC/USD data from `../data/btcusd_1-min_data.csv`
|
||||
- Aggregates to multiple timeframes (e.g., `5min`, `15min`, `30min`, `1h`, `4h`, `1d`)
|
||||
- Computes three Supertrend variants and creates a meta signal when all agree
|
||||
- Executes entries/exits at the aggregated bar open price
|
||||
- Applies OKX spot fee assumptions (taker by default)
|
||||
- Evaluates stop-loss using intra-bar 1-minute data
|
||||
- Writes detailed trade logs and a summary CSV
|
||||
|
||||
### Requirements
|
||||
- Python 3.12+
|
||||
- Dependencies: `pandas`, `numpy`, `ta`
|
||||
- Package management: `uv`
|
||||
|
||||
Install dependencies with uv:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
# If a dependency is missing, add it explicitly and sync
|
||||
uv add pandas numpy ta
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Data
|
||||
- Expected CSV location: `../data/btcusd_1-min_data.csv` (relative to the repo root)
|
||||
- Required columns: `Timestamp`, `Open`, `High`, `Low`, `Close`, `Volume`
|
||||
- `Timestamp` should be UNIX seconds; zero-volume rows are ignored
|
||||
|
||||
### Quickstart
|
||||
Run the backtest with defaults:
|
||||
|
||||
```bash
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
Outputs:
|
||||
- Per-run trade logs in `backtest_logs/` named like `trade_log_<TIMEFRAME>_sl<STOPLOSS>.csv`
|
||||
- Run-level summary in `backtest_summary.csv`
|
||||
|
||||
### Configuring a Run
|
||||
Adjust parameters directly in `main.py`:
|
||||
- Date range (in `load_data`): `load_data('2021-11-01', '2024-10-16')`
|
||||
- Timeframes to test (any subset of `"5min", "15min", "30min", "1h", "4h", "1d"`):
|
||||
- `timeframes = ["5min", "15min", "30min", "1h", "4h", "1d"]`
|
||||
- Stop-loss percentages:
|
||||
- `stoplosses = [0.03, 0.05, 0.1]`
|
||||
- Supertrend settings (in `add_supertrend_indicators`): `(period, multiplier)` pairs `(12, 3.0)`, `(10, 1.0)`, `(11, 2.0)`
|
||||
- Fee model (in `calculate_okx_taker_maker_fee`): taker `0.0010`, maker `0.0008`
|
||||
|
||||
### What the Backtester Does
|
||||
- Aggregation: Resamples 1-minute data to your selected timeframe using OHLCV rules
|
||||
- Supertrend signals: Computes three Supertrends and derives a meta trend of `+1` (bullish) or `-1` (bearish) when all agree; otherwise `0`
|
||||
- Trade logic (long-only):
|
||||
- Entry when the meta trend changes to bullish; uses aggregated bar open price
|
||||
- Exit when the meta trend changes to bearish; uses aggregated bar open price
|
||||
- Stop-loss: For each aggregated bar, scans corresponding 1-minute closes to detect stop-loss and exits using a realistic fill (threshold or next 1-minute open if gapped)
|
||||
- Performance metrics: total return, max drawdown, Sharpe (daily, factor 252), win rate, number of trades, final/initial equity, and total fees
|
||||
|
||||
### Important: Lookahead Bias Note
|
||||
The current implementation uses the meta Supertrend signal of the same bar for entries/exits, which introduces lookahead bias. To avoid this, lag the signal by one bar inside `backtest()` in `main.py`:
|
||||
|
||||
```python
|
||||
# Replace the current line
|
||||
meta_trend_signal = meta_trend
|
||||
|
||||
# With a one-bar lag to remove lookahead
|
||||
# meta_trend_signal = np.roll(meta_trend, 1)
|
||||
# meta_trend_signal[0] = 0
|
||||
```
|
||||
|
||||
### Outputs
|
||||
- `backtest_logs/trade_log_<TIMEFRAME>_sl<STOPLOSS>.csv`: trade-by-trade records including type (`buy`, `sell`, `stop_loss`, `forced_close`), timestamps, prices, balances, PnL, and fees
|
||||
- `backtest_summary.csv`: one row per (timeframe, stop-loss) combination with `timeframe`, `stop_loss`, `total_return`, `max_drawdown`, `sharpe_ratio`, `win_rate`, `num_trades`, `final_equity`, `initial_equity`, `num_stop_losses`, `total_fees`
|
||||
|
||||
### Troubleshooting
|
||||
- CSV not found: Ensure the dataset is located at `../data/btcusd_1-min_data.csv`
|
||||
- Missing packages: Run `uv add pandas numpy ta` then `uv sync`
|
||||
- Memory/performance: Large date ranges on 1-minute data can be heavy; narrow the date span or test fewer timeframes
|
||||
|
||||
|
||||
368
main.py
368
main.py
@ -1,12 +1,16 @@
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from ta.volatility import AverageTrueRange
|
||||
import time
|
||||
import csv
|
||||
import math
|
||||
import os
|
||||
|
||||
|
||||
def load_data(since):
|
||||
def load_data(since, until):
|
||||
df = pd.read_csv('../data/btcusd_1-min_data.csv')
|
||||
df['Timestamp'] = pd.to_datetime(df['Timestamp'], unit='s')
|
||||
df = df[df['Timestamp'] >= pd.Timestamp(since)]
|
||||
df = df[(df['Timestamp'] >= pd.Timestamp(since)) & (df['Timestamp'] <= pd.Timestamp(until))]
|
||||
return df
|
||||
|
||||
def aggregate_data(df, timeframe):
|
||||
@ -38,10 +42,32 @@ def calculate_supertrend(df, period, multiplier):
|
||||
Returns:
|
||||
pd.Series: Supertrend values.
|
||||
"""
|
||||
# Ensure we have enough data for ATR calculation
|
||||
if len(df) < period + 1:
|
||||
print(f"Warning: Not enough data for ATR period {period}. Need at least {period + 1} rows, got {len(df)}")
|
||||
return pd.Series([np.nan] * len(df), index=df.index)
|
||||
|
||||
high = df['High'].values
|
||||
low = df['Low'].values
|
||||
close = df['Close'].values
|
||||
atr = AverageTrueRange(df['High'], df['Low'], df['Close'], window=period).average_true_range().values
|
||||
|
||||
# Calculate True Range first
|
||||
tr = np.zeros_like(close)
|
||||
for i in range(1, len(close)):
|
||||
tr[i] = max(
|
||||
high[i] - low[i], # Current high - current low
|
||||
abs(high[i] - close[i-1]), # Current high - previous close
|
||||
abs(low[i] - close[i-1]) # Current low - previous close
|
||||
)
|
||||
|
||||
# Calculate ATR using simple moving average
|
||||
atr = np.zeros_like(close)
|
||||
atr[period] = np.mean(tr[1:period+1]) # First ATR value
|
||||
for i in range(period+1, len(close)):
|
||||
atr[i] = (atr[i-1] * (period-1) + tr[i]) / period # Exponential-like smoothing
|
||||
|
||||
# Fill initial values with the first valid ATR
|
||||
atr[:period] = atr[period] if atr[period] > 0 else 0.001
|
||||
|
||||
hl2 = (high + low) / 2
|
||||
upperband = hl2 + (multiplier * atr)
|
||||
@ -105,23 +131,38 @@ def precompute_1min_slice_indices(df_aggregated, df_1min):
|
||||
indices.append((start_idx, end_idx))
|
||||
return indices, sorted_1min
|
||||
|
||||
def backtest(df_aggregated, df_1min, stop_loss_pct, progress_step=1000):
|
||||
def backtest(timeframe, df_aggregated, df_1min, stop_loss_pct, progress_step=1000):
|
||||
"""
|
||||
Backtest trading strategy based on Supertrend indicators with trailing stop loss.
|
||||
Buys when all three Supertrend columns are positive (>0),
|
||||
sells when any is negative (<0), or when trailing stop loss is hit.
|
||||
|
||||
Args:
|
||||
df_aggregated (pd.DataFrame): Aggregated OHLCV data with Supertrend columns.
|
||||
df_1min (pd.DataFrame): 1-minute OHLCV data.
|
||||
stop_loss_pct (float): Trailing stop loss percentage (e.g., 0.02 for 2%).
|
||||
progress_step (int): Step interval for progress display.
|
||||
Backtest trading strategy based on meta supertrend logic (all three supertrends agree).
|
||||
Uses signal transitions and open prices for entry/exit to match original implementation.
|
||||
"""
|
||||
start_time = time.time()
|
||||
required_st_cols = ["supertrend_12_3.0", "supertrend_10_1.0", "supertrend_11_2.0"]
|
||||
for col in required_st_cols:
|
||||
if col not in df_aggregated.columns:
|
||||
raise ValueError(f"Missing required Supertrend column: {col}")
|
||||
|
||||
# Calculate trend directions for each supertrend (-1, 0, 1)
|
||||
trends = []
|
||||
for col in required_st_cols:
|
||||
# Convert supertrend values to trend direction based on close price position
|
||||
trend = np.where(df_aggregated['Close'] > df_aggregated[col], 1, -1)
|
||||
trends.append(trend)
|
||||
|
||||
# Stack trends and calculate meta trend (all must agree)
|
||||
trends_arr = np.stack(trends, axis=1)
|
||||
meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
|
||||
trends_arr[:,0], 0)
|
||||
|
||||
meta_trend_signal = meta_trend #incorrect: should be lagging as it introduces lookahead bias.
|
||||
# Next step: modify OHLCV predictor to not use supertrend as a feature or anyother feature
|
||||
# that introduces lookahead bias and predict the next close price.
|
||||
#
|
||||
# Old code, not that efficient:
|
||||
# Add signal lagging to avoid lookahead bias
|
||||
# meta_trend_signal = np.roll(meta_trend, 1)
|
||||
# meta_trend_signal[0] = 0 # No signal for first bar
|
||||
|
||||
# Precompute 1-min slice indices for each aggregated bar
|
||||
slice_indices, sorted_1min = precompute_1min_slice_indices(df_aggregated, df_1min)
|
||||
df_1min_sorted = df_1min.iloc[sorted_1min].reset_index(drop=True)
|
||||
@ -130,74 +171,275 @@ def backtest(df_aggregated, df_1min, stop_loss_pct, progress_step=1000):
|
||||
init_usd = 1000
|
||||
usd = init_usd
|
||||
coin = 0
|
||||
highest_price = None
|
||||
nb_stop_loss = 0
|
||||
trade_log = []
|
||||
equity_curve = []
|
||||
trade_results = []
|
||||
entry_price = None
|
||||
entry_time = None
|
||||
|
||||
total_steps = len(df_aggregated) - 1
|
||||
for i in range(1, len(df_aggregated)):
|
||||
st_vals = [df_aggregated[col][i] for col in required_st_cols]
|
||||
all_positive = all(val > 0 for val in st_vals)
|
||||
any_negative = any(val < 0 for val in st_vals)
|
||||
open_price = df_aggregated['Open'][i] # Use open price for entry/exit
|
||||
close_price = df_aggregated['Close'][i]
|
||||
timestamp = df_aggregated['Timestamp'][i]
|
||||
|
||||
# Get previous and current meta trend signals
|
||||
prev_mt = meta_trend_signal[i-1] if i > 0 else 0
|
||||
curr_mt = meta_trend_signal[i]
|
||||
|
||||
# Buy condition: all Supertrend values positive
|
||||
if not in_position and all_positive:
|
||||
in_position = True
|
||||
coin = usd / close_price
|
||||
usd = 0
|
||||
highest_price = close_price
|
||||
# If in position, update highest price and check stop loss on 1-min data
|
||||
elif in_position:
|
||||
# Update highest price if new high on aggregated bar
|
||||
if close_price > highest_price:
|
||||
highest_price = close_price
|
||||
# Track equity at each bar
|
||||
equity = usd + coin * close_price
|
||||
equity_curve.append((timestamp, equity))
|
||||
|
||||
# Use precomputed indices for this bar
|
||||
# Check stop loss if in position
|
||||
if in_position:
|
||||
start_idx, end_idx = slice_indices[i-1]
|
||||
df_1min_slice = df_1min_sorted.iloc[start_idx:end_idx]
|
||||
|
||||
stop_triggered = False
|
||||
for _, row in df_1min_slice.iterrows():
|
||||
# Update highest price if new high in 1-min bar
|
||||
if row['Close'] > highest_price:
|
||||
highest_price = row['Close']
|
||||
# Trailing stop loss condition on 1-min close
|
||||
if row['Close'] < highest_price * (1 - stop_loss_pct):
|
||||
in_position = False
|
||||
usd = coin * row['Close']
|
||||
coin = 0
|
||||
# print(f"Stop loss triggered at {row['Close']:.2f} on {row['Timestamp']}")
|
||||
nb_stop_loss += 1
|
||||
highest_price = None
|
||||
|
||||
if not df_1min_slice.empty:
|
||||
stop_loss_threshold = entry_price * (1 - stop_loss_pct)
|
||||
below_stop = df_1min_slice['Low'] < stop_loss_threshold
|
||||
|
||||
if below_stop.any():
|
||||
first_idx = below_stop.idxmax()
|
||||
stop_row = df_1min_slice.loc[first_idx]
|
||||
stop_triggered = True
|
||||
break
|
||||
|
||||
# If stop loss was triggered, skip further checks for this bar
|
||||
in_position = False
|
||||
|
||||
# More realistic stop loss fill logic
|
||||
if stop_row['Open'] < stop_loss_threshold:
|
||||
exit_price = stop_row['Open']
|
||||
else:
|
||||
exit_price = stop_loss_threshold
|
||||
|
||||
exit_time = stop_row['Timestamp']
|
||||
gross_usd = coin * exit_price
|
||||
fee = calculate_okx_taker_maker_fee(gross_usd, is_maker=False)
|
||||
usd = gross_usd - fee
|
||||
trade_pnl = (exit_price - entry_price) / entry_price if entry_price else 0
|
||||
trade_results.append(trade_pnl)
|
||||
trade_log.append({
|
||||
'type': 'stop_loss',
|
||||
'time': exit_time,
|
||||
'price': exit_price,
|
||||
'usd': usd,
|
||||
'coin': 0,
|
||||
'pnl': trade_pnl,
|
||||
'fee': fee
|
||||
})
|
||||
coin = 0
|
||||
nb_stop_loss += 1
|
||||
entry_price = None
|
||||
entry_time = None
|
||||
|
||||
if stop_triggered:
|
||||
continue
|
||||
|
||||
# Sell condition: any Supertrend value negative (on aggregated bar close)
|
||||
if any_negative:
|
||||
in_position = False
|
||||
usd = coin * close_price
|
||||
coin = 0
|
||||
highest_price = None
|
||||
# Entry condition: signal changes TO bullish (prev != 1 and curr == 1)
|
||||
if not in_position and prev_mt != 1 and curr_mt == 1:
|
||||
in_position = True
|
||||
fee = calculate_okx_taker_maker_fee(usd, is_maker=False)
|
||||
usd_after_fee = usd - fee
|
||||
coin = usd_after_fee / open_price # Use open price
|
||||
entry_price = open_price
|
||||
entry_time = timestamp
|
||||
usd = 0
|
||||
trade_log.append({
|
||||
'type': 'buy',
|
||||
'time': timestamp,
|
||||
'price': open_price,
|
||||
'usd': usd,
|
||||
'coin': coin,
|
||||
'fee': fee
|
||||
})
|
||||
|
||||
# Exit condition: signal changes TO bearish (prev == 1 and curr == -1)
|
||||
elif in_position and prev_mt == 1 and curr_mt == -1:
|
||||
in_position = False
|
||||
exit_price = open_price # Use open price
|
||||
exit_time = timestamp
|
||||
gross_usd = coin * open_price
|
||||
fee = calculate_okx_taker_maker_fee(gross_usd, is_maker=False)
|
||||
usd = gross_usd - fee
|
||||
trade_pnl = (exit_price - entry_price) / entry_price if entry_price else 0
|
||||
trade_results.append(trade_pnl)
|
||||
trade_log.append({
|
||||
'type': 'sell',
|
||||
'time': exit_time,
|
||||
'price': exit_price,
|
||||
'usd': usd,
|
||||
'coin': 0,
|
||||
'pnl': trade_pnl,
|
||||
'fee': fee
|
||||
})
|
||||
coin = 0
|
||||
entry_price = None
|
||||
entry_time = None
|
||||
|
||||
if i % progress_step == 0 or i == total_steps:
|
||||
percent = (i / total_steps) * 100
|
||||
print(f"Progress: {percent:.1f}% ({i}/{total_steps})")
|
||||
print(f"\rTimeframe: {timeframe},\tProgress: {percent:.1f}%\tCurrent equity: {equity:.2f}\033[K", end='', flush=True)
|
||||
|
||||
print(f"Total profit: {usd - init_usd}")
|
||||
print(f"Number of stop losses: {nb_stop_loss}")
|
||||
# Force close any open position at the end
|
||||
if in_position:
|
||||
final_open_price = df_aggregated['Open'].iloc[-1] # Use open price for consistency
|
||||
final_timestamp = df_aggregated['Timestamp'].iloc[-1]
|
||||
gross_usd = coin * final_open_price
|
||||
fee = calculate_okx_taker_maker_fee(gross_usd, is_maker=False)
|
||||
usd = gross_usd - fee
|
||||
trade_pnl = (final_open_price - entry_price) / entry_price if entry_price else 0
|
||||
trade_results.append(trade_pnl)
|
||||
trade_log.append({
|
||||
'type': 'forced_close',
|
||||
'time': final_timestamp,
|
||||
'price': final_open_price,
|
||||
'usd': usd,
|
||||
'coin': 0,
|
||||
'pnl': trade_pnl,
|
||||
'fee': fee
|
||||
})
|
||||
coin = 0
|
||||
in_position = False
|
||||
entry_price = None
|
||||
|
||||
print()
|
||||
print(f"Timeframe: {timeframe},\tTotal profit: {usd - init_usd},\tNumber of stop losses: {nb_stop_loss}")
|
||||
|
||||
# --- Performance Metrics ---
|
||||
equity_arr = np.array([e[1] for e in equity_curve])
|
||||
# Handle edge cases for empty or invalid equity data
|
||||
if len(equity_arr) == 0:
|
||||
print("Warning: No equity data available")
|
||||
return None
|
||||
returns = np.diff(equity_arr) / equity_arr[:-1]
|
||||
# Filter out infinite and NaN returns
|
||||
returns = returns[np.isfinite(returns)]
|
||||
total_return = (equity_arr[-1] - equity_arr[0]) / equity_arr[0] if equity_arr[0] != 0 else 0
|
||||
running_max = np.maximum.accumulate(equity_arr)
|
||||
if equity_arr[-1] <= 0.01:
|
||||
max_drawdown = -1.0
|
||||
else:
|
||||
drawdowns = (equity_arr - running_max) / running_max
|
||||
max_drawdown = drawdowns.min() if len(drawdowns) > 0 and np.isfinite(drawdowns).any() else 0
|
||||
if len(returns) > 1 and np.std(returns) > 1e-9:
|
||||
sharpe = np.mean(returns) / np.std(returns) * math.sqrt(252)
|
||||
else:
|
||||
sharpe = 0
|
||||
wins = [1 for r in trade_results if r > 0]
|
||||
win_rate = len(wins) / len(trade_results) if trade_results else 0
|
||||
num_trades = len(trade_results)
|
||||
|
||||
print(f"Performance Metrics:")
|
||||
print(f" Total Return: {total_return*100:.2f}%")
|
||||
print(f" Max Drawdown: {max_drawdown*100:.2f}%")
|
||||
print(f" Sharpe Ratio: {sharpe:.2f}")
|
||||
print(f" Win Rate: {win_rate*100:.2f}%")
|
||||
print(f" Number of Trades: {num_trades}")
|
||||
print(f" Final Equity: ${equity_arr[-1]:.2f}")
|
||||
print(f" Initial Equity: ${equity_arr[0]:.2f}")
|
||||
|
||||
# --- Save Trade Log ---
|
||||
log_dir = "backtest_logs"
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
# Format stop_loss_pct for filename (e.g., 0.05 -> 0p05)
|
||||
stop_loss_str = f"{stop_loss_pct:.2f}".replace('.', 'p')
|
||||
log_path = os.path.join(log_dir, f"trade_log_{timeframe}_sl{stop_loss_str}.csv")
|
||||
if trade_log:
|
||||
all_keys = set()
|
||||
for entry in trade_log:
|
||||
all_keys.update(entry.keys())
|
||||
all_keys = list(all_keys)
|
||||
|
||||
trade_log_filled = []
|
||||
for entry in trade_log:
|
||||
filled_entry = {k: entry.get(k, None) for k in all_keys}
|
||||
trade_log_filled.append(filled_entry)
|
||||
|
||||
# Calculate total fees for this backtest
|
||||
total_fees = sum(entry.get('fee', 0) for entry in trade_log)
|
||||
|
||||
# Write summary header row, then trade log header and rows
|
||||
with open(log_path, 'w', newline='') as f:
|
||||
writer = csv.writer(f)
|
||||
summary_header = [
|
||||
'elapsed_time_sec', 'total_return', 'max_drawdown', 'sharpe_ratio',
|
||||
'win_rate', 'num_trades', 'final_equity', 'initial_equity', 'num_stop_losses', 'total_fees'
|
||||
]
|
||||
summary_values = [
|
||||
f"{time.time() - start_time:.2f}",
|
||||
f"{total_return*100:.2f}%",
|
||||
f"{max_drawdown*100:.2f}%",
|
||||
f"{sharpe:.2f}",
|
||||
f"{win_rate*100:.2f}%",
|
||||
str(num_trades),
|
||||
f"${equity_arr[-1]:.2f}",
|
||||
f"${equity_arr[0]:.2f}",
|
||||
str(nb_stop_loss),
|
||||
f"${total_fees:.4f}"
|
||||
]
|
||||
writer.writerow(summary_header)
|
||||
writer.writerow(summary_values)
|
||||
writer.writerow([]) # Blank row for separation
|
||||
dict_writer = csv.DictWriter(f, fieldnames=all_keys)
|
||||
dict_writer.writeheader()
|
||||
dict_writer.writerows(trade_log_filled)
|
||||
|
||||
print(f"Trade log saved to {log_path}")
|
||||
else:
|
||||
print("No trades to log.")
|
||||
|
||||
# Return summary metrics (excluding elapsed time)
|
||||
return {
|
||||
'timeframe': timeframe,
|
||||
'stop_loss': stop_loss_pct,
|
||||
'total_return': total_return,
|
||||
'max_drawdown': max_drawdown,
|
||||
'sharpe_ratio': sharpe,
|
||||
'win_rate': win_rate,
|
||||
'num_trades': num_trades,
|
||||
'final_equity': equity_arr[-1],
|
||||
'initial_equity': equity_arr[0],
|
||||
'num_stop_losses': nb_stop_loss,
|
||||
'total_fees': total_fees if trade_log else 0
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
df_1min = load_data('2020-01-01')
|
||||
df_aggregated = aggregate_data(df_1min, '5min')
|
||||
timeframes = ["5min", "15min", "30min", "1h", "4h", "1d"]
|
||||
# timeframes = ["5min", "15min", "1h", "4h", "1d"]
|
||||
# timeframes = ["30min"]
|
||||
stoplosses = [0.03, 0.05, 0.1]
|
||||
|
||||
df_1min = load_data('2021-11-01', '2024-10-16')
|
||||
|
||||
# Add Supertrend indicators
|
||||
df_aggregated = add_supertrend_indicators(df_aggregated)
|
||||
|
||||
df_aggregated['log_return'] = np.log(df_aggregated['Close'] / df_aggregated['Close'].shift(1))
|
||||
|
||||
# Example: 2% trailing stop loss
|
||||
backtest(df_aggregated, df_1min, stop_loss_pct=0.02)
|
||||
# Prepare summary CSV
|
||||
summary_csv_path = "backtest_summary.csv"
|
||||
summary_header = [
|
||||
'timeframe', 'stop_loss', 'total_return', 'max_drawdown', 'sharpe_ratio',
|
||||
'win_rate', 'num_trades', 'final_equity', 'initial_equity', 'num_stop_losses', 'total_fees'
|
||||
]
|
||||
with open(summary_csv_path, 'w', newline='') as summary_file:
|
||||
writer = csv.DictWriter(summary_file, fieldnames=summary_header)
|
||||
writer.writeheader()
|
||||
for timeframe in timeframes:
|
||||
df_aggregated = aggregate_data(df_1min, timeframe)
|
||||
df_aggregated = add_supertrend_indicators(df_aggregated)
|
||||
for stop_loss_pct in stoplosses:
|
||||
summary = backtest(timeframe, df_aggregated, df_1min, stop_loss_pct=stop_loss_pct)
|
||||
if summary is not None:
|
||||
# Format values for CSV (e.g., floats as rounded strings)
|
||||
summary_row = {
|
||||
'timeframe': summary['timeframe'],
|
||||
'stop_loss': summary['stop_loss'],
|
||||
'total_return': f"{summary['total_return']*100:.2f}%",
|
||||
'max_drawdown': f"{summary['max_drawdown']*100:.2f}%",
|
||||
'sharpe_ratio': f"{summary['sharpe_ratio']:.2f}",
|
||||
'win_rate': f"{summary['win_rate']*100:.2f}%",
|
||||
'num_trades': summary['num_trades'],
|
||||
'final_equity': f"${summary['final_equity']:.2f}",
|
||||
'initial_equity': f"${summary['initial_equity']:.2f}",
|
||||
'num_stop_losses': summary['num_stop_losses'],
|
||||
'total_fees': f"${summary['total_fees']:.4f}"
|
||||
}
|
||||
writer.writerow(summary_row)
|
||||
|
||||
@ -4,4 +4,6 @@ version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
"ta>=0.11.0",
|
||||
]
|
||||
|
||||
160
uv.lock
generated
Normal file
160
uv.lock
generated
Normal file
@ -0,0 +1,160 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "lowkey-backtest"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "ta" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "ta", specifier = ">=0.11.0" }]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "2.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ta"
|
||||
version = "0.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pandas" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/9a/37d92a6b470dc9088612c2399a68f1a9ac22872d4e1eff416818e22ab11b/ta-0.11.0.tar.gz", hash = "sha256:de86af43418420bd6b088a2ea9b95483071bf453c522a8441bc2f12bcf8493fd", size = 25308, upload-time = "2023-11-02T13:53:35.434Z" }
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
Loading…
x
Reference in New Issue
Block a user