Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backtrader/feeds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@

from .rollover import RollOver
from .chainer import Chainer

from .sentimentfeed import SentimentCSVData, SentimentCSV
37 changes: 37 additions & 0 deletions backtrader/feeds/sentimentfeed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2023 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
unicode_literals)

from .csvgeneric import GenericCSVData, GenericCSV
from .. import feed


class SentimentCSVData(GenericCSVData):
lines = ('sentiment',)

params = (
('sentiment', 7),
)


class SentimentCSV(GenericCSV):
DataCls = SentimentCSVData
1 change: 1 addition & 0 deletions backtrader/sizers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@

from .fixedsize import *
from .percents_sizer import *
from .sentiment_sizer import SentimentSizer
93 changes: 93 additions & 0 deletions backtrader/sizers/sentiment_sizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2023 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
unicode_literals)

import backtrader as bt


class SentimentSizer(bt.Sizer):
'''
Dynamic position sizing based on smoothed sentiment value.

The size is determined by the absolute value of the smoothed sentiment:
- If abs(sentiment) >= extreme_threshold: use large_stake
- If medium_threshold <= abs(sentiment) < extreme_threshold: use small_stake
- If abs(sentiment) < medium_threshold: use 0 (no trade)

This sizer expects the strategy to have an attribute `smoothed_sentiment`
that provides the smoothed sentiment value (e.g., 5-day SMA).
'''

params = (
('extreme_threshold', 0.6),
('medium_threshold', 0.3),
('large_stake', 100),
('small_stake', 10),
)

def __init__(self):
pass

def _getsizing(self, comminfo, cash, data, isbuy):
position = self.strategy.getposition(data)

if not position:
smoothed_sentiment = self._get_smoothed_sentiment()
if smoothed_sentiment is None:
return self.p.small_stake

abs_sentiment = abs(smoothed_sentiment)

if abs_sentiment >= self.p.extreme_threshold:
size = self.p.large_stake
elif abs_sentiment >= self.p.medium_threshold:
size = self.p.small_stake
else:
size = 0

max_possible = int(cash / data.close[0])
size = min(size, max_possible)

return size
else:
return position.size

def _get_smoothed_sentiment(self):
if hasattr(self.strategy, 'smoothed_sentiment'):
try:
return self.strategy.smoothed_sentiment[0]
except (TypeError, IndexError):
return None

if hasattr(self.strategy, 'sentiment_sma'):
try:
return self.strategy.sentiment_sma[0]
except (TypeError, IndexError):
return None

if hasattr(self.data, 'sentiment'):
try:
return self.data.sentiment[0]
except (TypeError, IndexError):
return None

return None
1 change: 1 addition & 0 deletions backtrader/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
unicode_literals)

from .sma_crossover import *
from .sentiment_strategy import SentimentStrategy
121 changes: 121 additions & 0 deletions backtrader/strategies/sentiment_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
# Copyright (C) 2015-2023 Daniel Rodriguez
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
unicode_literals)

import backtrader as bt


class SentimentStrategy(bt.Strategy):
'''
Sentiment-based trading strategy with SMA smoothing.

Features:
- 5-day SMA smoothing on sentiment data to reduce noise
- Uses smoothed sentiment for trading decisions
- Exposes smoothed sentiment for dynamic position sizing (SentimentSizer)

Buy Logic:
- No position is open
- Smoothed sentiment > buy_threshold

Sell Logic:
- A position exists
- Smoothed sentiment < sell_threshold
'''

params = (
('buy_threshold', 0.5),
('sell_threshold', -0.3),
('sma_period', 5),
('use_smoothed', True),
('prdata', True),
('prtrade', True),
)

def __init__(self):
self.raw_sentiment = self.data.sentiment

if self.p.use_smoothed:
self.sentiment_sma = bt.indicators.SMA(
self.data.sentiment,
period=self.p.sma_period
)
self.smoothed_sentiment = self.sentiment_sma
else:
self.smoothed_sentiment = self.raw_sentiment

self.sentiment = self.smoothed_sentiment

def next(self):
current_date = self.data.datetime.date(0)
current_raw_sentiment = self.raw_sentiment[0]
current_close = self.data.close[0]

try:
current_smoothed = self.smoothed_sentiment[0]
except (TypeError, IndexError):
current_smoothed = None

