|
|
|
import backtrader as bt |
|
import sys |
|
import os |
|
import pandas as pd |
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) |
|
from data.api_client import YahooFinanceClient |
|
|
|
|
|
class Logger: |
|
def __init__(self): |
|
self.logs = [] |
|
|
|
def add_log(self, log_entry): |
|
self.logs.append(log_entry) |
|
|
|
def get_logs(self): |
|
return self.logs.copy() |
|
|
|
class CustomStrategy(bt.Strategy): |
|
params = ( |
|
('analysis', None), |
|
('logger', None), |
|
('rsi_period', 14), |
|
('rsi_upper', 70), |
|
('rsi_lower', 30), |
|
('sma_short', 50), |
|
('sma_long', 200), |
|
('max_loss_percent', 0.02), |
|
('take_profit_percent', 0.05), |
|
('position_size', 0.1), |
|
('atr_period', 14), |
|
('atr_multiplier', 3), |
|
('sentiment_threshold', 0.6), |
|
('confidence_threshold', 0.7) |
|
) |
|
|
|
def __init__(self): |
|
|
|
self.logger = self.params.logger |
|
self.recommendation = self.params.analysis['recommendation'] if self.params.analysis else 'HOLD' |
|
self.technical_analysis = self.params.analysis['technical'] if self.params.analysis else None |
|
self.sentiment_analysis = self.params.analysis['sentiment'] if self.params.analysis else None |
|
self.confidence = self.params.analysis['confidence']['total_confidence'] if self.params.analysis else 0.5 |
|
|
|
|
|
self.rsi = bt.indicators.RSI( |
|
self.data.close, |
|
period=self.params.rsi_period |
|
) |
|
|
|
self.sma_short = bt.indicators.SMA( |
|
self.data.close, |
|
period=self.params.sma_short |
|
) |
|
|
|
self.sma_long = bt.indicators.SMA( |
|
self.data.close, |
|
period=self.params.sma_long |
|
) |
|
|
|
|
|
|
|
self.rsi = bt.indicators.RSI(self.data.close, period=self.p.rsi_period) |
|
self.sma_short = bt.indicators.SMA(self.data.close, period=self.p.sma_short) |
|
self.sma_long = bt.indicators.SMA(self.data.close, period=self.p.sma_long) |
|
|
|
|
|
self.atr = bt.indicators.ATR(self.data, period=self.p.atr_period) |
|
|
|
|
|
self.order = None |
|
self.stop_price = None |
|
self.take_profit_price = None |
|
self.buy_price = None |
|
self.entry_date = None |
|
|
|
def log(self, txt, dt=None): |
|
dt = dt or self.datas[0].datetime.date(0) |
|
log_entry = f'{dt.isoformat()}, {txt}' |
|
self.logger.add_log(log_entry) |
|
print(log_entry) |
|
|
|
def notify_order(self, order): |
|
if order.status in [order.Submitted, order.Accepted]: |
|
return |
|
|
|
if order.status in [order.Completed]: |
|
if order.isbuy(): |
|
self.log(f'BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}') |
|
self.buy_price = order.executed.price |
|
self.entry_date = self.datas[0].datetime.date(0) |
|
else: |
|
self.log(f'SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm {order.executed.comm:.2f}') |
|
|
|
self.order = None |
|
|
|
def notify_trade(self, trade): |
|
if not trade.isclosed: |
|
return |
|
|
|
self.log(f'TRADE PROFIT, GROSS: {trade.pnl:.2f}, NET: {trade.pnlcomm:.2f}') |
|
|
|
def calculate_position_size(self): |
|
portfolio_value = self.broker.getvalue() |
|
return int((portfolio_value * self.p.position_size) / self.data.close[0]) |
|
|
|
def next(self): |
|
|
|
if self.order: |
|
return |
|
|
|
current_price = self.data.close[0] |
|
|
|
|
|
stop_loss = current_price * (1 - self.params.max_loss_percent) |
|
take_profit = current_price * (1 + self.params.take_profit_percent) |
|
|
|
|
|
analysis_confirmation = self._analyze_prior_research() |
|
|
|
|
|
if not self.position: |
|
|
|
|
|
entry_conditions = ( |
|
current_price > self.sma_long[0] and |
|
self.rsi[0] < self.params.rsi_lower and |
|
bool(self.params.analysis['confidence']['total_confidence'] > self.p.confidence_threshold) |
|
) |
|
|
|
if entry_conditions: |
|
|
|
size = self.calculate_position_size() |
|
|
|
|
|
self.order = self.buy(size=size) |
|
|
|
|
|
stop_loss = current_price * (1 - self.p.max_loss_percent) |
|
take_profit = current_price * (1 + self.p.take_profit_percent) |
|
|
|
|
|
atr_stop = current_price - (self.atr[0] * self.p.atr_multiplier) |
|
self.stop_price = max(stop_loss, atr_stop) |
|
self.take_profit_price = take_profit |
|
|
|
|
|
else: |
|
|
|
exit_conditions = ( |
|
current_price < self.stop_price or |
|
current_price > self.take_profit_price or |
|
self.rsi[0] > self.p.rsi_upper or |
|
current_price < self.sma_short[0] or |
|
not analysis_confirmation |
|
) |
|
|
|
if exit_conditions: |
|
self.close() |
|
self.stop_price = None |
|
self.take_profit_price = None |
|
|
|
def _analyze_prior_research(self): |
|
|
|
if not self.p.analysis: |
|
return True |
|
|
|
|
|
sentiment_positive = ( |
|
self.sentiment_analysis and |
|
self.sentiment_analysis['positive'] > self.p.sentiment_threshold |
|
) |
|
|
|
|
|
technical_bullish = ( |
|
self.technical_analysis and |
|
self.technical_analysis['trend'] == 'bullish' |
|
) |
|
|
|
|
|
high_confidence = bool(self.confidence > self.p.confidence_threshold) |
|
|
|
|
|
return sentiment_positive and technical_bullish and high_confidence |
|
|
|
def stop(self): |
|
|
|
self.log('Final Portfolio Value: %.2f' % self.broker.getvalue()) |
|
|
|
class BacktraderIntegration: |
|
def __init__(self, analysis_result=None, strategy_params=None): |
|
self.cerebro = bt.Cerebro() |
|
self.analysis = analysis_result |
|
self.strategy_params = strategy_params or {} |
|
self.logger = Logger() |
|
self.setup_environment() |
|
|
|
def setup_environment(self): |
|
self.cerebro.broker.setcash(100000.0) |
|
self.cerebro.broker.setcommission(commission=0.001) |
|
self.cerebro.addstrategy(CustomStrategy, analysis=self.analysis, logger=self.logger, **self.strategy_params) |
|
|
|
def add_data_feed(self, ticker, start_date, end_date): |
|
|
|
start_str = start_date.strftime("%Y-%m-%d") |
|
end_str = end_date.strftime("%Y-%m-%d") |
|
|
|
|
|
df = YahooFinanceClient.download_data(ticker, start_str, end_str, period=None) |
|
|
|
|
|
if not isinstance(df, pd.DataFrame): |
|
raise TypeError(f"Esperado pandas.DataFrame, mas recebeu {type(df)}") |
|
|
|
|
|
if isinstance(df.columns, pd.MultiIndex): |
|
df.columns = df.columns.droplevel(1) |
|
|
|
|
|
expected_columns = ["Open", "High", "Low", "Close", "Volume"] |
|
|
|
|
|
if not all(col in df.columns for col in expected_columns): |
|
raise ValueError(f"Colunas do DataFrame incorretas: {df.columns}") |
|
|
|
data = bt.feeds.PandasData(dataname=df) |
|
self.cerebro.adddata(data) |
|
|
|
def run_simulation(self, initial_cash, commission): |
|
self.cerebro.broker.setcash(initial_cash) |
|
self.cerebro.broker.setcommission(commission=commission/100) |
|
self.cerebro.run() |
|
|
|
logs = self.logger.get_logs() |
|
|
|
return self.cerebro.broker.getvalue(), logs |