From 802b5e92298b76e76a69805ccd90a095786bbe58 Mon Sep 17 00:00:00 2001
From: ouyu <1986834078@qq.com>
Date: Thu, 16 Apr 2026 21:34:06 +0800
Subject: [PATCH 1/2] =?UTF-8?q?feat(=E7=AD=96=E7=95=A5):=20=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E5=9F=BA=E4=BA=8E=E6=83=85=E6=84=9F=E5=88=86=E6=9E=90?=
=?UTF-8?q?=E7=9A=84=E4=BA=A4=E6=98=93=E7=AD=96=E7=95=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
添加情感数据源SentimentCSVData和SentimentCSV
实现SentimentStrategy策略,根据情感指标进行买卖决策
添加测试数据和测试脚本验证策略功能
---
backtrader/feeds/__init__.py | 2 +
backtrader/feeds/sentimentfeed.py | 37 +++++++
backtrader/strategies/__init__.py | 1 +
backtrader/strategies/sentiment_strategy.py | 87 +++++++++++++++
datas/sentiment-test-data.csv | 20 ++++
.../test_sentiment_strategy.py | 101 ++++++++++++++++++
6 files changed, 248 insertions(+)
create mode 100644 backtrader/feeds/sentimentfeed.py
create mode 100644 backtrader/strategies/sentiment_strategy.py
create mode 100644 datas/sentiment-test-data.csv
create mode 100644 samples/sentiment-strategy/test_sentiment_strategy.py
diff --git a/backtrader/feeds/__init__.py b/backtrader/feeds/__init__.py
index 8054715dc..2a201ee39 100644
--- a/backtrader/feeds/__init__.py
+++ b/backtrader/feeds/__init__.py
@@ -52,3 +52,5 @@
from .rollover import RollOver
from .chainer import Chainer
+
+from .sentimentfeed import SentimentCSVData, SentimentCSV
diff --git a/backtrader/feeds/sentimentfeed.py b/backtrader/feeds/sentimentfeed.py
new file mode 100644
index 000000000..ac19445bf
--- /dev/null
+++ b/backtrader/feeds/sentimentfeed.py
@@ -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 .
+#
+###############################################################################
+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
diff --git a/backtrader/strategies/__init__.py b/backtrader/strategies/__init__.py
index b453b16f3..e8ced2b26 100644
--- a/backtrader/strategies/__init__.py
+++ b/backtrader/strategies/__init__.py
@@ -22,3 +22,4 @@
unicode_literals)
from .sma_crossover import *
+from .sentiment_strategy import SentimentStrategy
diff --git a/backtrader/strategies/sentiment_strategy.py b/backtrader/strategies/sentiment_strategy.py
new file mode 100644
index 000000000..a3648471c
--- /dev/null
+++ b/backtrader/strategies/sentiment_strategy.py
@@ -0,0 +1,87 @@
+#!/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 .
+#
+###############################################################################
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+import backtrader as bt
+
+
+class SentimentStrategy(bt.Strategy):
+ '''
+ Sentiment-based trading strategy.
+
+ Buy Logic:
+ - No position is open
+ - Today's sentiment > 0.8
+
+ Sell Logic:
+ - A position exists
+ - Today's sentiment < -0.5
+ '''
+
+ params = (
+ ('buy_threshold', 0.8),
+ ('sell_threshold', -0.5),
+ ('prdata', True),
+ ('prtrade', True),
+ )
+
+ def __init__(self):
+ self.sentiment = self.data.sentiment
+
+ def next(self):
+ current_date = self.data.datetime.date(0)
+ current_sentiment = self.data.sentiment[0]
+ current_close = self.data.close[0]
+
+ if self.p.prdata:
+ print(f'DATE: {current_date}, CLOSE: {current_close:.2f}, SENTIMENT: {current_sentiment:.2f}')
+
+ if not self.position:
+ if current_sentiment > self.p.buy_threshold:
+ if self.p.prdata:
+ print(f' >>> BUY SIGNAL: sentiment ({current_sentiment:.2f}) > threshold ({self.p.buy_threshold})')
+ self.buy()
+ else:
+ if current_sentiment < self.p.sell_threshold:
+ if self.p.prdata:
+ print(f' <<< SELL SIGNAL: sentiment ({current_sentiment:.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 at {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')
+ else:
+ print(f' --- ORDER EXECUTED: SELL at {order.executed.price:.2f}, 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}')
diff --git a/datas/sentiment-test-data.csv b/datas/sentiment-test-data.csv
new file mode 100644
index 000000000..0a7277f12
--- /dev/null
+++ b/datas/sentiment-test-data.csv
@@ -0,0 +1,20 @@
+Date,Open,High,Low,Close,Volume,OpenInterest,Sentiment
+2024-01-02,100.0,102.0,99.5,101.5,10000,0,0.3
+2024-01-03,101.5,103.0,101.0,102.5,12000,0,0.5
+2024-01-04,102.5,105.0,102.0,104.0,15000,0,0.9
+2024-01-05,104.0,106.0,103.5,105.5,18000,0,0.85
+2024-01-08,105.5,107.0,105.0,106.0,20000,0,0.6
+2024-01-09,106.0,107.5,105.5,107.0,15000,0,0.4
+2024-01-10,107.0,108.0,104.0,105.0,25000,0,-0.3
+2024-01-11,105.0,106.0,102.0,103.0,30000,0,-0.6
+2024-01-12,103.0,104.5,101.0,102.0,20000,0,-0.7
+2024-01-15,102.0,103.5,100.5,101.5,18000,0,-0.2
+2024-01-16,101.5,104.0,101.0,103.5,15000,0,0.7
+2024-01-17,103.5,106.0,103.0,105.0,16000,0,0.95
+2024-01-18,105.0,107.5,104.5,107.0,17000,0,0.88
+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.55
+2024-01-23,104.0,105.0,102.0,103.0,19000,0,-0.8
+2024-01-24,103.0,104.5,101.5,102.5,15000,0,-0.1
+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.82
diff --git a/samples/sentiment-strategy/test_sentiment_strategy.py b/samples/sentiment-strategy/test_sentiment_strategy.py
new file mode 100644
index 000000000..f030c5e16
--- /dev/null
+++ b/samples/sentiment-strategy/test_sentiment_strategy.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+# -*- coding: utf-8; py-indent-offset:4 -*-
+###############################################################################
+#
+# Test script for Sentiment-based trading strategy
+#
+###############################################################################
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+import argparse
+import os
+import sys
+
+import backtrader as bt
+
+
+def runstrat(args=None):
+ args = parse_args(args)
+
+ cerebro = bt.Cerebro()
+
+ cerebro.broker.setcash(100000.0)
+ cerebro.broker.setcommission(commission=0.001)
+
+ datapath = args.data
+ if not os.path.isabs(datapath):
+ datapath = os.path.join(os.path.dirname(__file__), '..', '..', 'datas', datapath)
+
+ data = bt.feeds.SentimentCSVData(
+ dataname=datapath,
+ dtformat='%Y-%m-%d',
+ datetime=0,
+ open=1,
+ high=2,
+ low=3,
+ close=4,
+ volume=5,
+ openinterest=6,
+ sentiment=7,
+ )
+
+ cerebro.adddata(data)
+
+ cerebro.addstrategy(bt.strategies.SentimentStrategy,
+ buy_threshold=args.buy_threshold,
+ sell_threshold=args.sell_threshold,
+ prdata=args.prdata,
+ prtrade=args.prtrade)
+
+ cerebro.addsizer(bt.sizers.FixedSize, stake=10)
+
+ print('=' * 60)
+ print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
+ print('=' * 60)
+
+ results = cerebro.run()
+
+ print('=' * 60)
+ print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
+ print('=' * 60)
+
+ if args.plot:
+ cerebro.plot()
+
+
+def parse_args(pargs=None):
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ description='Sentiment Strategy Test')
+
+ parser.add_argument('--data', required=False,
+ default='sentiment-test-data.csv',
+ metavar='CSV_FILE',
+ help='CSV data file with sentiment column')
+
+ parser.add_argument('--buy-threshold', required=False, type=float,
+ default=0.8,
+ help='Buy when sentiment > this threshold')
+
+ parser.add_argument('--sell-threshold', required=False, type=float,
+ default=-0.5,
+ help='Sell when sentiment < this threshold')
+
+ parser.add_argument('--prdata', required=False, action='store_true',
+ default=True,
+ help='Print data bars')
+
+ parser.add_argument('--prtrade', required=False, action='store_true',
+ default=True,
+ help='Print trade information')
+
+ parser.add_argument('--plot', required=False, action='store_true',
+ default=False,
+ help='Plot the results')
+
+ return parser.parse_args(pargs)
+
+
+if __name__ == '__main__':
+ runstrat()
From 18a76b98d662e5af41d5708b398f6f4d2afc0902 Mon Sep 17 00:00:00 2001
From: ouyu <1986834078@qq.com>
Date: Sat, 18 Apr 2026 10:55:51 +0800
Subject: [PATCH 2/2] =?UTF-8?q?feat(=E7=AD=96=E7=95=A5):=20=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E5=9F=BA=E4=BA=8E=E6=83=85=E7=BB=AA=E5=88=86=E6=9E=90?=
=?UTF-8?q?=E7=9A=84=E5=8A=A8=E6=80=81=E4=BB=93=E4=BD=8D=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增SentimentSizer用于根据平滑情绪值动态调整仓位大小
修改SentimentStrategy添加SMA平滑情绪指标
更新测试数据以包含更多样本
扩展测试脚本支持动态仓位配置
---
backtrader/sizers/__init__.py | 1 +
backtrader/sizers/sentiment_sizer.py | 93 +++++++++++++++++++
backtrader/strategies/sentiment_strategy.py | 62 ++++++++++---
datas/sentiment-test-data.csv | 46 +++++----
.../test_sentiment_strategy.py | 72 ++++++++++++--
5 files changed, 233 insertions(+), 41 deletions(-)
create mode 100644 backtrader/sizers/sentiment_sizer.py
diff --git a/backtrader/sizers/__init__.py b/backtrader/sizers/__init__.py
index b79ab6cb0..3da3d399a 100644
--- a/backtrader/sizers/__init__.py
+++ b/backtrader/sizers/__init__.py
@@ -26,3 +26,4 @@
from .fixedsize import *
from .percents_sizer import *
+from .sentiment_sizer import SentimentSizer
diff --git a/backtrader/sizers/sentiment_sizer.py b/backtrader/sizers/sentiment_sizer.py
new file mode 100644
index 000000000..529b4ba0f
--- /dev/null
+++ b/backtrader/sizers/sentiment_sizer.py
@@ -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 .
+#
+###############################################################################
+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
diff --git a/backtrader/strategies/sentiment_strategy.py b/backtrader/strategies/sentiment_strategy.py
index a3648471c..91502f978 100644
--- a/backtrader/strategies/sentiment_strategy.py
+++ b/backtrader/strategies/sentiment_strategy.py
@@ -26,44 +26,76 @@
class SentimentStrategy(bt.Strategy):
'''
- Sentiment-based trading 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
- - Today's sentiment > 0.8
+ - Smoothed sentiment > buy_threshold
Sell Logic:
- A position exists
- - Today's sentiment < -0.5
+ - Smoothed sentiment < sell_threshold
'''
params = (
- ('buy_threshold', 0.8),
- ('sell_threshold', -0.5),
+ ('buy_threshold', 0.5),
+ ('sell_threshold', -0.3),
+ ('sma_period', 5),
+ ('use_smoothed', True),
('prdata', True),
('prtrade', True),
)
def __init__(self):
- self.sentiment = self.data.sentiment
+ 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_sentiment = self.data.sentiment[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:
- print(f'DATE: {current_date}, CLOSE: {current_close:.2f}, SENTIMENT: {current_sentiment:.2f}')
+ 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_sentiment > self.p.buy_threshold:
+ if current_smoothed > self.p.buy_threshold:
if self.p.prdata:
- print(f' >>> BUY SIGNAL: sentiment ({current_sentiment:.2f}) > threshold ({self.p.buy_threshold})')
+ print(f' >>> BUY SIGNAL: smoothed sentiment ({current_smoothed:.2f}) > threshold ({self.p.buy_threshold})')
self.buy()
else:
- if current_sentiment < self.p.sell_threshold:
+ if current_smoothed < self.p.sell_threshold:
if self.p.prdata:
- print(f' <<< SELL SIGNAL: sentiment ({current_sentiment:.2f}) < threshold ({self.p.sell_threshold})')
+ print(f' <<< SELL SIGNAL: smoothed sentiment ({current_smoothed:.2f}) < threshold ({self.p.sell_threshold})')
self.sell()
def notify_order(self, order):
@@ -72,9 +104,11 @@ def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy():
- print(f' --- ORDER EXECUTED: BUY at {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')
+ 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 at {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}')
+ 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()}')
diff --git a/datas/sentiment-test-data.csv b/datas/sentiment-test-data.csv
index 0a7277f12..6f162ed86 100644
--- a/datas/sentiment-test-data.csv
+++ b/datas/sentiment-test-data.csv
@@ -1,20 +1,30 @@
Date,Open,High,Low,Close,Volume,OpenInterest,Sentiment
-2024-01-02,100.0,102.0,99.5,101.5,10000,0,0.3
-2024-01-03,101.5,103.0,101.0,102.5,12000,0,0.5
-2024-01-04,102.5,105.0,102.0,104.0,15000,0,0.9
-2024-01-05,104.0,106.0,103.5,105.5,18000,0,0.85
-2024-01-08,105.5,107.0,105.0,106.0,20000,0,0.6
-2024-01-09,106.0,107.5,105.5,107.0,15000,0,0.4
-2024-01-10,107.0,108.0,104.0,105.0,25000,0,-0.3
-2024-01-11,105.0,106.0,102.0,103.0,30000,0,-0.6
-2024-01-12,103.0,104.5,101.0,102.0,20000,0,-0.7
-2024-01-15,102.0,103.5,100.5,101.5,18000,0,-0.2
-2024-01-16,101.5,104.0,101.0,103.5,15000,0,0.7
-2024-01-17,103.5,106.0,103.0,105.0,16000,0,0.95
-2024-01-18,105.0,107.5,104.5,107.0,17000,0,0.88
+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.55
-2024-01-23,104.0,105.0,102.0,103.0,19000,0,-0.8
-2024-01-24,103.0,104.5,101.5,102.5,15000,0,-0.1
-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.82
+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
diff --git a/samples/sentiment-strategy/test_sentiment_strategy.py b/samples/sentiment-strategy/test_sentiment_strategy.py
index f030c5e16..042ba2c73 100644
--- a/samples/sentiment-strategy/test_sentiment_strategy.py
+++ b/samples/sentiment-strategy/test_sentiment_strategy.py
@@ -2,7 +2,8 @@
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
#
-# Test script for Sentiment-based trading strategy
+# Test script for Sentiment-based trading strategy with SMA smoothing
+# and dynamic position sizing
#
###############################################################################
from __future__ import (absolute_import, division, print_function,
@@ -45,19 +46,38 @@ def runstrat(args=None):
cerebro.addstrategy(bt.strategies.SentimentStrategy,
buy_threshold=args.buy_threshold,
sell_threshold=args.sell_threshold,
+ sma_period=args.sma_period,
+ use_smoothed=args.use_smoothed,
prdata=args.prdata,
prtrade=args.prtrade)
- cerebro.addsizer(bt.sizers.FixedSize, stake=10)
+ if args.use_dynamic_sizer:
+ print(f'Using SentimentSizer: extreme_threshold={args.extreme_threshold}, '
+ f'medium_threshold={args.medium_threshold}, '
+ f'large_stake={args.large_stake}, small_stake={args.small_stake}')
+ cerebro.addsizer(bt.sizers.SentimentSizer,
+ extreme_threshold=args.extreme_threshold,
+ medium_threshold=args.medium_threshold,
+ large_stake=args.large_stake,
+ small_stake=args.small_stake)
+ else:
+ print(f'Using FixedSize sizer: stake={args.fixed_stake}')
+ cerebro.addsizer(bt.sizers.FixedSize, stake=args.fixed_stake)
print('=' * 60)
- print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
+ print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')
+ print(f'SMA Period: {args.sma_period}')
+ print(f'Buy Threshold: {args.buy_threshold}')
+ print(f'Sell Threshold: {args.sell_threshold}')
+ print(f'Use Smoothed Sentiment: {args.use_smoothed}')
+ print(f'Use Dynamic Sizer: {args.use_dynamic_sizer}')
print('=' * 60)
results = cerebro.run()
print('=' * 60)
- print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
+ print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')
+ print(f'Portfolio Return: {(cerebro.broker.getvalue() - 100000.0):.2f}')
print('=' * 60)
if args.plot:
@@ -67,7 +87,7 @@ def runstrat(args=None):
def parse_args(pargs=None):
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
- description='Sentiment Strategy Test')
+ description='Sentiment Strategy Test with SMA Smoothing and Dynamic Position Sizing')
parser.add_argument('--data', required=False,
default='sentiment-test-data.csv',
@@ -75,12 +95,46 @@ def parse_args(pargs=None):
help='CSV data file with sentiment column')
parser.add_argument('--buy-threshold', required=False, type=float,
- default=0.8,
- help='Buy when sentiment > this threshold')
+ default=0.5,
+ help='Buy when smoothed sentiment > this threshold')
parser.add_argument('--sell-threshold', required=False, type=float,
- default=-0.5,
- help='Sell when sentiment < this threshold')
+ default=-0.3,
+ help='Sell when smoothed sentiment < this threshold')
+
+ parser.add_argument('--sma-period', required=False, type=int,
+ default=5,
+ help='Period for SMA smoothing of sentiment')
+
+ parser.add_argument('--no-smooth', required=False, action='store_false',
+ default=True,
+ dest='use_smoothed',
+ help='Disable SMA smoothing (use raw sentiment)')
+
+ parser.add_argument('--no-dynamic-sizer', required=False, action='store_false',
+ default=True,
+ dest='use_dynamic_sizer',
+ help='Disable dynamic position sizing (use fixed size)')
+
+ parser.add_argument('--fixed-stake', required=False, type=int,
+ default=10,
+ help='Fixed stake size when using FixedSize sizer')
+
+ parser.add_argument('--extreme-threshold', required=False, type=float,
+ default=0.6,
+ help='Threshold for extreme sentiment (large position)')
+
+ parser.add_argument('--medium-threshold', required=False, type=float,
+ default=0.3,
+ help='Threshold for medium sentiment (small position)')
+
+ parser.add_argument('--large-stake', required=False, type=int,
+ default=100,
+ help='Large stake size for extreme sentiment')
+
+ parser.add_argument('--small-stake', required=False, type=int,
+ default=10,
+ help='Small stake size for medium sentiment')
parser.add_argument('--prdata', required=False, action='store_true',
default=True,