deltafq 0.4.0__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.
Files changed (42) hide show
  1. deltafq/__init__.py +29 -0
  2. deltafq/backtest/__init__.py +32 -0
  3. deltafq/backtest/engine.py +145 -0
  4. deltafq/backtest/metrics.py +74 -0
  5. deltafq/backtest/performance.py +350 -0
  6. deltafq/charts/__init__.py +14 -0
  7. deltafq/charts/performance.py +319 -0
  8. deltafq/charts/price.py +64 -0
  9. deltafq/charts/signals.py +181 -0
  10. deltafq/core/__init__.py +18 -0
  11. deltafq/core/base.py +21 -0
  12. deltafq/core/config.py +62 -0
  13. deltafq/core/exceptions.py +34 -0
  14. deltafq/core/logger.py +44 -0
  15. deltafq/data/__init__.py +16 -0
  16. deltafq/data/cleaner.py +39 -0
  17. deltafq/data/fetcher.py +58 -0
  18. deltafq/data/storage.py +264 -0
  19. deltafq/data/validator.py +51 -0
  20. deltafq/indicators/__init__.py +14 -0
  21. deltafq/indicators/fundamental.py +28 -0
  22. deltafq/indicators/talib_indicators.py +67 -0
  23. deltafq/indicators/technical.py +251 -0
  24. deltafq/live/__init__.py +16 -0
  25. deltafq/live/connection.py +235 -0
  26. deltafq/live/data_feed.py +158 -0
  27. deltafq/live/monitoring.py +191 -0
  28. deltafq/live/risk_control.py +192 -0
  29. deltafq/strategy/__init__.py +12 -0
  30. deltafq/strategy/base.py +34 -0
  31. deltafq/strategy/signals.py +193 -0
  32. deltafq/trader/__init__.py +16 -0
  33. deltafq/trader/broker.py +119 -0
  34. deltafq/trader/engine.py +174 -0
  35. deltafq/trader/order_manager.py +110 -0
  36. deltafq/trader/position_manager.py +92 -0
  37. deltafq-0.4.0.dist-info/METADATA +115 -0
  38. deltafq-0.4.0.dist-info/RECORD +42 -0
  39. deltafq-0.4.0.dist-info/WHEEL +5 -0
  40. deltafq-0.4.0.dist-info/entry_points.txt +2 -0
  41. deltafq-0.4.0.dist-info/licenses/LICENSE +21 -0
  42. deltafq-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,319 @@
