mirror of
https://github.com/OpenByteInc/QuantDinger.git
synced 2026-09-28 15:22:56 +08:00
175 lines
5.3 KiB
Python
175 lines
5.3 KiB
Python
"""Tests for indicator_code_quality heuristics."""
|
|
|
|
from app.services.indicator_code_quality import analyze_indicator_code_quality
|
|
|
|
|
|
def test_empty_code():
|
|
hints = analyze_indicator_code_quality("")
|
|
assert any(h["code"] == "EMPTY_CODE" for h in hints)
|
|
|
|
|
|
def test_minimal_valid_style():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
df = df.copy()
|
|
buy_signal = df['close'] > df['close'].rolling(10).mean()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
codes = {h["code"] for h in hints}
|
|
assert "MISSING_OUTPUT" not in codes
|
|
assert "STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR" not in codes
|
|
|
|
|
|
def test_execution_columns_are_ignored_for_chart_indicators():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
df = df.copy()
|
|
df['open_long'] = True
|
|
df['close_long'] = False
|
|
df['open_short'] = True
|
|
df['close_short'] = False
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
codes = [h["code"] for h in hints]
|
|
assert "EXECUTION_COLUMNS_IGNORED_FOR_INDICATOR" in codes
|
|
|
|
|
|
def test_strategy_annotations_are_ignored_for_chart_indicators():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# @strategy tradeDirection long
|
|
# @strategy entryPct 1
|
|
df = df.copy()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert any(h["code"] == "STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR" for h in hints)
|
|
|
|
|
|
def test_strategy_signal_form_metadata_is_blocked_for_chart_indicators():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# signal_form: four_way
|
|
# exit_owner: indicator
|
|
# flip_mode: R2
|
|
df = df.copy()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
match = [h for h in hints if h["code"] == "STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR"]
|
|
assert match
|
|
assert match[0]["severity"] == "error"
|
|
|
|
|
|
def test_strategy_annotation_is_rejected_for_chart_indicators():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# @strategy leverage 2
|
|
df = df.copy()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert any(h["code"] == "STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR" for h in hints)
|
|
|
|
|
|
def test_strategy_timing_annotation_is_rejected_for_chart_indicators():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# @strategy signalTiming same_bar_close
|
|
df = df.copy()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert any(h["code"] == "STRATEGY_ANNOTATIONS_IGNORED_FOR_INDICATOR" for h in hints)
|
|
|
|
|
|
def test_declared_params_must_be_read_via_params_get():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# @param fast_period int 10 Fast MA
|
|
df = df.copy()
|
|
ma = df['close'].rolling(window=fast_period).mean()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert any(h["code"] == "DECLARED_PARAMS_NOT_READ_VIA_PARAMS_GET" for h in hints)
|
|
|
|
|
|
def test_declared_params_read_via_params_get_is_ok():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# @param fast_period int 10 Fast MA
|
|
fast_period = params.get('fast_period', 10)
|
|
df = df.copy()
|
|
ma = df['close'].rolling(window=fast_period).mean()
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert not any(h["code"] == "DECLARED_PARAMS_NOT_READ_VIA_PARAMS_GET" for h in hints)
|
|
|
|
|
|
def test_declared_params_read_via_params_helper_is_ok():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
# @param fast_period int 10 Fast MA
|
|
# @param confirmation_mode bool true Confirmation mode
|
|
try:
|
|
params
|
|
except NameError:
|
|
params = {}
|
|
|
|
def _param(name, default, cast):
|
|
try:
|
|
return cast(params.get(name, default))
|
|
except Exception:
|
|
return default
|
|
|
|
fast_period = _param("fast_period", 10, int)
|
|
confirmation_mode = _param("confirmation_mode", True, bool)
|
|
df = df.copy()
|
|
ma = df['close'].rolling(window=fast_period).mean()
|
|
output = {'name': 'T', 'plots': [], 'signals': [], 'calculatedVars': {'confirmation': confirmation_mode}}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert not any(h["code"] == "DECLARED_PARAMS_NOT_READ_VIA_PARAMS_GET" for h in hints)
|
|
|
|
|
|
def test_four_way_columns_no_missing_buy_sell_warn():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
df = df.copy()
|
|
df['open_long'] = False
|
|
df['close_long'] = False
|
|
df['open_short'] = False
|
|
df['close_short'] = False
|
|
output = {'name': 'T', 'plots': [], 'signals': []}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
codes = {h["code"] for h in hints}
|
|
assert "MISSING_BUY_SELL_COLUMNS" not in codes
|
|
assert "EXECUTION_COLUMNS_IGNORED_FOR_INDICATOR" in codes
|
|
|
|
|
|
def test_where_none_signal_markers_warned():
|
|
code = """
|
|
my_indicator_name = "T"
|
|
my_indicator_description = "D"
|
|
df = df.copy()
|
|
entry_marks = df['close'].where(df['close'] > df['close'].rolling(5).mean(), None).tolist()
|
|
output = {'name': 'T', 'plots': [], 'signals': [{'type': 'entry', 'data': entry_marks}]}
|
|
"""
|
|
hints = analyze_indicator_code_quality(code)
|
|
assert any(h["code"] == "SIGNAL_MARKERS_USE_WHERE_NONE" for h in hints)
|