Compare commits
4 Commits
10cc047975
...
14905017c8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14905017c8 | ||
|
|
ec1a86e098 | ||
|
|
0a919f825e | ||
|
|
c2886a2aab |
@ -114,6 +114,10 @@ def calculate_supertrend_external(data, period, multiplier):
|
|||||||
# Call the cached function
|
# Call the cached function
|
||||||
return cached_supertrend_calculation(period, multiplier, (high_tuple, low_tuple, close_tuple))
|
return cached_supertrend_calculation(period, multiplier, (high_tuple, low_tuple, close_tuple))
|
||||||
|
|
||||||
|
def calculate_okx_fee(amount, is_maker=True):
|
||||||
|
fee_rate = 0.0008 if is_maker else 0.0010
|
||||||
|
return amount * fee_rate
|
||||||
|
|
||||||
class TrendDetectorSimple:
|
class TrendDetectorSimple:
|
||||||
def __init__(self, data, verbose=False, display=False):
|
def __init__(self, data, verbose=False, display=False):
|
||||||
"""
|
"""
|
||||||
@ -638,7 +642,7 @@ class TrendDetectorSimple:
|
|||||||
ax.plot([], [], color_down, linewidth=self.line_width,
|
ax.plot([], [], color_down, linewidth=self.line_width,
|
||||||
label=f'ST (P:{period}, M:{multiplier}) Down')
|
label=f'ST (P:{period}, M:{multiplier}) Down')
|
||||||
|
|
||||||
def backtest_meta_supertrend(self, min1_df, initial_usd=10000, stop_loss_pct=0.05, transaction_cost=0.001, debug=False):
|
def backtest_meta_supertrend(self, min1_df, initial_usd=10000, stop_loss_pct=0.05, debug=False):
|
||||||
"""
|
"""
|
||||||
Backtest a simple strategy using the meta supertrend (all three supertrends agree).
|
Backtest a simple strategy using the meta supertrend (all three supertrends agree).
|
||||||
Buys when meta supertrend is positive, sells when negative, applies a percentage stop loss.
|
Buys when meta supertrend is positive, sells when negative, applies a percentage stop loss.
|
||||||
@ -647,7 +651,6 @@ class TrendDetectorSimple:
|
|||||||
- min1_df: pandas DataFrame, 1-minute timeframe data for more accurate stop loss checking (optional)
|
- min1_df: pandas DataFrame, 1-minute timeframe data for more accurate stop loss checking (optional)
|
||||||
- initial_usd: float, starting USD amount
|
- initial_usd: float, starting USD amount
|
||||||
- stop_loss_pct: float, stop loss as a fraction (e.g. 0.05 for 5%)
|
- stop_loss_pct: float, stop loss as a fraction (e.g. 0.05 for 5%)
|
||||||
- transaction_cost: float, transaction cost as a fraction (e.g. 0.001 for 0.1%)
|
|
||||||
- debug: bool, whether to print debug info
|
- debug: bool, whether to print debug info
|
||||||
"""
|
"""
|
||||||
df = self.data.copy().reset_index(drop=True)
|
df = self.data.copy().reset_index(drop=True)
|
||||||
@ -709,16 +712,16 @@ class TrendDetectorSimple:
|
|||||||
if debug:
|
if debug:
|
||||||
print(f"STOP LOSS triggered: entry={entry_price}, stop={stop_price}, sell_price={sell_price}, entry_time={entry_time}, stop_time={stop_candle.name}")
|
print(f"STOP LOSS triggered: entry={entry_price}, stop={stop_price}, sell_price={sell_price}, entry_time={entry_time}, stop_time={stop_candle.name}")
|
||||||
btc_to_sell = coin
|
btc_to_sell = coin
|
||||||
fee_btc = btc_to_sell * transaction_cost
|
usd_gross = btc_to_sell * sell_price
|
||||||
btc_after_fee = btc_to_sell - fee_btc
|
exit_fee = calculate_okx_fee(usd_gross, is_maker=False) # taker fee
|
||||||
usd = btc_after_fee * sell_price
|
usd = usd_gross - exit_fee
|
||||||
trade_log.append({
|
trade_log.append({
|
||||||
'type': 'STOP',
|
'type': 'STOP',
|
||||||
'entry': entry_price,
|
'entry': entry_price,
|
||||||
'exit': sell_price,
|
'exit': sell_price,
|
||||||
'entry_time': entry_time,
|
'entry_time': entry_time,
|
||||||
'exit_time': stop_candle.name, # Use index name instead of timestamp column
|
'exit_time': stop_candle.name,
|
||||||
'fee_btc': fee_btc
|
'fee_usd': exit_fee
|
||||||
})
|
})
|
||||||
coin = 0
|
coin = 0
|
||||||
position = 0
|
position = 0
|
||||||
@ -731,10 +734,10 @@ class TrendDetectorSimple:
|
|||||||
|
|
||||||
# Entry: only if not in position and signal changes to 1
|
# Entry: only if not in position and signal changes to 1
|
||||||
if position == 0 and prev_mt != 1 and curr_mt == 1:
|
if position == 0 and prev_mt != 1 and curr_mt == 1:
|
||||||
# Buy at open, fee is charged in BTC (base currency)
|
# Buy at open, fee is charged in USD
|
||||||
gross_btc = usd / price_open
|
entry_fee = calculate_okx_fee(usd, is_maker=False)
|
||||||
fee_btc = gross_btc * transaction_cost
|
usd_after_fee = usd - entry_fee
|
||||||
coin = gross_btc - fee_btc
|
coin = usd_after_fee / price_open
|
||||||
entry_price = price_open
|
entry_price = price_open
|
||||||
entry_time = date
|
entry_time = date
|
||||||
usd = 0
|
usd = 0
|
||||||
@ -746,23 +749,23 @@ class TrendDetectorSimple:
|
|||||||
'exit': None,
|
'exit': None,
|
||||||
'entry_time': entry_time,
|
'entry_time': entry_time,
|
||||||
'exit_time': None,
|
'exit_time': None,
|
||||||
'fee_btc': fee_btc
|
'fee_usd': entry_fee
|
||||||
})
|
})
|
||||||
|
|
||||||
# Exit: only if in position and signal changes from 1 to -1
|
# Exit: only if in position and signal changes from 1 to -1
|
||||||
elif position == 1 and prev_mt == 1 and curr_mt == -1:
|
elif position == 1 and prev_mt == 1 and curr_mt == -1:
|
||||||
# Sell at open, fee is charged in BTC (base currency)
|
# Sell at open, fee is charged in USD
|
||||||
btc_to_sell = coin
|
btc_to_sell = coin
|
||||||
fee_btc = btc_to_sell * transaction_cost
|
usd_gross = btc_to_sell * price_open
|
||||||
btc_after_fee = btc_to_sell - fee_btc
|
exit_fee = calculate_okx_fee(usd_gross, is_maker=False)
|
||||||
usd = btc_after_fee * price_open
|
usd = usd_gross - exit_fee
|
||||||
trade_log.append({
|
trade_log.append({
|
||||||
'type': 'SELL',
|
'type': 'SELL',
|
||||||
'entry': entry_price,
|
'entry': entry_price,
|
||||||
'exit': price_open,
|
'exit': price_open,
|
||||||
'entry_time': entry_time,
|
'entry_time': entry_time,
|
||||||
'exit_time': date,
|
'exit_time': date,
|
||||||
'fee_btc': fee_btc
|
'fee_usd': exit_fee
|
||||||
})
|
})
|
||||||
coin = 0
|
coin = 0
|
||||||
position = 0
|
position = 0
|
||||||
@ -779,16 +782,16 @@ class TrendDetectorSimple:
|
|||||||
# If still in position at end, sell at last close
|
# If still in position at end, sell at last close
|
||||||
if position == 1:
|
if position == 1:
|
||||||
btc_to_sell = coin
|
btc_to_sell = coin
|
||||||
fee_btc = btc_to_sell * transaction_cost
|
usd_gross = btc_to_sell * df['close'].iloc[-1]
|
||||||
btc_after_fee = btc_to_sell - fee_btc
|
exit_fee = calculate_okx_fee(usd_gross, is_maker=False)
|
||||||
usd = btc_after_fee * df['close'].iloc[-1]
|
usd = usd_gross - exit_fee
|
||||||
trade_log.append({
|
trade_log.append({
|
||||||
'type': 'EOD',
|
'type': 'EOD',
|
||||||
'entry': entry_price,
|
'entry': entry_price,
|
||||||
'exit': df['close'].iloc[-1],
|
'exit': df['close'].iloc[-1],
|
||||||
'entry_time': entry_time,
|
'entry_time': entry_time,
|
||||||
'exit_time': df['timestamp'].iloc[-1],
|
'exit_time': df['timestamp'].iloc[-1],
|
||||||
'fee_btc': fee_btc
|
'fee_usd': exit_fee
|
||||||
})
|
})
|
||||||
coin = 0
|
coin = 0
|
||||||
position = 0
|
position = 0
|
||||||
@ -803,7 +806,6 @@ class TrendDetectorSimple:
|
|||||||
avg_trade = np.mean([t['exit']/t['entry']-1 for t in trade_log if t['exit'] is not None]) if trade_log else 0
|
avg_trade = np.mean([t['exit']/t['entry']-1 for t in trade_log if t['exit'] is not None]) if trade_log else 0
|
||||||
|
|
||||||
trades = []
|
trades = []
|
||||||
total_fees_btc = 0.0
|
|
||||||
total_fees_usd = 0.0
|
total_fees_usd = 0.0
|
||||||
for trade in trade_log:
|
for trade in trade_log:
|
||||||
if trade['exit'] is not None:
|
if trade['exit'] is not None:
|
||||||
@ -816,13 +818,11 @@ class TrendDetectorSimple:
|
|||||||
'entry': trade['entry'],
|
'entry': trade['entry'],
|
||||||
'exit': trade['exit'],
|
'exit': trade['exit'],
|
||||||
'profit_pct': profit_pct,
|
'profit_pct': profit_pct,
|
||||||
'type': trade.get('type', 'SELL')
|
'type': trade.get('type', 'SELL'),
|
||||||
|
'fee_usd': trade.get('fee_usd')
|
||||||
})
|
})
|
||||||
# Sum up BTC fees and their USD equivalent (use exit price if available)
|
fee_usd = trade.get('fee_usd')
|
||||||
fee_btc = trade.get('fee_btc', 0.0)
|
total_fees_usd += fee_usd
|
||||||
total_fees_btc += fee_btc
|
|
||||||
if fee_btc and trade.get('exit') is not None:
|
|
||||||
total_fees_usd += fee_btc * trade['exit']
|
|
||||||
|
|
||||||
results = {
|
results = {
|
||||||
"initial_usd": initial_usd,
|
"initial_usd": initial_usd,
|
||||||
@ -833,7 +833,6 @@ class TrendDetectorSimple:
|
|||||||
"avg_trade": avg_trade,
|
"avg_trade": avg_trade,
|
||||||
"trade_log": trade_log,
|
"trade_log": trade_log,
|
||||||
"trades": trades,
|
"trades": trades,
|
||||||
"total_fees_btc": total_fees_btc,
|
|
||||||
"total_fees_usd": total_fees_usd,
|
"total_fees_usd": total_fees_usd,
|
||||||
}
|
}
|
||||||
if n_trades > 0:
|
if n_trades > 0:
|
||||||
|
|||||||
@ -144,6 +144,7 @@ class Storage:
|
|||||||
"avg_trade": f"{row['avg_trade']*100:.2f}%",
|
"avg_trade": f"{row['avg_trade']*100:.2f}%",
|
||||||
"profit_ratio": f"{row['profit_ratio']*100:.2f}%",
|
"profit_ratio": f"{row['profit_ratio']*100:.2f}%",
|
||||||
"final_usd": f"{row['final_usd']:.2f}",
|
"final_usd": f"{row['final_usd']:.2f}",
|
||||||
|
"total_fees_usd": f"{row['total_fees_usd']:.2f}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None):
|
def write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None):
|
||||||
|
|||||||
56
main.py
56
main.py
@ -61,6 +61,7 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
|||||||
final_usd = initial_usd
|
final_usd = initial_usd
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
final_usd *= (1 + trade['profit_pct'])
|
final_usd *= (1 + trade['profit_pct'])
|
||||||
|
total_fees_usd = sum(trade.get('fee_usd', 0.0) for trade in trades)
|
||||||
row = {
|
row = {
|
||||||
"timeframe": rule_name,
|
"timeframe": rule_name,
|
||||||
"stop_loss_pct": stop_loss_pct,
|
"stop_loss_pct": stop_loss_pct,
|
||||||
@ -74,6 +75,7 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
|||||||
"profit_ratio": profit_ratio,
|
"profit_ratio": profit_ratio,
|
||||||
"initial_usd": initial_usd,
|
"initial_usd": initial_usd,
|
||||||
"final_usd": final_usd,
|
"final_usd": final_usd,
|
||||||
|
"total_fees_usd": total_fees_usd,
|
||||||
}
|
}
|
||||||
results_rows.append(row)
|
results_rows.append(row)
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
@ -85,7 +87,8 @@ def process_timeframe_data(min1_df, df, stop_loss_pcts, rule_name, initial_usd,
|
|||||||
"entry_price": trade.get("entry"),
|
"entry_price": trade.get("entry"),
|
||||||
"exit_price": trade.get("exit"),
|
"exit_price": trade.get("exit"),
|
||||||
"profit_pct": trade.get("profit_pct"),
|
"profit_pct": trade.get("profit_pct"),
|
||||||
"type": trade.get("type", ""),
|
"type": trade.get("type"),
|
||||||
|
"fee_usd": trade.get("fee_usd"),
|
||||||
})
|
})
|
||||||
logging.info(f"Timeframe: {rule_name}, Stop Loss: {stop_loss_pct}, Trades: {n_trades}")
|
logging.info(f"Timeframe: {rule_name}, Stop Loss: {stop_loss_pct}, Trades: {n_trades}")
|
||||||
if debug:
|
if debug:
|
||||||
@ -136,6 +139,7 @@ def aggregate_results(all_rows):
|
|||||||
|
|
||||||
# Calculate final USD
|
# Calculate final USD
|
||||||
final_usd = np.mean([r.get('final_usd', initial_usd) for r in rows])
|
final_usd = np.mean([r.get('final_usd', initial_usd) for r in rows])
|
||||||
|
total_fees_usd = np.mean([r.get('total_fees_usd') for r in rows])
|
||||||
|
|
||||||
summary_rows.append({
|
summary_rows.append({
|
||||||
"timeframe": rule,
|
"timeframe": rule,
|
||||||
@ -148,6 +152,7 @@ def aggregate_results(all_rows):
|
|||||||
"profit_ratio": avg_profit_ratio,
|
"profit_ratio": avg_profit_ratio,
|
||||||
"initial_usd": initial_usd,
|
"initial_usd": initial_usd,
|
||||||
"final_usd": final_usd,
|
"final_usd": final_usd,
|
||||||
|
"total_fees_usd": total_fees_usd,
|
||||||
})
|
})
|
||||||
return summary_rows
|
return summary_rows
|
||||||
|
|
||||||
@ -166,7 +171,9 @@ if __name__ == "__main__":
|
|||||||
# stop_date = '2023-01-01'
|
# stop_date = '2023-01-01'
|
||||||
start_date = '2024-05-15'
|
start_date = '2024-05-15'
|
||||||
stop_date = '2025-05-15'
|
stop_date = '2025-05-15'
|
||||||
|
|
||||||
initial_usd = 10000
|
initial_usd = 10000
|
||||||
|
|
||||||
debug = False
|
debug = False
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M")
|
||||||
@ -194,11 +201,6 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
workers = system_utils.get_optimal_workers()
|
workers = system_utils.get_optimal_workers()
|
||||||
|
|
||||||
# Start the background batch pusher
|
|
||||||
# spreadsheet_name = "GlimBit Backtest Results"
|
|
||||||
# batch_pusher = GSheetBatchPusher(results_queue, timestamp, spreadsheet_name, interval=65)
|
|
||||||
# batch_pusher.start()
|
|
||||||
|
|
||||||
# Process tasks with optimized concurrency
|
# Process tasks with optimized concurrency
|
||||||
with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
|
with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
|
||||||
futures = {executor.submit(process_timeframe, task, debug): task for task in tasks}
|
futures = {executor.submit(process_timeframe, task, debug): task for task in tasks}
|
||||||
@ -209,56 +211,18 @@ if __name__ == "__main__":
|
|||||||
if results or trades:
|
if results or trades:
|
||||||
all_results_rows.extend(results)
|
all_results_rows.extend(results)
|
||||||
all_trade_rows.extend(trades)
|
all_trade_rows.extend(trades)
|
||||||
# results_queue.put((results, trades)) # Enqueue for batch update
|
|
||||||
|
|
||||||
# After all tasks, flush any remaining updates
|
|
||||||
# batch_pusher.stop()
|
|
||||||
# batch_pusher.join()
|
|
||||||
|
|
||||||
# Ensure all batches are pushed, even after 429 errors
|
|
||||||
# while not results_queue.empty():
|
|
||||||
# logging.info("Waiting for Google Sheets quota to reset. Retrying batch push in 60 seconds...")
|
|
||||||
# time.sleep(65)
|
|
||||||
# batch_pusher.push_all()
|
|
||||||
|
|
||||||
# Write all results to a single CSV file
|
# Write all results to a single CSV file
|
||||||
combined_filename = os.path.join(f"{timestamp}_backtest_combined.csv")
|
combined_filename = os.path.join(f"{timestamp}_backtest_combined.csv")
|
||||||
combined_fieldnames = [
|
combined_fieldnames = [
|
||||||
"timeframe", "stop_loss_pct", "n_trades", "n_stop_loss", "win_rate",
|
"timeframe", "stop_loss_pct", "n_trades", "n_stop_loss", "win_rate",
|
||||||
"max_drawdown", "avg_trade", "profit_ratio", "final_usd"
|
"max_drawdown", "avg_trade", "profit_ratio", "final_usd", "total_fees_usd"
|
||||||
]
|
]
|
||||||
storage.write_results_combined(combined_filename, combined_fieldnames, all_results_rows)
|
storage.write_results_combined(combined_filename, combined_fieldnames, all_results_rows)
|
||||||
|
|
||||||
# --- Add taxes to combined results CSV ---
|
|
||||||
# taxes = Taxes() # Default 20% tax rate
|
|
||||||
# taxed_filename = combined_filename.replace('.csv', '_taxed.csv')
|
|
||||||
# taxes.add_taxes_to_results_csv(combined_filename, taxed_filename, profit_col='total_profit')
|
|
||||||
# logging.info(f"Taxed results written to {taxed_filename}")
|
|
||||||
|
|
||||||
# --- Write trades to separate CSVs per timeframe and stop loss ---
|
|
||||||
# Collect all trades from each task (need to run tasks to collect trades)
|
|
||||||
# Since only all_results_rows is collected above, we need to also collect all trades.
|
|
||||||
# To do this, modify the above loop to collect all trades as well.
|
|
||||||
# But for now, let's assume you have a list all_trade_rows (list of dicts)
|
|
||||||
# If not, you need to collect it in the ProcessPoolExecutor loop above.
|
|
||||||
|
|
||||||
# --- BEGIN: Collect all trades from each task ---
|
|
||||||
# To do this, modify the ProcessPoolExecutor loop above:
|
|
||||||
# all_results_rows = []
|
|
||||||
# all_trade_rows = []
|
|
||||||
# ...
|
|
||||||
# for future in concurrent.futures.as_completed(futures):
|
|
||||||
# results, trades = future.result()
|
|
||||||
# if results or trades:
|
|
||||||
# all_results_rows.extend(results)
|
|
||||||
# all_trade_rows.extend(trades)
|
|
||||||
# --- END: Collect all trades from each task ---
|
|
||||||
|
|
||||||
# Now, group all_trade_rows by (timeframe, stop_loss_pct)
|
# Now, group all_trade_rows by (timeframe, stop_loss_pct)
|
||||||
|
|
||||||
|
|
||||||
trades_fieldnames = [
|
trades_fieldnames = [
|
||||||
"entry_time", "exit_time", "entry_price", "exit_price", "profit_pct", "type"
|
"entry_time", "exit_time", "entry_price", "exit_price", "profit_pct", "type", "fee_usd"
|
||||||
]
|
]
|
||||||
storage.write_trades(all_trade_rows, trades_fieldnames)
|
storage.write_trades(all_trade_rows, trades_fieldnames)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user