1
+ """Performance visualization utilities."""
2
+
3
+ from typing import Optional
4
+
5
+ import matplotlib.dates as mdates
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from ..core.base import BaseComponent
11
+
12
+ try: # pragma: no cover - optional dependency
13
+ from scipy.stats import gaussian_kde
14
+ except ImportError: # pragma: no cover
15
+ gaussian_kde = None
16
+
17
+
18
+ class PerformanceChart(BaseComponent):
19
+ """Visualize backtest performance with stacked panels."""
20
+
21
+ def initialize(self) -> bool:
22
+ self.logger.info("Initializing performance chart")
23
+ return True
24
+
25
+ def plot_backtest_charts(
26
+ self,
27
+ values_df: pd.DataFrame,
28
+ benchmark_close: Optional[pd.Series] = None,
29
+ title: Optional[str] = None,
30
+ save_path: Optional[str] = None,
31
+ use_plotly: bool = False,
32
+ ) -> None:
33
+ plt.rcParams["font.sans-serif"] = [
34
+ "Microsoft YaHei",
35
+ "SimHei",
36
+ "Heiti TC",
37
+ "Arial Unicode MS",
38
+ "DejaVu Sans",
39
+ ]
40
+ plt.rcParams["axes.unicode_minus"] = False
41
+
42
+ df = values_df.copy()
43
+ if "date" in df.columns:
44
+ df["date"] = pd.to_datetime(df["date"])
45
+ df = df.set_index("date")
46
+ df.index = pd.to_datetime(df.index)
47
+
48
+ if "total_value" not in df.columns:
49
+ raise KeyError("values_df must contain 'total_value'")
50
+
51
+ if "returns" not in df.columns:
52
+ df["returns"] = df["total_value"].pct_change().fillna(0.0)
53
+
54
+ has_price = "price" in df.columns
55
+ if not df.empty:
56
+ date_text = f"{df.index.min().date()} — {df.index.max().date()}"
57
+ else:
58
+ date_text = "no data"
59
+
60
+ # Pre-compute series used by both matplotlib and plotly paths
61
+ strategy_nv = df["total_value"] / df["total_value"].iloc[0]
62
+ drawdown = (df["total_value"].expanding().max() - df["total_value"]) / df["total_value"].expanding().max() * -100
63
+ returns_pct = df["returns"] * 100
64
+ price_norm = df["price"].astype(float) / df["price"].iloc[0] if has_price else None
65
+ bench_norm_price = None
66
+ bench_norm_nv = None
67
+ if benchmark_close is not None:
68
+ bench_series = pd.Series(benchmark_close).astype(float)
69
+ bench_series.index = pd.to_datetime(bench_series.index)
70
+ bench_series = bench_series.sort_index().reindex(df.index).fillna(method="ffill").dropna()
71
+ if not bench_series.empty:
72
+ bench_returns = bench_series.pct_change().fillna(0.0)
73
+ bench_nv = (1 + bench_returns).cumprod()
74
+ bench_norm_nv = bench_nv / bench_nv.iloc[0]
75
+ if has_price:
76
+ bench_norm_price = bench_series / bench_series.iloc[0]
77
+
78
+ if use_plotly:
79
+ try:
80
+ import plotly.graph_objects as go
81
+ from plotly.subplots import make_subplots
82
+ except ImportError as exc: # pragma: no cover
83
+ self.logger.warning("Plotly not available (%s); falling back to Matplotlib", exc)
84
+ else:
85
+ rows = 5
86
+ fig = make_subplots(
87
+ rows=rows,
88
+ cols=1,
89
+ shared_xaxes=True,
90
+ vertical_spacing=0.04,
91
+ specs=[[{"type": "scatter"}]] * rows,
92
+ row_heights=[0.18, 0.22, 0.2, 0.2, 0.2],
93
+ )
94
+
95
+ if has_price and price_norm is not None:
96
+ fig.add_trace(
97
+ go.Scatter(x=df.index, y=price_norm, name="策略收盘价", line=dict(color="#2E86AB", width=1.5)),
98
+ row=1,
99
+ col=1,
100
+ )
101
+ if bench_norm_price is not None:
102
+ fig.add_trace(
103
+ go.Scatter(
104
+ x=bench_norm_price.index,
105
+ y=bench_norm_price.values,
106
+ name="基准收盘价",
107
+ line=dict(color="#E63946", width=1.5, dash="dash"),
108
+ ),
109
+ row=1,
110
+ col=1,
111
+ )
112
+ fig.add_trace(
113
+ go.Scatter(x=df.index, y=strategy_nv, name="策略净值", line=dict(color="#2E86AB", width=2)),
114
+ row=2,
115
+ col=1,
116
+ )
117
+ if bench_norm_nv is not None:
118
+ fig.add_trace(
119
+ go.Scatter(
120
+ x=bench_norm_nv.index,
121
+ y=bench_norm_nv.values,
122
+ name="基准净值",
123
+ line=dict(color="#E63946", width=2, dash="dash"),
124
+ ),
125
+ row=2,
126
+ col=1,
127
+ )
128
+ fig.add_trace(
129
+ go.Scatter(
130
+ x=df.index,
131
+ y=drawdown,
132
+ name="回撤",
133
+ fill="tozeroy",
134
+ line=dict(color="#C1121F"),
135
+ fillcolor="rgba(242,66,54,0.4)",
136
+ ),
137
+ row=3,
138
+ col=1,
139
+ )
140
+ fig.add_trace(
141
+ go.Bar(x=df.index, y=returns_pct, name="每日盈亏", marker_color=np.where(returns_pct >= 0, "#ef4444", "#22c55e")),
142
+ row=4,
143
+ col=1,
144
+ )
145
+ returns_for_dist = returns_pct[returns_pct != 0]
146
+ if len(returns_for_dist) > 1:
147
+ if gaussian_kde is not None:
148
+ kde = gaussian_kde(returns_for_dist)
149
+ kde.set_bandwidth(kde.factor * 0.5)
150
+ x_range = np.linspace(returns_for_dist.min(), returns_for_dist.max(), 300)
151
+ density = kde(x_range)
152
+ bin_width = x_range[1] - x_range[0]
153
+ frequency = density * bin_width * len(returns_for_dist)
154
+ fig.add_trace(
155
+ go.Scatter(
156
+ x=x_range,
157
+ y=frequency,
158
+ name="盈亏分布",
159
+ fill="tozeroy",
160
+ line=dict(color="#8B6F5E"),
161
+ fillcolor="rgba(107,76,63,0.6)",
162
+ ),
163
+ row=5,
164
+ col=1,
165
+ )
166
+ else:
167
+ fig.add_trace(
168
+ go.Histogram(x=returns_for_dist, name="盈亏分布", nbinsx=40, marker_color="#6B4C3F"),
169
+ row=5,
170
+ col=1,
171
+ )
172
+ else:
173
+ fig.add_trace(
174
+ go.Histogram(x=returns_pct, name="盈亏分布", nbinsx=10, marker_color="#6B4C3F"),
175
+ row=5,
176
+ col=1,
177
+ )
178
+
179
+ fig.update_xaxes(title_text="日期", row=5, col=1)
180
+ fig.update_yaxes(title_text="价格(归一化)", row=1, col=1)
181
+ fig.update_yaxes(title_text="净值", row=2, col=1)
182
+ fig.update_yaxes(title_text="回撤 (%)", row=3, col=1)
183
+ fig.update_yaxes(title_text="收益率 (%)", row=4, col=1)
184
+ fig.update_yaxes(title_text="频数", row=5, col=1)
185
+
186
+ base_title = title or "策略表现分析"
187
+ fig.update_layout(
188
+ title=f"{base_title}<br><sup>{date_text}</sup>",
189
+ template="plotly_white",
190
+ showlegend=True,
191
+ )
192
+
193
+ if save_path:
194
+ html_path = save_path if str(save_path).lower().endswith(".html") else f"{save_path}.html"
195
+ fig.write_html(html_path, include_plotlyjs="cdn")
196
+ self.logger.info("Chart saved to %s", html_path)
197
+ else:
198
+ fig.show()
199
+ return
200
+
201
+ # ------------------------------ Matplotlib branch ------------------------------
202
+ n_panels = 5 if has_price else 4
203
+ fig, axes = plt.subplots(n_panels, 1, figsize=(16, 14 if has_price else 12))
204
+ base_title = title or "策略表现分析"
205
+ fig.suptitle(f"{base_title} | {date_text}", fontsize=16, y=0.995)
206
+
207
+ idx = 0
208
+ if has_price and price_norm is not None:
209
+ self._plot_price_compare(ax=axes[idx], df=df, benchmark_close=benchmark_close, price_norm=price_norm, bench_norm=bench_norm_price)
210
+ idx += 1
211
+
212
+ self._plot_net_value(ax=axes[idx], df=df, strategy_nv=strategy_nv, bench_norm=bench_norm_nv)
213
+ self._plot_drawdown(ax=axes[idx + 1], drawdown=drawdown)
214
+ self._plot_daily_returns(ax=axes[idx + 2], returns_pct=returns_pct)
215
+ self._plot_return_distribution(ax=axes[idx + 3], returns_pct=returns_pct)
216
+
217
+ for ax in axes[:-1]:
218
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
219
+ ax.xaxis.set_major_locator(mdates.MonthLocator(interval=6))
220
+ plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right")
221
+
222
+ plt.tight_layout(rect=[0, 0, 1, 0.97])
223
+
224
+ if save_path:
225
+ fig.savefig(save_path, dpi=300, bbox_inches="tight")
226
+ self.logger.info("Chart saved to %s", save_path)
227
+ else:
228
+ plt.show()
229
+
230
+ # ------------------------------------------------------------------
231
+ # Individual panels (Matplotlib)
232
+ # ------------------------------------------------------------------
233
+
234
+ def _plot_price_compare(
235
+ self,
236
+ ax: plt.Axes,
237
+ df: pd.DataFrame,
238
+ benchmark_close: Optional[pd.Series],
239
+ price_norm: pd.Series,
240
+ bench_norm: Optional[pd.Series],
241
+ ) -> None:
242
+ ax.plot(df.index, price_norm, linewidth=1.5, color="#2E86AB", label="策略收盘价")
243
+
244
+ if bench_norm is not None:
245
+ ax.plot(bench_norm.index, bench_norm.values, linewidth=1.5, color="#E63946", linestyle="--", label="基准收盘价")
246
+ ax.legend()
247
+
248
+ ax.set_title("价格对比", fontsize=14)
249
+ ax.set_xlabel("日期", fontsize=11)
250
+ ax.set_ylabel("价格(归一化)", fontsize=11)
251
+ ax.grid(True, alpha=0.3, linestyle="--")
252
+
253
+ def _plot_net_value(
254
+ self,
255
+ ax: plt.Axes,
256
+ df: pd.DataFrame,
257
+ strategy_nv: pd.Series,
258
+ bench_norm: Optional[pd.Series],
259
+ ) -> None:
260
+ ax.plot(df.index, strategy_nv, linewidth=2, color="#2E86AB", label="策略净值")
261
+
262
+ if bench_norm is not None:
263
+ ax.plot(bench_norm.index, bench_norm.values, linewidth=2, color="#E63946", linestyle="--", label="基准净值")
264
+ ax.legend()
265
+
266
+ ax.set_title("账户净值", fontsize=14)
267
+ ax.set_xlabel("日期", fontsize=11)
268
+ ax.set_ylabel("净值(起始=1)", fontsize=11)
269
+ ax.grid(True, alpha=0.3, linestyle="--")
270
+
271
+ def _plot_drawdown(self, ax: plt.Axes, drawdown: pd.Series) -> None:
272
+ ax.fill_between(drawdown.index, drawdown, 0, color="#F24236", alpha=0.5)
273
+ ax.plot(drawdown.index, drawdown, color="#C1121F", linewidth=1.5)
274
+ ax.set_title("净值回撤", fontsize=14)
275
+ ax.set_xlabel("日期", fontsize=11)
276
+ ax.set_ylabel("回撤 (%)", fontsize=11)
277
+ ax.grid(True, alpha=0.3, linestyle="--")
278
+
279
+ def _plot_daily_returns(self, ax: plt.Axes, returns_pct: pd.Series) -> None:
280
+ colors = ["#ef4444" if x >= 0 else "#22c55e" for x in returns_pct]
281
+ ax.bar(returns_pct.index, returns_pct, color=colors, alpha=0.7, width=0.8)
282
+ ax.axhline(y=0, color="black", linewidth=0.5)
283
+
284
+ max_return = abs(returns_pct.max()) if returns_pct.max() != 0 else 1
285
+ min_return = abs(returns_pct.min()) if returns_pct.min() != 0 else 1
286
+ y_max = max(max_return, min_return) * 1.1
287
+ ax.set_ylim(-y_max, y_max)
288
+
289
+ ax.set_title("每日盈亏", fontsize=14)
290
+ ax.set_xlabel("日期", fontsize=11)
291
+ ax.set_ylabel("收益率 (%)", fontsize=11)
292
+ ax.grid(True, alpha=0.3, linestyle="--", axis="y")
293
+
294
+ def _plot_return_distribution(self, ax: plt.Axes, returns_pct: pd.Series) -> None:
295
+ returns_for_dist = returns_pct[returns_pct != 0]
296
+ ax.set_title("盈亏分布(已交易日期)", fontsize=14)
297
+ ax.set_xlabel("盈亏值 (%)", fontsize=11)
298
+ ax.set_ylabel("频数", fontsize=11)
299
+ ax.grid(True, alpha=0.3, linestyle="--", axis="y")
300
+
301
+ if len(returns_for_dist) < 2:
302
+ return
303
+
304
+ if gaussian_kde is None:
305
+ ax.hist(returns_for_dist, bins=40, color="#6B4C3F", alpha=0.7, edgecolor="white")
306
+ ax.axvline(x=0, color="gray", linestyle="--", linewidth=1, alpha=0.7)
307
+ return
308
+
309
+ kde = gaussian_kde(returns_for_dist)
310
+ kde.set_bandwidth(kde.factor * 0.5)
311
+ x_range = np.linspace(returns_for_dist.min(), returns_for_dist.max(), 300)
312
+ density = kde(x_range)
313
+ bin_width = x_range[1] - x_range[0]
314
+ frequency = density * bin_width * len(returns_for_dist)
315
+
316
+ ax.fill_between(x_range, 0, frequency, color="#6B4C3F", alpha=0.7)
317
+ ax.plot(x_range, frequency, color="#8B6F5E", linewidth=2)
318
+ ax.axvline(x=0, color="gray", linestyle="--", linewidth=1, alpha=0.7)
319
+
@@ -0,0 +1,64 @@
1
+ """Minimal price chart helper (Matplotlib or Plotly)."""
2
+ from typing import Dict, Optional, Union
3
+
4
+ import matplotlib.pyplot as plt
5
+ import pandas as pd
6
+
7
+ from ..core.base import BaseComponent
8
+
9
+
10
+ class PriceChart(BaseComponent):
11
+ def initialize(self) -> bool:
12
+ """Initialize the price chart."""
13
+ self.logger.info("Initializing price chart")
14
+ return True
15
+
16
+ def plot_prices(
17
+ self,
18
+ data: Union[pd.DataFrame, Dict[str, pd.DataFrame]],
19
+ price_column: Optional[str] = "Close",
20
+ normalize: Optional[bool] = True,
21
+ title: Optional[str] = None,
22
+ use_plotly: Optional[bool] = False,
23
+ ) -> None:
24
+ series_map = self._collect(data, price_column, normalize)
25
+ ylabel = "Normalized Price" if normalize else "Price"
26
+ title = title or ("Normalized Price Comparison" if len(series_map) > 1 else "Price Chart")
27
+
28
+ if use_plotly:
29
+ import plotly.graph_objects as go
30
+
31
+ fig = go.Figure()
32
+ for label, series in series_map.items():
33
+ fig.add_trace(go.Scatter(x=series.index, y=series.values, mode="lines", name=label))
34
+ fig.update_layout(title=title, xaxis_title="Date", yaxis_title=ylabel, template="plotly_white")
35
+ fig.show()
36
+ return
37
+
38
+ fig, ax = plt.subplots(figsize=(12, 6))
39
+ for label, series in series_map.items():
40
+ ax.plot(series.index, series.values, label=label)
41
+ ax.set_xlabel("Date")
42
+ ax.set_ylabel(ylabel)
43
+ ax.set_title(title)
44
+ ax.legend()
45
+ ax.grid(True, alpha=0.3)
46
+ plt.xticks(rotation=45)
47
+ plt.tight_layout()
48
+ plt.show()
49
+
50
+ def _collect(
51
+ self,
52
+ data: Union[pd.DataFrame, Dict[str, pd.DataFrame]],
53
+ price_column: str,
54
+ normalize: bool,
55
+ ) -> Dict[str, pd.Series]:
56
+ def prep(frame: pd.DataFrame) -> pd.Series:
57
+ series = frame[price_column]
58
+ if normalize:
59
+ series = series / series.iloc[0]
60
+ return series
61
+
62
+ if isinstance(data, pd.DataFrame):
63
+ return {price_column: prep(data)}
64
+ return {name: prep(df) for name, df in data.items()}
@@ -0,0 +1,181 @@
1
+ """Minimal signal chart helper."""
2
+
3
+ from typing import Dict, Optional
4
+
5
+ import matplotlib.dates as mdates
6
+ import matplotlib.pyplot as plt
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from ..core.base import BaseComponent
11
+
12
+
13
+ class SignalChart(BaseComponent):
14
+ def initialize(self) -> bool:
15
+ """Initialize the signal chart."""
16
+ self.logger.info("Initializing signal chart")
17
+ return True
18
+
19
+ def plot_boll_signals(
20
+ self,
21
+ data: pd.DataFrame,
22
+ bands: Dict[str, pd.Series],
23
+ signals: pd.Series,
24
+ title: Optional[str] = None,
25
+ use_plotly: bool = False,
26
+ ) -> None:
27
+ required = {"Open", "High", "Low", "Close"}
28
+ if not required.issubset(data.columns):
29
+ raise ValueError(f"data must contain columns: {required}")
30
+
31
+ frame = data.copy()
32
+ frame.index = pd.to_datetime(frame.index)
33
+ signals = signals.reindex(frame.index)
34
+
35
+ upper = bands.get("upper", pd.Series(index=frame.index)).reindex(frame.index)
36
+ middle = bands.get("middle", pd.Series(index=frame.index)).reindex(frame.index)
37
+ lower = bands.get("lower", pd.Series(index=frame.index)).reindex(frame.index)
38
+
39
+ if use_plotly:
40
+ try:
41
+ import plotly.graph_objects as go
42
+ from plotly.subplots import make_subplots
43
+ except ImportError as exc: # pragma: no cover
44
+ self.logger.warning("Plotly not available (%s); falling back to Matplotlib", exc)
45
+ else:
46
+ fig = make_subplots(
47
+ rows=2,
48
+ cols=1,
49
+ shared_xaxes=True,
50
+ row_heights=[0.78, 0.22],
51
+ specs=[[{"type": "candlestick"}], [{"type": "scatter"}]],
52
+ )
53
+ fig.add_trace(
54
+ go.Candlestick(
55
+ x=frame.index,
56
+ open=frame["Open"],
57
+ high=frame["High"],
58
+ low=frame["Low"],
59
+ close=frame["Close"],
60
+ name="Price",
61
+ increasing_line_color="#ef4444",
62
+ decreasing_line_color="#22c55e",
63
+ ),
64
+ row=1,
65
+ col=1,
66
+ )
67
+ for name, series, color in (
68
+ ("Upper", upper, "#2563eb"),
69
+ ("Middle", middle, "#7c3aed"),
70
+ ("Lower", lower, "#0ea5e9"),
71
+ ):
72
+ fig.add_trace(
73
+ go.Scatter(x=series.index, y=series.values, mode="lines", name=name, line=dict(color=color)),
74
+ row=1,
75
+ col=1,
76
+ )
77
+ buy_mask = signals == 1
78
+ sell_mask = signals == -1
79
+ fig.add_trace(
80
+ go.Scatter(
81
+ x=signals.index[buy_mask],
82
+ y=frame.loc[buy_mask, "Close"],
83
+ mode="markers",
84
+ name="Buy",
85
+ marker=dict(symbol="triangle-up", size=12, color="#ef4444"),
86
+ ),
87
+ row=1,
88
+ col=1,
89
+ )
90
+ fig.add_trace(
91
+ go.Scatter(
92
+ x=signals.index[sell_mask],
93
+ y=frame.loc[sell_mask, "Close"],
94
+ mode="markers",
95
+ name="Sell",
96
+ marker=dict(symbol="triangle-down", size=12, color="#22c55e"),
97
+ ),
98
+ row=1,
99
+ col=1,
100
+ )
101
+ fig.add_trace(
102
+ go.Scatter(
103
+ x=signals.index,
104
+ y=signals.values,
105
+ name="Signal",
106
+ mode="lines",
107
+ line=dict(color="#6b7280", dash="dot"),
108
+ showlegend=False,
109
+ ),
110
+ row=2,
111
+ col=1,
112
+ )
113
+ fig.update_xaxes(title_text="Date", row=2, col=1)
114
+ fig.update_yaxes(title_text="Price", row=1, col=1)
115
+ fig.update_yaxes(
116
+ title_text="Signal",
117
+ row=2,
118
+ col=1,
119
+ tickvals=[-1, 0, 1],
120
+ ticktext=["Sell", "Hold", "Buy"],
121
+ range=[-1.5, 1.5],
122
+ )
123
+ fig.update_layout(
124
+ title=title or "Bollinger Signal Chart",
125
+ template="plotly_white",
126
+ showlegend=True,
127
+ )
128
+ fig.show()
129
+ return
130
+
131
+ fig, (ax_price, ax_signal) = plt.subplots(2, 1, figsize=(12, 6), sharex=True, gridspec_kw={"height_ratios": [4, 1]})
132
+ self._plot_candles(ax_price, frame)
133
+ ax_price.plot(upper.index, upper.values, color="#2563eb", linewidth=1.2, label="Upper", zorder=2)
134
+ ax_price.plot(middle.index, middle.values, color="#7c3aed", linewidth=1.0, linestyle="--", label="Middle", zorder=2)
135
+ ax_price.plot(lower.index, lower.values, color="#0ea5e9", linewidth=1.2, label="Lower", zorder=2)
136
+ self._plot_markers(ax_price, frame["Close"], signals)
137
+
138
+ ax_price.set_title(title or "Bollinger Signal Chart", fontsize=14)
139
+ ax_price.set_ylabel("Price")
140
+ ax_price.grid(True, alpha=0.2, color="#d1d5db")
141
+ handles, labels = ax_price.get_legend_handles_labels()
142
+ unique = dict(zip(labels, handles))
143
+ ax_price.legend(unique.values(), unique.keys(), frameon=False, loc="upper left")
144
+
145
+ ax_signal.plot(signals.index, signals.values, color="#6b7280", linewidth=1.0, linestyle=":")
146
+ ax_signal.set_ylabel("Signal")
147
+ ax_signal.set_xlabel("Date")
148
+ ax_signal.set_yticks([-1, 0, 1])
149
+ ax_signal.set_yticklabels(["Sell", "Hold", "Buy"])
150
+ ax_signal.grid(True, alpha=0.2, color="#d1d5db")
151
+
152
+ for ax in (ax_price, ax_signal):
153
+ ax.xaxis.set_major_locator(mdates.MonthLocator(interval=2))
154
+ ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
155
+ plt.setp(ax.xaxis.get_majorticklabels(), rotation=45, ha="right")
156
+ plt.tight_layout()
157
+ plt.show()
158
+
159
+ def _plot_candles(self, ax: plt.Axes, frame: pd.DataFrame) -> None:
160
+ x = mdates.date2num(frame.index.to_pydatetime())
161
+ for xi, row in zip(x, frame[["Open", "High", "Low", "Close"]].itertuples(index=False)):
162
+ o, h, l, c = row
163
+ color = "#ef4444" if c >= o else "#22c55e"
164
+ ax.vlines(xi, l, h, color="#6b7280", linewidth=0.8)
165
+ ax.add_patch(
166
+ plt.Rectangle(
167
+ (xi - 0.2, min(o, c)),
168
+ 0.4,
169
+ abs(c - o) or 0.2,
170
+ facecolor=color,
171
+ edgecolor=color,
172
+ alpha=0.85,
173
+ )
174
+ )
175
+ ax.xaxis_date()
176
+
177
+ def _plot_markers(self, ax: plt.Axes, price: pd.Series, signals: pd.Series) -> None:
178
+ buy = signals == 1
179
+ sell = signals == -1
180
+ ax.scatter(price.index[buy], price[buy], marker="^", color="#ef4444", edgecolors="white", linewidths=0.6, s=140, label="Buy", zorder=3)
181
+ ax.scatter(price.index[sell], price[sell], marker="v", color="#22c55e", edgecolors="white", linewidths=0.6, s=140, label="Sell", zorder=3)
@@ -0,0 +1,18 @@
1
+ """
2
+ Core functionality for DeltaFQ.
3
+ """
4
+
5
+ from .config import Config
6
+ from .logger import Logger
7
+ from .exceptions import DeltaFQError, DataError, TradingError
8
+ from .base import BaseComponent
9
+
10
+ __all__ = [
11
+ "Config",
12
+ "Logger",
13
+ "DeltaFQError",
14
+ "DataError",
15
+ "TradingError",
16
+ "BaseComponent"
17
+ ]
18
+
deltafq/core/base.py ADDED
@@ -0,0 +1,21 @@
1
+ """
2
+ Base classes for DeltaFQ components.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from .logger import Logger
7
+
8
+
9
+ class BaseComponent(ABC):
10
+ """Base class for all DeltaFQ components."""
11
+
12
+ def __init__(self, name: str = None, **kwargs):
13
+ """Initialize base component."""
14
+ self.name = name or self.__class__.__name__
15
+ self.logger = Logger(self.name)
16
+
17
+ @abstractmethod
18
+ def initialize(self) -> bool:
19
+ """Initialize the component."""
20
+ pass
21
+
deltafq/core/config.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Configuration management for DeltaFQ.
3
+ """
4
+
5
+ import os
6
+ from typing import Dict, Any
7
+ from pathlib import Path
8
+
9
+
10
+ class Config:
11
+ """Configuration manager for DeltaFQ."""
12
+
13
+ def __init__(self, config_file: str = None):
14
+ """Initialize configuration."""
15
+ self.config = self._load_default_config()
16
+ if config_file and os.path.exists(config_file):
17
+ self._load_config_file(config_file)
18
+
19
+ def _load_default_config(self) -> Dict[str, Any]:
20
+ """Load default configuration."""
21
+ return {
22
+ "data": {
23
+ "cache_dir": "data_cache",
24
+ "default_source": "yahoo"
25
+ },
26
+ "trading": {
27
+ "initial_capital": 1000000,
28
+ "commission": 0.001,
29
+ "slippage": 0.0005
30
+ },
31
+ "logging": {
32
+ "level": "INFO",
33
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
34
+ }
35
+ }
36
+
37
+ def _load_config_file(self, config_file: str):
38
+ """Load configuration from file."""
39
+ # Placeholder for config file loading
40
+ pass
41
+
42
+ def get(self, key: str, default: Any = None) -> Any:
43
+ """Get configuration value."""
44
+ keys = key.split('.')
45
+ value = self.config
46
+ for k in keys:
47
+ if isinstance(value, dict) and k in value:
48
+ value = value[k]
49
+ else:
50
+ return default
51
+ return value
52
+
53
+ def set(self, key: str, value: Any):
54
+ """Set configuration value."""
55
+ keys = key.split('.')
56
+ config = self.config
57
+ for k in keys[:-1]:
58
+ if k not in config:
59
+ config[k] = {}
60
+ config = config[k]
61
+ config[keys[-1]] = value
62
+