改用marimo编辑notebook方便跨平台使用
This commit is contained in:
1
.idea/vcs.xml
generated
1
.idea/vcs.xml
generated
@@ -2,6 +2,5 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
<mapping directory="$PROJECT_DIR$/backtestingpy" vcs="Git" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
337
notebook/__marimo__/backtest.ipynb
Normal file
337
notebook/__marimo__/backtest.ipynb
Normal file
File diff suppressed because one or more lines are too long
222
notebook/__marimo__/indicator.ipynb
Normal file
222
notebook/__marimo__/indicator.ipynb
Normal file
File diff suppressed because one or more lines are too long
182
notebook/__marimo__/session/backtest.py.json
Normal file
182
notebook/__marimo__/session/backtest.py.json
Normal file
File diff suppressed because one or more lines are too long
111
notebook/__marimo__/session/indicator.py.json
Normal file
111
notebook/__marimo__/session/indicator.py.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
184
notebook/backtest.py
Normal file
184
notebook/backtest.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import marimo
|
||||||
|
|
||||||
|
__generated_with = "0.19.6"
|
||||||
|
app = marimo.App(width="full", auto_download=["ipynb"], sql_output="pandas")
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
import marimo as mo
|
||||||
|
return (mo,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
import urllib
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
host = "81.71.3.24"
|
||||||
|
port = 6785
|
||||||
|
username = "leopard"
|
||||||
|
password = urllib.parse.quote_plus("9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X")
|
||||||
|
database = "leopard_dev"
|
||||||
|
engine = sqlalchemy.create_engine(f"postgresql://{username}:{password}@{host}:{port}/{database}")
|
||||||
|
return (engine,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(engine, mo):
|
||||||
|
dailies_df = mo.sql(
|
||||||
|
f"""
|
||||||
|
select trade_date,
|
||||||
|
open * factor as Open,
|
||||||
|
close * factor as Close,
|
||||||
|
high * factor as High,
|
||||||
|
low * factor as Low,
|
||||||
|
volume as Volume,
|
||||||
|
coalesce(factor, 1.0) as factor
|
||||||
|
from leopard_daily daily
|
||||||
|
left join leopard_stock stock on stock.id = daily.stock_id
|
||||||
|
where stock.code = '000001.SZ'
|
||||||
|
and daily.trade_date between '2024-01-01 00:00:00' and '2025-12-31 23:59:59'
|
||||||
|
order by daily.trade_date
|
||||||
|
""",
|
||||||
|
engine=engine
|
||||||
|
)
|
||||||
|
return (dailies_df,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(dailies_df):
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
dailies_df.rename(
|
||||||
|
columns={'open': 'Open', 'close': 'Close', 'high': 'High', 'low': 'Low', 'volume': 'Volume'}, inplace=True
|
||||||
|
)
|
||||||
|
dailies_df['trade_date'] = pd.to_datetime(dailies_df['trade_date'], format='%Y-%m-%d')
|
||||||
|
dailies_df.set_index('trade_date', inplace=True)
|
||||||
|
dailies_df
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(dailies_df):
|
||||||
|
import talib
|
||||||
|
|
||||||
|
dailies_df['sma10'] = talib.SMA(dailies_df['Close'], timeperiod=10)
|
||||||
|
dailies_df['sma30'] = talib.SMA(dailies_df['Close'], timeperiod=30)
|
||||||
|
dailies_df['sma60'] = talib.SMA(dailies_df['Close'], timeperiod=60)
|
||||||
|
dailies_df['sma120'] = talib.SMA(dailies_df['Close'], timeperiod=120)
|
||||||
|
dailies_df
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(dailies_df):
|
||||||
|
# 指标计算完成后截取后面指标的完整的部份使用
|
||||||
|
target_dailies_df = dailies_df.loc['2025-01-01':'2025-12-31']
|
||||||
|
target_dailies_df
|
||||||
|
return (target_dailies_df,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
from backtesting import Strategy
|
||||||
|
from backtesting.lib import crossover
|
||||||
|
|
||||||
|
class SmaCross(Strategy):
|
||||||
|
def init(self):
|
||||||
|
self.sma10 = self.I(lambda x: x, self.data.sma10)
|
||||||
|
self.sma30 = self.I(lambda x: x, self.data.sma30)
|
||||||
|
self.sma60 = self.I(lambda x: x, self.data.sma60)
|
||||||
|
# self.sma120 = self.I(lambda x: x, self.data.sma120)
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
if self.sma60 > 0 and crossover(self.data.sma10, self.data.sma30):
|
||||||
|
self.buy()
|
||||||
|
elif self.position.size > 0 and crossover(self.data.sma30, self.data.sma10):
|
||||||
|
self.position.close()
|
||||||
|
return (SmaCross,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.function
|
||||||
|
def stats_print(stats):
|
||||||
|
indicator_name_mapping = {
|
||||||
|
# 'Start': '回测开始时间',
|
||||||
|
# 'End': '回测结束时间',
|
||||||
|
# 'Duration': '回测持续时长',
|
||||||
|
# 'Exposure Time [%]': '持仓时间占比(%)',
|
||||||
|
'Equity Final [$]': '最终收益',
|
||||||
|
'Equity Peak [$]': '峰值收益',
|
||||||
|
'Return [%]': '总收益率(%)',
|
||||||
|
'Buy & Hold Return [%]': '买入并持有收益率(%)',
|
||||||
|
'Return (Ann.) [%]': '年化收益率(%)',
|
||||||
|
'Volatility (Ann.) [%]': '年化波动率(%)',
|
||||||
|
# 'CAGR [%]': '复合年均增长率(%)',
|
||||||
|
# 'Sharpe Ratio': '夏普比率',
|
||||||
|
'Sortino Ratio': '索提诺比率',
|
||||||
|
'Calmar Ratio': '卡尔玛比率',
|
||||||
|
# 'Alpha [%]': '阿尔法系数(%)',
|
||||||
|
# 'Beta': '贝塔系数',
|
||||||
|
'Max. Drawdown [%]': '最大回撤(%)',
|
||||||
|
'Avg. Drawdown [%]': '平均回撤(%)',
|
||||||
|
'Max. Drawdown Duration': '最大回撤持续时长',
|
||||||
|
'Avg. Drawdown Duration': '平均回撤持续时长',
|
||||||
|
'# Trades': '总交易次数',
|
||||||
|
'Win Rate [%]': '胜率(%)',
|
||||||
|
# 'Best Trade [%]': '最佳单笔交易收益率(%)',
|
||||||
|
# 'Worst Trade [%]': '最差单笔交易收益率(%)',
|
||||||
|
# 'Avg. Trade [%]': '平均单笔交易收益率(%)',
|
||||||
|
# 'Max. Trade Duration': '单笔交易最长持有时长',
|
||||||
|
# 'Avg. Trade Duration': '单笔交易平均持有时长',
|
||||||
|
# 'Profit Factor': '盈利因子',
|
||||||
|
# 'Expectancy [%]': '期望收益(%)',
|
||||||
|
'SQN': '系统质量数',
|
||||||
|
# 'Kelly Criterion': '凯利准则',
|
||||||
|
}
|
||||||
|
for k, v in stats.items():
|
||||||
|
if k in indicator_name_mapping:
|
||||||
|
cn_name = indicator_name_mapping.get(k, k)
|
||||||
|
if isinstance(v, (int, float)):
|
||||||
|
if "%" in cn_name or k in ['Sharpe Ratio', 'Sortino Ratio', 'Calmar Ratio', 'Profit Factor']:
|
||||||
|
formatted_value = f"{v:.2f}"
|
||||||
|
elif "$" in cn_name:
|
||||||
|
formatted_value = f"{v:.2f}"
|
||||||
|
elif "次数" in cn_name:
|
||||||
|
formatted_value = f"{v:.0f}"
|
||||||
|
else:
|
||||||
|
formatted_value = f"{v:.4f}"
|
||||||
|
else:
|
||||||
|
formatted_value = str(v)
|
||||||
|
print(f'{cn_name}: {formatted_value}')
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(SmaCross, target_dailies_df):
|
||||||
|
from backtesting import Backtest
|
||||||
|
|
||||||
|
import backtesting._plotting as plotting
|
||||||
|
from bokeh.colors.named import tomato, lime
|
||||||
|
|
||||||
|
plotting.BULL_COLOR = tomato
|
||||||
|
plotting.BEAR_COLOR = lime
|
||||||
|
|
||||||
|
bt = Backtest(target_dailies_df, SmaCross, cash=100000, commission=.002, finalize_trades=True)
|
||||||
|
stats = bt.run()
|
||||||
|
stats_print(stats)
|
||||||
|
return bt, stats
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(stats):
|
||||||
|
stats._trades
|
||||||
|
# stats._equity_curve
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(bt):
|
||||||
|
bt.plot()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
File diff suppressed because one or more lines are too long
131
notebook/indicator.py
Normal file
131
notebook/indicator.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import marimo
|
||||||
|
|
||||||
|
__generated_with = "0.19.6"
|
||||||
|
app = marimo.App(width="full", auto_download=["ipynb"], sql_output="pandas")
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
import marimo as mo
|
||||||
|
return (mo,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _():
|
||||||
|
import urllib
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
host = "81.71.3.24"
|
||||||
|
port = 6785
|
||||||
|
username = "leopard"
|
||||||
|
password = urllib.parse.quote_plus("9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X")
|
||||||
|
database = "leopard_dev"
|
||||||
|
engine = sqlalchemy.create_engine(f"postgresql://{username}:{password}@{host}:{port}/{database}")
|
||||||
|
return (engine,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(engine, mo):
|
||||||
|
dailies_df = mo.sql(
|
||||||
|
f"""
|
||||||
|
select trade_date, open * factor as "Open", close * factor as "Close", high * factor as "High", low * factor as "Low", volume as "Volume", coalesce(factor, 1.0) as factor
|
||||||
|
from leopard_daily daily
|
||||||
|
left join leopard_stock stock on stock.id = daily.stock_id
|
||||||
|
where stock.code = '000001.SZ'
|
||||||
|
and daily.trade_date between '2024-01-01 00:00:00' and '2025-12-31 23:59:59'
|
||||||
|
order by daily.trade_date
|
||||||
|
""",
|
||||||
|
engine=engine
|
||||||
|
)
|
||||||
|
return (dailies_df,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(dailies_df):
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
dailies_df["trade_date"] = pd.to_datetime(dailies_df["trade_date"], format='%Y-%m-%d')
|
||||||
|
dailies_df.set_index("trade_date", inplace=True)
|
||||||
|
dailies_df
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(dailies_df):
|
||||||
|
import talib
|
||||||
|
|
||||||
|
dailies_df['sma30'] = talib.SMA(dailies_df['Close'], timeperiod=30)
|
||||||
|
dailies_df['sma60'] = talib.SMA(dailies_df['Close'], timeperiod=60)
|
||||||
|
dailies_df['sma120'] = talib.SMA(dailies_df['Close'], timeperiod=120)
|
||||||
|
|
||||||
|
macd, signal, hist = talib.MACD(dailies_df["Close"], fastperiod=10, slowperiod=20, signalperiod=9)
|
||||||
|
dailies_df['macd'] = macd
|
||||||
|
dailies_df['signal'] = signal
|
||||||
|
dailies_df['hist'] = hist
|
||||||
|
|
||||||
|
target_dailies_df = dailies_df.loc['2025-01-01':'2025-12-31']
|
||||||
|
target_dailies_df
|
||||||
|
return (target_dailies_df,)
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def _(target_dailies_df):
|
||||||
|
target_dailies_df.reset_index(inplace=True)
|
||||||
|
target_dailies_df_inc = target_dailies_df[target_dailies_df["Close"] > target_dailies_df["Open"]]
|
||||||
|
target_dailies_df_dec = target_dailies_df[target_dailies_df["Close"] < target_dailies_df["Open"]]
|
||||||
|
return target_dailies_df_dec, target_dailies_df_inc
|
||||||
|
|
||||||
|
|
||||||
|
@app.cell
|
||||||
|
def daily_chart(
|
||||||
|
target_dailies_df,
|
||||||
|
target_dailies_df_dec,
|
||||||
|
target_dailies_df_inc,
|
||||||
|
):
|
||||||
|
from bokeh.io import show, output_notebook
|
||||||
|
from bokeh.layouts import column
|
||||||
|
from bokeh.plotting import figure
|
||||||
|
|
||||||
|
output_notebook()
|
||||||
|
|
||||||
|
width = 1200
|
||||||
|
up_color = "black"
|
||||||
|
down_color = "grey"
|
||||||
|
|
||||||
|
price_figure = figure(width=width, height=400, tools='pan,wheel_zoom,box_zoom,reset')
|
||||||
|
|
||||||
|
price_figure.min_border = 0
|
||||||
|
|
||||||
|
x_padding = 1
|
||||||
|
y_padding = 10
|
||||||
|
price_figure.x_range.bounds = (min(target_dailies_df.index) - x_padding, max(target_dailies_df.index) + x_padding)
|
||||||
|
price_figure.y_range.bounds = (min(target_dailies_df['Low']) - y_padding, max(target_dailies_df['High']) + y_padding)
|
||||||
|
price_figure.xaxis.major_label_overrides = {i: date.strftime('%Y-%m-%d') for i, date in zip(target_dailies_df.index, target_dailies_df['trade_date'])}
|
||||||
|
price_figure.segment(target_dailies_df.index, target_dailies_df['High'], target_dailies_df.index, target_dailies_df['Low'], color='black')
|
||||||
|
price_figure.vbar(target_dailies_df_inc.index, 0.6, target_dailies_df_inc['Open'], target_dailies_df_inc['Close'], color=up_color)
|
||||||
|
price_figure.vbar(target_dailies_df_dec.index, 0.6, target_dailies_df_dec['Open'], target_dailies_df_dec['Close'], color=down_color)
|
||||||
|
price_figure.line(target_dailies_df.index, target_dailies_df['sma30'], color='orange')
|
||||||
|
price_figure.line(target_dailies_df.index, target_dailies_df['sma60'], color='red')
|
||||||
|
# 控制图表只能放大不能缩小
|
||||||
|
macd_figure = figure(width=width, height=200, tools='pan,wheel_zoom,box_zoom,reset')
|
||||||
|
macd_figure.x_range.bounds = (min(target_dailies_df.index) - x_padding, max(target_dailies_df.index) + x_padding)
|
||||||
|
macd_figure.y_range.bounds = (min(target_dailies_df['macd']) - y_padding, max(target_dailies_df['macd']) + y_padding)
|
||||||
|
macd_figure.line(target_dailies_df.index, target_dailies_df['macd'], color='orange')
|
||||||
|
macd_figure.line(target_dailies_df.index, target_dailies_df['signal'], color='red')
|
||||||
|
# Add MACD histogram bars for positive and negative values
|
||||||
|
macd_positive = target_dailies_df[target_dailies_df['macd'] > 0]
|
||||||
|
macd_negative = target_dailies_df[target_dailies_df['macd'] < 0]
|
||||||
|
|
||||||
|
macd_figure.vbar(macd_positive.index, 0.6, 0, macd_positive['macd'], color=up_color)
|
||||||
|
macd_figure.vbar(macd_negative.index, 0.6, 0, macd_negative['macd'], color=down_color)
|
||||||
|
|
||||||
|
# Add zero line
|
||||||
|
macd_figure.line(target_dailies_df.index, [0] * len(target_dailies_df), color='black', line_dash='dashed')
|
||||||
|
# show(price_figure)
|
||||||
|
# show(macd_figure)
|
||||||
|
show(column(price_figure, macd_figure))
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
{
|
|
||||||
"cells": [
|
|
||||||
{
|
|
||||||
"metadata": {},
|
|
||||||
"cell_type": "code",
|
|
||||||
"outputs": [],
|
|
||||||
"execution_count": null,
|
|
||||||
"source": [
|
|
||||||
"import pandas as pd\n",
|
|
||||||
"\n",
|
|
||||||
"host = '81.71.3.24'\n",
|
|
||||||
"port = 6785\n",
|
|
||||||
"database = 'leopard_dev'\n",
|
|
||||||
"username = 'leopard'\n",
|
|
||||||
"password = '9NEzFzovnddf@PyEP?e*AYAWnCyd7UhYwQK$pJf>7?ccFiN^x4$eKEZ5~E<7<+~X'"
|
|
||||||
],
|
|
||||||
"id": "1e8d815ee9b8c936"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cell_type": "code",
|
|
||||||
"execution_count": null,
|
|
||||||
"id": "initial_id",
|
|
||||||
"metadata": {
|
|
||||||
"collapsed": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
|
||||||
"source": [
|
|
||||||
"import psycopg2\n",
|
|
||||||
"\n",
|
|
||||||
"dailies_df = pd.DataFrame()\n",
|
|
||||||
"\n",
|
|
||||||
"with psycopg2.connect(host=host, port=port, database=database, user=username, password=password) as connection:\n",
|
|
||||||
" with connection.cursor() as cursor:\n",
|
|
||||||
" # language=PostgreSQL\n",
|
|
||||||
" cursor.execute(\n",
|
|
||||||
" \"\"\"select trade_date, open, close, high, low, factor\n",
|
|
||||||
"from leopard_daily daily\n",
|
|
||||||
" left join leopard_stock stock on stock.id = daily.stock_id\n",
|
|
||||||
"where stock.code = '000001.SZ'\n",
|
|
||||||
" and daily.trade_date between '2025-01-01 00:00:00' and '2025-12-31 23:59:59'\n",
|
|
||||||
"order by daily.trade_date\"\"\"\n",
|
|
||||||
" )\n",
|
|
||||||
" rows = cursor.fetchall()\n",
|
|
||||||
"\n",
|
|
||||||
" dailies_df = pd.DataFrame.from_records(rows, columns=['trade_date', 'open', 'close', 'high', 'low', 'factor'])\n",
|
|
||||||
"\n",
|
|
||||||
"dailies_df"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"metadata": {
|
|
||||||
"kernelspec": {
|
|
||||||
"display_name": "Python 3",
|
|
||||||
"language": "python",
|
|
||||||
"name": "python3"
|
|
||||||
},
|
|
||||||
"language_info": {
|
|
||||||
"codemirror_mode": {
|
|
||||||
"name": "ipython",
|
|
||||||
"version": 2
|
|
||||||
},
|
|
||||||
"file_extension": ".py",
|
|
||||||
"mimetype": "text/x-python",
|
|
||||||
"name": "python",
|
|
||||||
"nbconvert_exporter": "python",
|
|
||||||
"pygments_lexer": "ipython2",
|
|
||||||
"version": "2.7.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nbformat": 4,
|
|
||||||
"nbformat_minor": 5
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ dependencies = [
|
|||||||
"duckdb>=1.4.3",
|
"duckdb>=1.4.3",
|
||||||
"jupyter~=1.1.1",
|
"jupyter~=1.1.1",
|
||||||
"jupyter-bokeh>=4.0.5",
|
"jupyter-bokeh>=4.0.5",
|
||||||
|
"marimo>=0.19.6",
|
||||||
"matplotlib~=3.10.8",
|
"matplotlib~=3.10.8",
|
||||||
"mplfinance>=0.12.10b0",
|
"mplfinance>=0.12.10b0",
|
||||||
"pandas~=2.3.3",
|
"pandas~=2.3.3",
|
||||||
@@ -19,3 +20,12 @@ dependencies = [
|
|||||||
"tabulate>=0.9.0",
|
"tabulate>=0.9.0",
|
||||||
"tqdm>=4.67.1",
|
"tqdm>=4.67.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"mcp>=1",
|
||||||
|
"openai>=2.16.0",
|
||||||
|
"pydantic>=2",
|
||||||
|
"pydantic-ai>=1.48.0",
|
||||||
|
"ruff>=0.14.14",
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user