forked from ZhuLinsen/daily_stock_analysis
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_stock_analyzer_bias.py
More file actions
179 lines (162 loc) · 7.05 KB
/
test_stock_analyzer_bias.py
File metadata and controls
179 lines (162 loc) · 7.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# -*- coding: utf-8 -*-
"""
Unit tests for StockTrendAnalyzer._generate_signal bias and strong-trend relief logic (Issue #296).
"""
import math
import unittest
from unittest.mock import patch, MagicMock
from src.stock_analyzer import (
StockTrendAnalyzer,
TrendAnalysisResult,
TrendStatus,
VolumeStatus,
MACDStatus,
RSIStatus,
)
def _make_result(
code: str = "000001",
trend_status: TrendStatus = TrendStatus.BULL,
trend_strength: float = 50.0,
bias_ma5: float = 0.0,
volume_status: VolumeStatus = VolumeStatus.NORMAL,
macd_status: MACDStatus = MACDStatus.BULLISH,
rsi_status: RSIStatus = RSIStatus.NEUTRAL,
support_ma5: bool = False,
support_ma10: bool = False,
) -> TrendAnalysisResult:
"""Build TrendAnalysisResult with defaults for _generate_signal bias branch testing."""
return TrendAnalysisResult(
code=code,
trend_status=trend_status,
ma_alignment="",
trend_strength=trend_strength,
ma5=10.0,
ma10=9.5,
ma20=9.0,
ma60=8.5,
current_price=10.0,
bias_ma5=bias_ma5,
bias_ma10=0.0,
bias_ma20=0.0,
volume_status=volume_status,
volume_ratio_5d=1.0,
volume_trend="",
support_ma5=support_ma5,
support_ma10=support_ma10,
macd_status=macd_status,
rsi_status=rsi_status,
)
class StockAnalyzerBiasTestCase(unittest.TestCase):
"""Tests for bias_ma5 and strong-trend relief in _generate_signal."""
def setUp(self) -> None:
self.analyzer = StockTrendAnalyzer()
def _assert_contains(self, items: list, substring: str) -> None:
"""Assert at least one item contains the substring."""
self.assertTrue(
any(substring in s for s in items),
msg=f"Expected substring '{substring}' in {items}",
)
def _assert_not_contains(self, items: list, substring: str) -> None:
"""Assert no item contains the substring."""
self.assertFalse(
any(substring in s for s in items),
msg=f"Did not expect substring '{substring}' in {items}",
)
@patch("src.stock_analyzer.get_config")
def test_bias_nan_defense(self, mock_get_config: MagicMock) -> None:
"""bias_ma5=NaN should be treated as 0.0 without exception."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.BULL,
bias_ma5=float("nan"),
)
self.analyzer._generate_signal(result)
self.assertIsInstance(result.signal_score, (int, float))
self.assertFalse(math.isnan(result.signal_score))
@patch("src.stock_analyzer.get_config")
def test_bias_negative_pullback(self, mock_get_config: MagicMock) -> None:
"""bias=-2% should yield '回踩买点'."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.BULL,
bias_ma5=-2.0,
)
self.analyzer._generate_signal(result)
self._assert_contains(result.signal_reasons, "回踩买点")
@patch("src.stock_analyzer.get_config")
def test_bias_close_to_ma5(self, mock_get_config: MagicMock) -> None:
"""bias=1.5% should yield '介入好时机'."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.BULL,
bias_ma5=1.5,
)
self.analyzer._generate_signal(result)
self._assert_contains(result.signal_reasons, "介入好时机")
@patch("src.stock_analyzer.get_config")
def test_bias_slightly_high(self, mock_get_config: MagicMock) -> None:
"""bias=4% (< base_threshold=5%) should yield '可小仓介入'."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.BULL,
bias_ma5=4.0,
)
self.analyzer._generate_signal(result)
self._assert_contains(result.signal_reasons, "可小仓介入")
@patch("src.stock_analyzer.get_config")
def test_strong_trend_relaxed_threshold(self, mock_get_config: MagicMock) -> None:
"""STRONG_BULL + trend_strength=75 + bias=6% -> '可轻仓追踪' (effective=7.5%)."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.STRONG_BULL,
trend_strength=75.0,
bias_ma5=6.0,
)
self.analyzer._generate_signal(result)
self._assert_contains(result.signal_reasons, "可轻仓追踪")
self._assert_not_contains(result.risk_factors, "严禁追高")
@patch("src.stock_analyzer.get_config")
def test_non_strong_trend_strict_threshold(self, mock_get_config: MagicMock) -> None:
"""BULL + bias=6% -> '严禁追高!'."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.BULL,
bias_ma5=6.0,
)
self.analyzer._generate_signal(result)
self._assert_contains(result.risk_factors, "严禁追高")
@patch("src.stock_analyzer.get_config")
def test_strong_trend_exceed_effective(self, mock_get_config: MagicMock) -> None:
"""STRONG_BULL + trend_strength=80 + bias=10% -> '严禁追高!' (exceeds 7.5%)."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.STRONG_BULL,
trend_strength=80.0,
bias_ma5=10.0,
)
self.analyzer._generate_signal(result)
self._assert_contains(result.risk_factors, "严禁追高")
@patch("src.stock_analyzer.get_config")
def test_boundary_at_base_threshold(self, mock_get_config: MagicMock) -> None:
"""bias=5.0% (exact base_threshold) -> '可小仓介入' (bias < base_threshold is False)."""
mock_get_config.return_value.bias_threshold = 5.0
result = _make_result(
trend_status=TrendStatus.BULL,
bias_ma5=5.0,
)
self.analyzer._generate_signal(result)
# bias=5.0: bias < base_threshold (5 < 5) is False, so we go to next branch
# bias < 2 is False, bias < base_threshold is False (5 < 5)
# bias > effective_threshold: 5 > 5 False
# bias > base_threshold and is_strong_trend: 5 > 5 False
# else: 5 > 5 False, so we'd get to the else branch with "严禁追高"
# Actually: bias < 2 -> False, bias < base_threshold (5 < 5) -> False
# bias > effective_threshold (5 > 5) -> False
# bias > base_threshold and is_strong_trend -> False
# else -> bias > base_threshold (5 > 5) False... wait
# Let me re-read: elif bias < base_threshold -> 5 < 5 is False
# elif bias > effective_threshold -> 5 > 5 is False
# elif bias > base_threshold and is_strong_trend -> 5 > 5 is False
# else: risks.append 严禁追高 - so we get 严禁追高
# Because 5.0 is not < 5.0, not > 5.0 when effective=base=5. So we hit the else.
self._assert_contains(result.risk_factors, "严禁追高")