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.
@@ -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
@@ -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)