diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e6198b --- /dev/null +++ b/.gitignore @@ -0,0 +1,180 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python + + +.idea +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eef6845 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 tbxark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0cedc7c --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY: run +run: + streamlit run main.py --server.runOnSave true \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b300e63 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# backtrader + diff --git a/main.py b/main.py new file mode 100644 index 0000000..642f060 --- /dev/null +++ b/main.py @@ -0,0 +1,146 @@ +import streamlit as st +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import numpy as np + +def calculate_rsi(data, window=14): + delta = data.diff() + gain = (delta.where(delta > 0, 0)).rolling(window=window).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean() + rs = gain / loss + return 100 - (100 / (1 + rs)) + +def backtest_strategy(df, strategy_function, initial_capital=10000): + position = 0 + capital = initial_capital + trades = [] + + for i in range(1, len(df)): + current_data = df.iloc[i] + prev_data = df.iloc[i-1] + + action, amount = strategy_function(current_data, prev_data, position, capital) + + if action == "买入" and amount > 0: + cost = amount * current_data['close'] + if cost <= capital: + capital -= cost + position += amount + trades.append({ + "时间": current_data['timestamp'], + "操作": "买入", + "数量": amount, + "价格": current_data['close'], + "资金": capital + }) + elif action == "卖出" and position > 0: + sell_amount = min(amount, position) + capital += sell_amount * current_data['close'] + position -= sell_amount + trades.append({ + "时间": current_data['timestamp'], + "操作": "卖出", + "数量": sell_amount, + "价格": current_data['close'], + "资金": capital + }) + + final_assets = capital + position * df.iloc[-1]['close'] + return trades, final_assets + +def rsi_strategy(current_data, prev_data, position, capital): + current_rsi = current_data['RSI'] + current_close = current_data['close'] + prev_close = prev_data['close'] + + if current_rsi > 80 and position > 0: + return "卖出", position + elif current_rsi < 20 and position == 0: + shares_to_buy = capital // current_close + return "买入", shares_to_buy + elif position > 0 and (current_close - prev_close) > 10: + return "卖出", position + elif position > 0 and (prev_close - current_close) > 5: + return "卖出", position + + return "持有", 0 + + +uploaded_file = st.file_uploader("选择CSV文件", type="csv") + +if uploaded_file is not None: + df = pd.read_csv(uploaded_file, sep=',') + print(df.head()) + df['timestamp'] = pd.to_datetime(df['timestamp']) + df = df.dropna() + df_resampled = df.set_index('timestamp').resample('15T').agg({'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}) + df_resampled = df_resampled.dropna() + df_resampled['RSI'] = calculate_rsi(df_resampled['close'], window=14) + df_resampled = df_resampled.dropna() + df_resampled = df_resampled.reset_index() + + + st.write("数据预览:") + st.write(df_resampled.head()) + + columns = df_resampled.columns.tolist() + + default_x = 'timestamp' if 'timestamp' in columns else columns[0] + default_y = 'close' if 'close' in columns else columns[0] + + x_axis = st.selectbox("选择X轴", options=columns, index=columns.index(default_x)) + y_axis = st.selectbox("选择Y轴", options=columns, index=columns.index(default_y)) + + + fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1, row_heights=[0.7, 0.3]) + fig.add_trace(go.Scatter(x=df_resampled[x_axis], y=df_resampled[y_axis], name=y_axis), row=1, col=1) + fig.add_trace(go.Scatter(x=df_resampled[x_axis], y=df_resampled['RSI'], name='RSI'), row=2, col=1) + + short_signals = df_resampled[df_resampled['RSI'] > 80] + fig.add_trace(go.Scatter( + x=short_signals[x_axis], + y=short_signals[y_axis], + mode='markers', + marker=dict(symbol='triangle-down', size=10, color='red'), + name='做空信号' + ), row=1, col=1) + + long_signals = df_resampled[df_resampled['RSI'] < 20] + fig.add_trace(go.Scatter( + x=long_signals[x_axis], + y=long_signals[y_axis], + mode='markers', + marker=dict(symbol='triangle-up', size=10, color='green'), + name='做多信号' + ), row=1, col=1) + + fig.update_layout(height=600, title_text=f'{y_axis} and RSI vs {x_axis}') + fig.update_yaxes(title_text=y_axis, row=1, col=1) + fig.update_yaxes(title_text='RSI', row=2, col=1) + + st.plotly_chart(fig) + + # 回测逻辑 + if 'close' in df_resampled.columns and 'RSI' in df_resampled.columns: + trades, final_assets = backtest_strategy(df_resampled, rsi_strategy) + + st.subheader("回测结果") + st.write("交易记录:") + + if trades: + df_trades = pd.DataFrame(trades) + df_trades['时间'] = pd.to_datetime(df_trades['时间']) + df_trades['时间'] = df_trades['时间'].dt.strftime('%Y-%m-%d %H:%M') + df_trades['价格'] = df_trades['价格'].round(2) + df_trades['资金'] = df_trades['资金'].round(2) + st.table(df_trades) + else: + st.write("没有执行任何交易。") + + st.write(f"最终资产: {final_assets:.2f}") + else: + st.error("数据中缺少 'close' 或 'RSI' 列,无法进行回测。") + +else: + st.write("请上传CSV文件以查看回测结果。")