BackcastPro 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- BackcastPro/__init__.py +28 -0
- BackcastPro/_broker.py +430 -0
- BackcastPro/_stats.py +177 -0
- BackcastPro/api/__init__.py +4 -0
- BackcastPro/api/board.py +130 -0
- BackcastPro/api/chart.py +527 -0
- BackcastPro/api/db_manager.py +283 -0
- BackcastPro/api/db_stocks_board.py +428 -0
- BackcastPro/api/db_stocks_daily.py +507 -0
- BackcastPro/api/db_stocks_info.py +260 -0
- BackcastPro/api/lib/__init__.py +4 -0
- BackcastPro/api/lib/e_api.py +588 -0
- BackcastPro/api/lib/jquants.py +384 -0
- BackcastPro/api/lib/kabusap.py +222 -0
- BackcastPro/api/lib/stooq.py +409 -0
- BackcastPro/api/lib/util.py +38 -0
- BackcastPro/api/stocks_board.py +77 -0
- BackcastPro/api/stocks_info.py +88 -0
- BackcastPro/api/stocks_price.py +131 -0
- BackcastPro/backtest.py +594 -0
- BackcastPro/order.py +161 -0
- BackcastPro/position.py +60 -0
- BackcastPro/trade.py +227 -0
- backcastpro-0.3.4.dist-info/METADATA +112 -0
- backcastpro-0.3.4.dist-info/RECORD +26 -0
- backcastpro-0.3.4.dist-info/WHEEL +4 -0
BackcastPro/api/board.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import datetime
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import numpy as np
|
|
5
|
+
import plotly.graph_objects as go
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def board(code: str = "", date: datetime = None,
|
|
9
|
+
df: pd.DataFrame = None):
|
|
10
|
+
"""
|
|
11
|
+
銘柄コードを指定して板情報チャートを表示する
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
code: 銘柄コード(例: "6363")
|
|
15
|
+
|
|
16
|
+
Raises:
|
|
17
|
+
NameError: get_stock_board関数が存在しない場合
|
|
18
|
+
ValueError: データが空の場合、または必要なカラムが存在しない場合
|
|
19
|
+
"""
|
|
20
|
+
if df is None:
|
|
21
|
+
# 板情報データを取得
|
|
22
|
+
from .stocks_board import stocks_board
|
|
23
|
+
__sb__ = stocks_board()
|
|
24
|
+
df = __sb__.get_japanese_stock_board_data(code, date)
|
|
25
|
+
|
|
26
|
+
# データが空の場合のエラーハンドリング
|
|
27
|
+
if df.empty:
|
|
28
|
+
raise ValueError(f"銘柄コード '{code}' の板情報が取得できませんでした。")
|
|
29
|
+
|
|
30
|
+
return board_by_df(df)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def board_by_df(df: pd.DataFrame):
|
|
34
|
+
"""
|
|
35
|
+
板情報データを指定して板情報チャートを表示する(plotly使用)
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
df: 板情報データ(pandas DataFrame)
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
# 必要なカラムの存在確認
|
|
42
|
+
required_cols = ['Price', 'Qty', 'Type']
|
|
43
|
+
missing_cols = [col for col in required_cols if col not in df.columns]
|
|
44
|
+
if missing_cols:
|
|
45
|
+
raise ValueError(f"必要なカラムが見つかりません: {missing_cols}。利用可能なカラム: {list(df.columns)}")
|
|
46
|
+
|
|
47
|
+
# データの準備
|
|
48
|
+
df_filtered = df[df['Price'] > 0].copy()
|
|
49
|
+
df_filtered = df_filtered.sort_values('Price', ascending=False)
|
|
50
|
+
|
|
51
|
+
# データが空になった場合のエラーハンドリング
|
|
52
|
+
if df_filtered.empty:
|
|
53
|
+
raise ValueError(f"有効な板情報データがありませんでした。")
|
|
54
|
+
|
|
55
|
+
# 買い板(Bid)と売り板(Ask)のデータを分離
|
|
56
|
+
bid_data = df_filtered[df_filtered['Type'] == 'Bid']
|
|
57
|
+
ask_data = df_filtered[df_filtered['Type'] == 'Ask']
|
|
58
|
+
|
|
59
|
+
# 買い板または売り板のデータが存在しない場合のエラーハンドリング
|
|
60
|
+
if len(bid_data) == 0 and len(ask_data) == 0:
|
|
61
|
+
raise ValueError(f"買い板または売り板のデータが見つかりませんでした。")
|
|
62
|
+
|
|
63
|
+
# すべての価格を統合してユニークな価格リストを作成(価格順にソート)
|
|
64
|
+
all_prices = sorted(df_filtered['Price'].unique(), reverse=True)
|
|
65
|
+
|
|
66
|
+
# plotlyのFigureを作成
|
|
67
|
+
fig = go.Figure()
|
|
68
|
+
|
|
69
|
+
# 買い板のデータをプロット(右側に表示)
|
|
70
|
+
if len(bid_data) > 0:
|
|
71
|
+
# 価格でソート(昇順)
|
|
72
|
+
bid_data_sorted = bid_data.sort_values('Price')
|
|
73
|
+
fig.add_trace(
|
|
74
|
+
go.Bar(
|
|
75
|
+
y=[f"{price:,.0f}円" for price in bid_data_sorted['Price']],
|
|
76
|
+
x=bid_data_sorted['Qty'],
|
|
77
|
+
name='買い板',
|
|
78
|
+
orientation='h',
|
|
79
|
+
marker=dict(color='#2196F3', line=dict(color='#1976D2', width=1)),
|
|
80
|
+
text=[f"{qty:,.0f}" for qty in bid_data_sorted['Qty']],
|
|
81
|
+
textposition='outside',
|
|
82
|
+
hovertemplate='価格: %{y}<br>数量: %{x:,.0f}株<extra></extra>'
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# 売り板のデータをプロット(左側に表示、負の値で表示)
|
|
87
|
+
if len(ask_data) > 0:
|
|
88
|
+
# 価格でソート(昇順)
|
|
89
|
+
ask_data_sorted = ask_data.sort_values('Price')
|
|
90
|
+
fig.add_trace(
|
|
91
|
+
go.Bar(
|
|
92
|
+
y=[f"{price:,.0f}円" for price in ask_data_sorted['Price']],
|
|
93
|
+
x=-ask_data_sorted['Qty'],
|
|
94
|
+
name='売り板',
|
|
95
|
+
orientation='h',
|
|
96
|
+
marker=dict(color='#F44336', line=dict(color='#D32F2F', width=1)),
|
|
97
|
+
text=[f"{qty:,.0f}" for qty in ask_data_sorted['Qty']],
|
|
98
|
+
textposition='outside',
|
|
99
|
+
hovertemplate='価格: %{y}<br>数量: %{x:,.0f}株<extra></extra>'
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# 銘柄コードを取得(board関数から呼び出された場合のみ)
|
|
104
|
+
# board_by_df単体で呼ばれた場合はタイトルに銘柄コードを含めない
|
|
105
|
+
title_text = '板情報チャート'
|
|
106
|
+
|
|
107
|
+
# レイアウト設定
|
|
108
|
+
fig.update_layout(
|
|
109
|
+
title=dict(text=title_text, font=dict(size=14), x=0.5, xanchor='center'),
|
|
110
|
+
xaxis=dict(
|
|
111
|
+
title='数量(株)',
|
|
112
|
+
gridcolor='rgba(0,0,0,0.1)',
|
|
113
|
+
gridwidth=1,
|
|
114
|
+
zeroline=True,
|
|
115
|
+
zerolinecolor='black',
|
|
116
|
+
zerolinewidth=2
|
|
117
|
+
),
|
|
118
|
+
yaxis=dict(
|
|
119
|
+
title='価格(円)',
|
|
120
|
+
categoryorder='array',
|
|
121
|
+
categoryarray=[f"{price:,.0f}円" for price in all_prices]
|
|
122
|
+
),
|
|
123
|
+
barmode='overlay',
|
|
124
|
+
height=600,
|
|
125
|
+
hovermode='closest',
|
|
126
|
+
showlegend=True,
|
|
127
|
+
legend=dict(x=1, y=1, xanchor='right', yanchor='top')
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return fig
|
BackcastPro/api/chart.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Lightweight Charts ベースの株価チャートモジュール
|
|
4
|
+
|
|
5
|
+
anywidget を使用してリアルタイム更新可能な金融チャートを提供する。
|
|
6
|
+
Plotly から移行し、Canvas 差分更新によりパフォーマンスを大幅に改善。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
11
|
+
|
|
12
|
+
import anywidget
|
|
13
|
+
import traitlets
|
|
14
|
+
|
|
15
|
+
import datetime
|
|
16
|
+
|
|
17
|
+
import pandas as pd
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CandleBar(TypedDict):
|
|
25
|
+
"""ローソク足バーの型定義"""
|
|
26
|
+
|
|
27
|
+
time: int # UNIXタイムスタンプ(UTC)
|
|
28
|
+
open: float
|
|
29
|
+
high: float
|
|
30
|
+
low: float
|
|
31
|
+
close: float
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class VolumeBar(TypedDict):
|
|
35
|
+
"""出来高バーの型定義"""
|
|
36
|
+
|
|
37
|
+
time: int
|
|
38
|
+
value: float
|
|
39
|
+
color: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MarkerData(TypedDict):
|
|
43
|
+
"""マーカーの型定義"""
|
|
44
|
+
|
|
45
|
+
time: int
|
|
46
|
+
position: str # "aboveBar" or "belowBar"
|
|
47
|
+
color: str
|
|
48
|
+
shape: str # "arrowUp", "arrowDown", "circle", "square"
|
|
49
|
+
text: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def to_lwc_timestamp(idx, tz: str = "Asia/Tokyo") -> int:
|
|
53
|
+
"""
|
|
54
|
+
インデックスをLightweight Charts用UTCタイムスタンプに変換
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
idx: DatetimeIndex, Timestamp, or date string
|
|
58
|
+
tz: 元データのタイムゾーン(日本株はAsia/Tokyo)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
UTCベースのUNIXタイムスタンプ
|
|
62
|
+
"""
|
|
63
|
+
import pandas as pd
|
|
64
|
+
|
|
65
|
+
ts = pd.Timestamp(idx)
|
|
66
|
+
if ts.tzinfo is None:
|
|
67
|
+
ts = ts.tz_localize(tz)
|
|
68
|
+
return int(ts.tz_convert("UTC").timestamp())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def df_to_lwc_data(df: pd.DataFrame, tz: str = "Asia/Tokyo") -> list[dict]:
|
|
72
|
+
"""
|
|
73
|
+
DataFrameをLightweight Charts形式に変換
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
df: OHLC データを含むDataFrame(Open, High, Low, Close列が必要)
|
|
77
|
+
tz: 元データのタイムゾーン
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Lightweight Charts形式のローソク足データリスト
|
|
81
|
+
"""
|
|
82
|
+
if len(df) == 0:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
records = []
|
|
86
|
+
for idx, row in df.iterrows():
|
|
87
|
+
records.append(
|
|
88
|
+
{
|
|
89
|
+
"time": to_lwc_timestamp(idx, tz),
|
|
90
|
+
"open": float(row["Open"]),
|
|
91
|
+
"high": float(row["High"]),
|
|
92
|
+
"low": float(row["Low"]),
|
|
93
|
+
"close": float(row["Close"]),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
return records
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_last_bar(df: pd.DataFrame, tz: str = "Asia/Tokyo") -> dict:
|
|
100
|
+
"""
|
|
101
|
+
DataFrameの最後のバーを取得
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
df: OHLC データを含むDataFrame
|
|
105
|
+
tz: 元データのタイムゾーン
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
最後のバーデータ(空DataFrameの場合は空辞書)
|
|
109
|
+
"""
|
|
110
|
+
if len(df) == 0:
|
|
111
|
+
return {}
|
|
112
|
+
|
|
113
|
+
last_row = df.iloc[-1]
|
|
114
|
+
idx = df.index[-1]
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"time": to_lwc_timestamp(idx, tz),
|
|
118
|
+
"open": float(last_row["Open"]),
|
|
119
|
+
"high": float(last_row["High"]),
|
|
120
|
+
"low": float(last_row["Low"]),
|
|
121
|
+
"close": float(last_row["Close"]),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def df_to_lwc_volume(df: pd.DataFrame, tz: str = "Asia/Tokyo") -> list[dict]:
|
|
126
|
+
"""
|
|
127
|
+
DataFrameの出来高をLightweight Charts形式に変換
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
df: Volume列を含むDataFrame
|
|
131
|
+
tz: 元データのタイムゾーン
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Lightweight Charts形式の出来高データリスト
|
|
135
|
+
"""
|
|
136
|
+
if "Volume" not in df.columns:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
records = []
|
|
140
|
+
for idx, row in df.iterrows():
|
|
141
|
+
# 陽線/陰線で色を変える
|
|
142
|
+
is_up = row["Close"] >= row["Open"]
|
|
143
|
+
records.append({
|
|
144
|
+
"time": to_lwc_timestamp(idx, tz),
|
|
145
|
+
"value": float(row["Volume"]),
|
|
146
|
+
"color": "rgba(38, 166, 154, 0.5)" if is_up else "rgba(239, 83, 80, 0.5)",
|
|
147
|
+
})
|
|
148
|
+
return records
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class LightweightChartWidget(anywidget.AnyWidget):
|
|
152
|
+
"""
|
|
153
|
+
Lightweight Charts ローソク足チャートウィジェット
|
|
154
|
+
|
|
155
|
+
marimo の mo.ui.anywidget() でラップして使用する。
|
|
156
|
+
差分更新に対応し、高速なリアルタイム更新が可能。
|
|
157
|
+
|
|
158
|
+
Attributes:
|
|
159
|
+
data: 全ローソク足データ(初回設定用)
|
|
160
|
+
volume_data: 出来高データ
|
|
161
|
+
markers: 売買マーカー
|
|
162
|
+
last_bar: 最新バー(差分更新用)
|
|
163
|
+
options: チャートオプション(height, showVolumeなど)
|
|
164
|
+
|
|
165
|
+
Example:
|
|
166
|
+
widget = LightweightChartWidget()
|
|
167
|
+
widget.options = {"height": 500, "showVolume": True}
|
|
168
|
+
widget.data = df_to_lwc_data(df)
|
|
169
|
+
|
|
170
|
+
# 差分更新
|
|
171
|
+
widget.last_bar = get_last_bar(df)
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
_esm = """
|
|
175
|
+
// CDNフォールバック付きのインポート
|
|
176
|
+
let createChart;
|
|
177
|
+
|
|
178
|
+
async function loadLibrary() {
|
|
179
|
+
const CDN_URLS = [
|
|
180
|
+
'https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.mjs',
|
|
181
|
+
'https://cdn.jsdelivr.net/npm/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.mjs',
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
for (const url of CDN_URLS) {
|
|
185
|
+
try {
|
|
186
|
+
const mod = await import(url);
|
|
187
|
+
return mod.createChart;
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.warn(`Failed to load from ${url}:`, e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw new Error('All CDN sources failed');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// バーデータの検証
|
|
196
|
+
function isValidBar(bar) {
|
|
197
|
+
return bar &&
|
|
198
|
+
typeof bar.time === 'number' &&
|
|
199
|
+
typeof bar.open === 'number' &&
|
|
200
|
+
typeof bar.high === 'number' &&
|
|
201
|
+
typeof bar.low === 'number' &&
|
|
202
|
+
typeof bar.close === 'number';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function render({ model, el }) {
|
|
206
|
+
// ライブラリ読み込み
|
|
207
|
+
try {
|
|
208
|
+
createChart = await loadLibrary();
|
|
209
|
+
} catch (e) {
|
|
210
|
+
el.innerHTML = '<p style="color:#ef5350;padding:20px;">Chart library failed to load. Check network connection.</p>';
|
|
211
|
+
console.error(e);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// チャート作成
|
|
216
|
+
const options = model.get("options") || {};
|
|
217
|
+
const chart = createChart(el, {
|
|
218
|
+
width: el.clientWidth || 800,
|
|
219
|
+
height: options.height || 400,
|
|
220
|
+
layout: {
|
|
221
|
+
background: { color: options.backgroundColor || '#1e1e1e' },
|
|
222
|
+
textColor: options.textColor || '#d1d4dc',
|
|
223
|
+
},
|
|
224
|
+
grid: {
|
|
225
|
+
vertLines: { color: '#2B2B43' },
|
|
226
|
+
horzLines: { color: '#2B2B43' },
|
|
227
|
+
},
|
|
228
|
+
timeScale: {
|
|
229
|
+
timeVisible: true,
|
|
230
|
+
secondsVisible: false,
|
|
231
|
+
},
|
|
232
|
+
crosshair: {
|
|
233
|
+
mode: 1,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ローソク足シリーズ
|
|
238
|
+
const candleSeries = chart.addCandlestickSeries({
|
|
239
|
+
upColor: '#26a69a',
|
|
240
|
+
downColor: '#ef5350',
|
|
241
|
+
borderVisible: false,
|
|
242
|
+
wickUpColor: '#26a69a',
|
|
243
|
+
wickDownColor: '#ef5350',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// 出来高シリーズ(オプション)
|
|
247
|
+
let volumeSeries = null;
|
|
248
|
+
const showVolume = options.showVolume !== false;
|
|
249
|
+
if (showVolume) {
|
|
250
|
+
volumeSeries = chart.addHistogramSeries({
|
|
251
|
+
color: '#26a69a',
|
|
252
|
+
priceFormat: { type: 'volume' },
|
|
253
|
+
priceScaleId: 'volume',
|
|
254
|
+
});
|
|
255
|
+
chart.priceScale('volume').applyOptions({
|
|
256
|
+
scaleMargins: { top: 0.8, bottom: 0 },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 初期データ設定
|
|
261
|
+
const data = model.get("data") || [];
|
|
262
|
+
if (data.length > 0) {
|
|
263
|
+
candleSeries.setData(data);
|
|
264
|
+
chart.timeScale().fitContent();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 出来高データ設定
|
|
268
|
+
const volumeData = model.get("volume_data") || [];
|
|
269
|
+
if (volumeSeries && volumeData.length > 0) {
|
|
270
|
+
volumeSeries.setData(volumeData);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// マーカー設定
|
|
274
|
+
const markers = model.get("markers") || [];
|
|
275
|
+
if (markers.length > 0) {
|
|
276
|
+
candleSeries.setMarkers(markers);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// データ全体が変更された時
|
|
280
|
+
model.on("change:data", () => {
|
|
281
|
+
const newData = model.get("data") || [];
|
|
282
|
+
if (newData.length > 0) {
|
|
283
|
+
candleSeries.setData(newData);
|
|
284
|
+
chart.timeScale().fitContent();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// 出来高データ変更時
|
|
289
|
+
model.on("change:volume_data", () => {
|
|
290
|
+
if (!volumeSeries) return;
|
|
291
|
+
const newVolumeData = model.get("volume_data") || [];
|
|
292
|
+
if (newVolumeData.length > 0) {
|
|
293
|
+
volumeSeries.setData(newVolumeData);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// マーカー変更時
|
|
298
|
+
model.on("change:markers", () => {
|
|
299
|
+
const newMarkers = model.get("markers") || [];
|
|
300
|
+
candleSeries.setMarkers(newMarkers);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 最後のバーのみ更新(差分更新)
|
|
304
|
+
model.on("change:last_bar", () => {
|
|
305
|
+
const bar = model.get("last_bar");
|
|
306
|
+
if (isValidBar(bar)) {
|
|
307
|
+
candleSeries.update(bar);
|
|
308
|
+
} else if (bar && Object.keys(bar).length > 0) {
|
|
309
|
+
console.warn('Invalid bar format:', bar);
|
|
310
|
+
}
|
|
311
|
+
// 空オブジェクトの場合は無視(クリア時)
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// リサイズ対応
|
|
315
|
+
const resizeObserver = new ResizeObserver(entries => {
|
|
316
|
+
const { width } = entries[0].contentRect;
|
|
317
|
+
if (width > 0) {
|
|
318
|
+
chart.applyOptions({ width });
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
resizeObserver.observe(el);
|
|
322
|
+
|
|
323
|
+
// クリーンアップ
|
|
324
|
+
return () => {
|
|
325
|
+
resizeObserver.disconnect();
|
|
326
|
+
chart.remove();
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export default { render };
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
_css = """
|
|
334
|
+
:host {
|
|
335
|
+
display: block;
|
|
336
|
+
width: 100%;
|
|
337
|
+
}
|
|
338
|
+
"""
|
|
339
|
+
|
|
340
|
+
# 同期するトレイト
|
|
341
|
+
data = traitlets.List([]).tag(sync=True)
|
|
342
|
+
volume_data = traitlets.List([]).tag(sync=True)
|
|
343
|
+
markers = traitlets.List([]).tag(sync=True)
|
|
344
|
+
last_bar = traitlets.Dict({}).tag(sync=True)
|
|
345
|
+
options = traitlets.Dict({}).tag(sync=True)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _prepare_chart_df(df: pd.DataFrame) -> pd.DataFrame:
|
|
349
|
+
"""チャート表示用データを準備"""
|
|
350
|
+
df = df.copy()
|
|
351
|
+
|
|
352
|
+
# DatetimeIndexの場合はそのまま使用
|
|
353
|
+
if isinstance(df.index, pd.DatetimeIndex):
|
|
354
|
+
df.index.name = "Date"
|
|
355
|
+
elif "Date" in df.columns:
|
|
356
|
+
df["Date"] = pd.to_datetime(df["Date"])
|
|
357
|
+
df = df.set_index("Date")
|
|
358
|
+
elif "date" in df.columns:
|
|
359
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
360
|
+
df = df.set_index("date")
|
|
361
|
+
df.index.name = "Date"
|
|
362
|
+
else:
|
|
363
|
+
try:
|
|
364
|
+
df.index = pd.to_datetime(df.index)
|
|
365
|
+
df.index.name = "Date"
|
|
366
|
+
except (ValueError, TypeError):
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
# カラム名を大文字に統一
|
|
370
|
+
column_mapping = {
|
|
371
|
+
"open": "Open",
|
|
372
|
+
"high": "High",
|
|
373
|
+
"low": "Low",
|
|
374
|
+
"close": "Close",
|
|
375
|
+
"volume": "Volume",
|
|
376
|
+
}
|
|
377
|
+
for lower, upper in column_mapping.items():
|
|
378
|
+
if lower in df.columns and upper not in df.columns:
|
|
379
|
+
df.rename(columns={lower: upper}, inplace=True)
|
|
380
|
+
|
|
381
|
+
# 必要なカラムを抽出して数値変換
|
|
382
|
+
required_cols = ["Open", "High", "Low", "Close", "Volume"]
|
|
383
|
+
available_cols = [col for col in required_cols if col in df.columns]
|
|
384
|
+
df = df[available_cols].copy()
|
|
385
|
+
|
|
386
|
+
# 数値カラムを数値型に変換
|
|
387
|
+
for col in available_cols:
|
|
388
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
389
|
+
|
|
390
|
+
return df.dropna()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def trades_to_markers(
|
|
394
|
+
trades: list,
|
|
395
|
+
code: str = None,
|
|
396
|
+
show_tags: bool = True,
|
|
397
|
+
tz: str = "Asia/Tokyo",
|
|
398
|
+
) -> list[dict]:
|
|
399
|
+
"""
|
|
400
|
+
TradeオブジェクトをLightweight Chartsマーカー形式に変換
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
trades: Trade オブジェクトのリスト
|
|
404
|
+
code: 銘柄コード(フィルタリング用)
|
|
405
|
+
show_tags: 売買理由(tag)を表示するか
|
|
406
|
+
tz: 元データのタイムゾーン
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Lightweight Charts形式のマーカーリスト
|
|
410
|
+
"""
|
|
411
|
+
markers = []
|
|
412
|
+
|
|
413
|
+
for trade in trades:
|
|
414
|
+
# codeが指定されている場合はフィルタリング
|
|
415
|
+
if code is not None and hasattr(trade, "code") and trade.code != code:
|
|
416
|
+
continue
|
|
417
|
+
|
|
418
|
+
is_long = trade.size > 0
|
|
419
|
+
tag = getattr(trade, "tag", None)
|
|
420
|
+
|
|
421
|
+
# エントリーマーカー
|
|
422
|
+
entry_text = "BUY" if is_long else "SELL"
|
|
423
|
+
if show_tags and tag:
|
|
424
|
+
entry_text = f"{entry_text}: {tag}"
|
|
425
|
+
|
|
426
|
+
markers.append({
|
|
427
|
+
"time": to_lwc_timestamp(trade.entry_time, tz),
|
|
428
|
+
"position": "belowBar" if is_long else "aboveBar",
|
|
429
|
+
"color": "#26a69a" if is_long else "#ef5350",
|
|
430
|
+
"shape": "arrowUp" if is_long else "arrowDown",
|
|
431
|
+
"text": entry_text,
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
# イグジットマーカー(決済済みの場合)
|
|
435
|
+
exit_time = getattr(trade, "exit_time", None)
|
|
436
|
+
exit_price = getattr(trade, "exit_price", None)
|
|
437
|
+
if exit_time is not None and exit_price is not None:
|
|
438
|
+
pnl = (exit_price - trade.entry_price) * trade.size
|
|
439
|
+
markers.append({
|
|
440
|
+
"time": to_lwc_timestamp(exit_time, tz),
|
|
441
|
+
"position": "aboveBar" if is_long else "belowBar",
|
|
442
|
+
"color": "#2196F3",
|
|
443
|
+
"shape": "circle",
|
|
444
|
+
"text": f"EXIT ({pnl:+.0f})",
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
# 時間順にソート(Lightweight Chartsの要件)
|
|
448
|
+
markers.sort(key=lambda x: x["time"])
|
|
449
|
+
return markers
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def chart_by_df(
|
|
453
|
+
df: pd.DataFrame,
|
|
454
|
+
*,
|
|
455
|
+
trades: list = None,
|
|
456
|
+
height: int = 500,
|
|
457
|
+
show_tags: bool = True,
|
|
458
|
+
show_volume: bool = True,
|
|
459
|
+
title: str = None,
|
|
460
|
+
code: str = None,
|
|
461
|
+
tz: str = "Asia/Tokyo",
|
|
462
|
+
) -> LightweightChartWidget:
|
|
463
|
+
"""
|
|
464
|
+
株価データからLightweight Chartsチャートを作成
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
df: 株価データ(pandas DataFrame)
|
|
468
|
+
trades: 取引リスト(Trade オブジェクトのリスト)
|
|
469
|
+
height: チャートの高さ(ピクセル)
|
|
470
|
+
show_tags: 売買理由(tag)をチャートに表示するか
|
|
471
|
+
show_volume: 出来高を表示するか
|
|
472
|
+
title: チャートのタイトル(現在は未使用)
|
|
473
|
+
code: 銘柄コード(trades のフィルタリング用)
|
|
474
|
+
tz: タイムゾーン(デフォルト: Asia/Tokyo)
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
LightweightChartWidget: anywidget ベースのチャートウィジェット
|
|
478
|
+
"""
|
|
479
|
+
# データを整形
|
|
480
|
+
df = _prepare_chart_df(df)
|
|
481
|
+
|
|
482
|
+
# ウィジェット作成
|
|
483
|
+
widget = LightweightChartWidget()
|
|
484
|
+
widget.options = {
|
|
485
|
+
"height": height,
|
|
486
|
+
"showVolume": show_volume,
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# ローソク足データ設定
|
|
490
|
+
widget.data = df_to_lwc_data(df, tz)
|
|
491
|
+
|
|
492
|
+
# 出来高データ設定
|
|
493
|
+
if show_volume:
|
|
494
|
+
widget.volume_data = df_to_lwc_volume(df, tz)
|
|
495
|
+
|
|
496
|
+
# 売買マーカー設定
|
|
497
|
+
if trades:
|
|
498
|
+
widget.markers = trades_to_markers(trades, code, show_tags, tz)
|
|
499
|
+
|
|
500
|
+
return widget
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def chart(
|
|
504
|
+
code: str = "",
|
|
505
|
+
from_: datetime.datetime = None,
|
|
506
|
+
to: datetime.datetime = None,
|
|
507
|
+
df: pd.DataFrame = None,
|
|
508
|
+
):
|
|
509
|
+
"""
|
|
510
|
+
株価データを指定して株価チャートを表示する
|
|
511
|
+
|
|
512
|
+
Args:
|
|
513
|
+
code: 銘柄コード(例: "6723")
|
|
514
|
+
from_: 開始日(datetime, オプション)
|
|
515
|
+
to: 終了日(datetime, オプション)
|
|
516
|
+
df: 株価データ(pandas DataFrame)
|
|
517
|
+
"""
|
|
518
|
+
if df is None:
|
|
519
|
+
from .stocks_daily import stocks_price
|
|
520
|
+
|
|
521
|
+
__sp__ = stocks_price()
|
|
522
|
+
df = __sp__.get_japanese_stock_price_data(code, from_=from_, to=to)
|
|
523
|
+
|
|
524
|
+
if df.empty:
|
|
525
|
+
raise ValueError(f"銘柄コード '{code}' の株価が取得できませんでした。")
|
|
526
|
+
|
|
527
|
+
return chart_by_df(df)
|