if self.p.prdata:
if current_smoothed is not None:
print(f'DATE: {current_date}, CLOSE: {current_close:.2f}, '
f'RAW_SENT: {current_raw_sentiment:.2f}, '
f'SMOOTHED: {current_smoothed:.2f}')
else:
print(f'DATE: {current_date}, CLOSE: {current_close:.2f}, '
f'RAW_SENT: {current_raw_sentiment:.2f}')

if current_smoothed is None:
return

if not self.position:
if current_smoothed > self.p.buy_threshold:
if self.p.prdata:
print(f' >>> BUY SIGNAL: smoothed sentiment ({current_smoothed:.2f}) > threshold ({self.p.buy_threshold})')
self.buy()
else:
if current_smoothed < self.p.sell_threshold:
if self.p.prdata:
print(f' <<< SELL SIGNAL: smoothed sentiment ({current_smoothed:.2f}) < threshold ({self.p.sell_threshold})')
self.sell()

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return

if order.status in [order.Completed]:
if order.isbuy():
print(f' --- ORDER EXECUTED: BUY {order.executed.size} shares at {order.executed.price:.2f}, '
f'Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')
else:
print(f' --- ORDER EXECUTED: SELL {abs(order.executed.size)} shares at {order.executed.price:.2f}, '
f'Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')

elif order.status in [order.Canceled, order.Margin, order.Rejected]:
print(f' --- ORDER FAILED: {order.getstatusname()}')

def notify_trade(self, trade):
if not trade.isclosed:
return

if self.p.prtrade:
print(f' === TRADE CLOSED: PnL Gross: {trade.pnl:.2f}, Net: {trade.pnlcomm:.2f}')
30 changes: 30 additions & 0 deletions datas/sentiment-test-data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Date,Open,High,Low,Close,Volume,OpenInterest,Sentiment
2024-01-02,100.0,102.0,99.5,101.5,10000,0,0.1
2024-01-03,101.5,103.0,101.0,102.5,12000,0,0.2
2024-01-04,102.5,105.0,102.0,104.0,15000,0,0.3
2024-01-05,104.0,106.0,103.5,105.5,18000,0,0.4
2024-01-08,105.5,107.0,105.0,106.0,20000,0,0.5
2024-01-09,106.0,107.5,105.5,107.0,15000,0,0.6
2024-01-10,107.0,108.0,104.0,105.0,25000,0,0.7
2024-01-11,105.0,106.0,102.0,103.0,30000,0,0.8
2024-01-12,103.0,104.5,101.0,102.0,20000,0,0.85
2024-01-15,102.0,103.5,100.5,101.5,18000,0,0.9
2024-01-16,101.5,104.0,101.0,103.5,15000,0,0.88
2024-01-17,103.5,106.0,103.0,105.0,16000,0,0.85
2024-01-18,105.0,107.5,104.5,107.0,17000,0,0.5
2024-01-19,107.0,108.0,105.5,106.0,14000,0,0.2
2024-01-22,106.0,106.5,103.0,104.0,22000,0,-0.1
2024-01-23,104.0,105.0,102.0,103.0,19000,0,-0.3
2024-01-24,103.0,104.5,101.5,102.5,15000,0,-0.5
2024-01-25,102.5,105.0,102.0,104.5,16000,0,-0.6
2024-01-26,104.5,107.0,104.0,106.0,18000,0,-0.7
2024-01-29,106.0,108.0,105.5,107.5,19000,0,-0.75
2024-01-30,107.5,109.0,107.0,108.5,20000,0,-0.8
2024-01-31,108.5,110.0,108.0,109.5,21000,0,-0.82
2024-02-01,109.5,111.0,109.0,110.5,22000,0,-0.5
2024-02-02,110.5,112.0,110.0,111.5,23000,0,-0.2
2024-02-05,111.5,113.0,111.0,112.5,24000,0,0.1
2024-02-06,112.5,114.0,112.0,113.5,25000,0,0.4
2024-02-07,113.5,115.0,113.0,114.5,26000,0,0.6
2024-02-08,114.5,116.0,114.0,115.5,27000,0,0.7
2024-02-09,115.5,117.0,115.0,116.5,28000,0,0.75
Loading