From 4f11778b094749cab8503b8df54ce3f67303231f Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 13 Nov 2024 22:15:35 -0400 Subject: [PATCH] The test are running and returning feedback. I am just about to implement a better solution for multithreading --- src/PythonGenerator.py | 10 +- src/backtesting.py | 182 ++++++++---------- .../generators/values_and_flags_generators.js | 4 +- 3 files changed, 91 insertions(+), 105 deletions(-) diff --git a/src/PythonGenerator.py b/src/PythonGenerator.py index 07edb7b..3ca552c 100644 --- a/src/PythonGenerator.py +++ b/src/PythonGenerator.py @@ -1201,7 +1201,7 @@ class PythonGenerator: :param indent_level: Current indentation level. :return: A string representing the variable retrieval. """ - variable_name = node.get('variable_name') + variable_name = node.get('variable_name', '').strip() if not variable_name: logger.error("get_variable node missing 'variable_name'.") return 'None' @@ -1217,7 +1217,7 @@ class PythonGenerator: """ code_lines = [] indent = ' ' * indent_level - variable_name = node.get('variable_name') + variable_name = node.get('variable_name', '').strip() value_node = node.get('values') if not variable_name: @@ -1391,9 +1391,9 @@ class PythonGenerator: if operator not in operator_map: logger.error(f"Invalid operator for math_operation: {operator}. Defaulting to 'ADD'.") - left_expr = self.process_numeric_list(left_operand, indent_level) - right_expr = self.process_numeric_list(right_operand, indent_level) - expr = f"{left_expr} {python_operator} {right_expr}" + left_expr = self.generate_condition_code(left_operand, indent_level) + right_expr = self.generate_condition_code(right_operand, indent_level) + expr = f"({left_expr} {python_operator} {right_expr})" logger.debug(f"Generated math_operation expression: {expr}") return expr diff --git a/src/backtesting.py b/src/backtesting.py index c499369..124f43b 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -197,13 +197,60 @@ class Backtester: for name in self.indicator_names: self.indicator_pointers[name] = 0 # Start at the first row + # Initialize an empty list to store orders + self.orders = [] + self.trade_list = [] + # Initialize any other needed variables self.starting_balance = self.broker.getvalue() - - # Existing code... self.current_step = 0 self.last_progress = 0 # Initialize last_progress + def notify_order(self, order): + if order.status in [order.Submitted, order.Accepted]: + # Order has been submitted/accepted by broker - nothing to do + return + + if order.status in [order.Completed]: + if order.isbuy(): + self.log(f"BUY EXECUTED, Price: {order.executed.price}, Size: {order.executed.size}") + elif order.issell(): + self.log(f"SELL EXECUTED, Price: {order.executed.price}, Size: {order.executed.size}") + self.bar_executed = len(self) + elif order.status in [order.Canceled, order.Margin, order.Rejected]: + self.log('Order Canceled/Margin/Rejected') + + # Remove the order from the list + if order in self.orders: + self.orders.remove(order) + + def notify_trade(self, trade): + if not trade.isclosed: + return + + self.log(f"TRADE CLOSED, GROSS P/L: {trade.pnl}, NET P/L: {trade.pnlcomm}") + + # Convert datetime objects to ISO-formatted strings + open_datetime = bt.num2date(trade.dtopen).isoformat() if trade.dtopen else None + close_datetime = bt.num2date(trade.dtclose).isoformat() if trade.dtclose else None + + # Store the trade details for later use + trade_info = { + 'ref': trade.ref, + 'size': trade.size, + 'price': trade.price, + 'pnl': trade.pnl, + 'pnlcomm': trade.pnlcomm, + 'open_datetime': open_datetime, + 'close_datetime': close_datetime + } + self.trade_list.append(trade_info) + + def log(self, txt, dt=None): + """ Logging function for this strategy""" + dt = dt or self.datas[0].datetime.datetime(0) + logger.info(f"{dt.isoformat()} - {txt}") + def next(self): self.current_step += 1 # Execute the strategy logic @@ -257,19 +304,6 @@ class Backtester: """ Custom trade_order method for backtesting. Executes trades within the Backtrader environment. - - :param trade_type: Type of trade ('buy' or 'sell'). - :param size: Size of the trade. - :param order_type: Type of order (e.g., 'market'). - :param source: Dictionary containing additional trade information, including 'market'. - :param tif: Time in Force for the order. - :param stop_loss: Dictionary with stop loss parameters. - :param trailing_stop: Dictionary with trailing stop parameters. - :param take_profit: Dictionary with take profit parameters. - :param limit: Dictionary with limit order parameters. - :param trailing_limit: Dictionary with trailing limit parameters. - :param target_market: Dictionary with target market parameters. - :param name_order: Dictionary with order name parameters. """ # Validate and extract 'symbol' from 'source' if source and 'market' in source: @@ -279,42 +313,44 @@ class Backtester: logger.error("Symbol not provided in source. Order not executed.") return # Abort the order execution + price = strategy_instance.backtrader_strategy.data.close[0] + if trade_type.lower() == 'buy': logger.info(f"Executing BUY order: Size={size}, Symbol={symbol}, Order Type={order_type}") - # Execute a buy order in Backtrader via Cerebro - order = strategy_instance.backtrader_strategy.buy(size=size, exectype=bt.Order.Market, name=symbol) + + # Prepare bracket order parameters + stop_loss_price = stop_loss.get('value') if stop_loss else None + take_profit_price = take_profit.get('value') if take_profit else None + + # Create bracket order and store the orders + bracket_orders = strategy_instance.backtrader_strategy.buy_bracket( + size=size, + price=price, + stopprice=stop_loss_price, + limitprice=take_profit_price, + exectype=bt.Order.Market + ) elif trade_type.lower() == 'sell': logger.info(f"Executing SELL order: Size={size}, Symbol={symbol}, Order Type={order_type}") - # Execute a sell order in Backtrader via Cerebro - order = strategy_instance.backtrader_strategy.sell(size=size, exectype=bt.Order.Market, name=symbol) + + # Prepare bracket order parameters + stop_loss_price = stop_loss.get('value') if stop_loss else None + take_profit_price = take_profit.get('value') if take_profit else None + + # Create bracket order and store the orders + bracket_orders = strategy_instance.backtrader_strategy.sell_bracket( + size=size, + price=price, + stopprice=stop_loss_price, + limitprice=take_profit_price, + exectype=bt.Order.Market + ) else: logger.error(f"Invalid trade_type '{trade_type}'. Order not executed.") return # Abort the order execution - # Handle trade options like stop_loss and take_profit - if stop_loss or take_profit: - if stop_loss: - stop_price = stop_loss.get('value') - if stop_price is not None: - logger.info(f"Setting STOP LOSS at {stop_price} for order {order.ref}") - strategy_instance.backtrader_strategy.sell( - size=size, - exectype=bt.Order.Stop, - price=stop_price, - parent=order, - name=f"StopLoss_{order.ref}" - ) - if take_profit: - take_profit_price = take_profit.get('value') - if take_profit_price is not None: - logger.info(f"Setting TAKE PROFIT at {take_profit_price} for order {order.ref}") - strategy_instance.backtrader_strategy.sell( - size=size, - exectype=bt.Order.Limit, - price=take_profit_price, - parent=order, - name=f"TakeProfit_{order.ref}" - ) + # Store the orders for tracking + strategy_instance.backtrader_strategy.orders.extend(bracket_orders) # Notify user about the trade execution strategy_instance.notify_user( @@ -706,12 +742,12 @@ class Backtester: final_value = cerebro.broker.getvalue() run_duration = (end_time - start_time).total_seconds() - # Extract equity curve from analyzers - equity_curve = results[0].analyzers.equity_curve.get_analysis().get('equity_curve', []) + strategy = results[0] - # Extract trade data from TradeAnalyzer - trade_analyzer = results[0].analyzers.trade_analyzer.get_analysis() - trades = self.parse_trade_analyzer(trade_analyzer) + # Extract equity curve from analyzers + equity_curve = strategy.analyzers.equity_curve.get_analysis().get('equity_curve', []) + + trades = strategy.trade_list # Send 100% completion self.socketio.emit('message', {'reply': 'progress', 'data': {'progress': 100}}, room=socket_conn_id) @@ -1052,54 +1088,4 @@ class Backtester: # Kept here for backward compatibility or future use return [] - def parse_trade_analyzer(self, trade_analyzer: dict) -> list: - """ - Parse the TradeAnalyzer results from Backtrader and return a list of trades. - :param trade_analyzer: Dictionary containing trade analysis. - :return: List of trade dictionaries with relevant information. - """ - trades = [] - if not trade_analyzer: - logger.warning("No trade data available in TradeAnalyzer.") - return trades - - # TradeAnalyzer stores trades under 'trades' - trade_list = trade_analyzer.get('trades', {}) - - # Check if 'trades' is a dict (with trade references) or a list - if isinstance(trade_list, dict): - for ref, trade in trade_list.items(): - trade_info = { - 'ref': ref, - 'size': trade.get('size'), - 'price': trade.get('price'), - 'value': trade.get('value'), - 'pnl': trade.get('pnl'), - 'commission': trade.get('commission'), - 'opendate': trade.get('opendate'), - 'closedate': trade.get('closedate'), - 'status': trade.get('status'), - } - trades.append(trade_info) - logger.debug(f"Parsed trade: {trade_info}") - elif isinstance(trade_list, list): - for trade in trade_list: - trade_info = { - 'ref': trade.get('ref'), - 'size': trade.get('size'), - 'price': trade.get('price'), - 'value': trade.get('value'), - 'pnl': trade.get('pnl'), - 'commission': trade.get('commission'), - 'opendate': trade.get('opendate'), - 'closedate': trade.get('closedate'), - 'status': trade.get('status'), - } - trades.append(trade_info) - logger.debug(f"Parsed trade: {trade_info}") - else: - logger.error("Unexpected format for 'trades' in TradeAnalyzer.") - - logger.info(f"Parsed {len(trades)} trades from TradeAnalyzer.") - return trades diff --git a/src/static/blocks/generators/values_and_flags_generators.js b/src/static/blocks/generators/values_and_flags_generators.js index 9c0af73..cc5e65f 100644 --- a/src/static/blocks/generators/values_and_flags_generators.js +++ b/src/static/blocks/generators/values_and_flags_generators.js @@ -165,13 +165,13 @@ export function defineVAFGenerators() { // Retrieve the 'variable_name' field input const variableName = currentBlock.getFieldValue('variable_name'); const trimmedName = variableName && variableName.trim() !== "" ? variableName.trim() : 'undefined_var'; - if (variableName.trim() === "") { + if (variableName === "") { console.warn("Empty variable_name in get_variable block. Defaulting to 'undefined_var'."); } variables.push({ type: 'get_variable', - variable_name: variableName + variable_name: trimmedName }); // Process the 'NEXT' connection