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,