alphax/tests/test_recommendation_execution_status.py
2026-05-18 00:58:19 +08:00

327 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
import os
import sqlite3
import tempfile
import unittest
from datetime import datetime
from unittest.mock import patch
from app.db import altcoin_db
class RecommendationExecutionStatusTests(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmpdir.name, 'test_altcoin.db')
self.db_patch = patch.object(altcoin_db, 'DB_PATH', self.db_path)
self.db_patch.start()
altcoin_db.init_db()
def tearDown(self):
self.db_patch.stop()
self.tmpdir.cleanup()
def _insert_rec(self, **kwargs):
defaults = dict(
symbol='AAA/USDT',
rec_time=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
rec_state='加速',
rec_score=40,
entry_price=100.0,
stop_loss=95.0,
tp1=110.0,
tp2=118.0,
sector='AI',
signals=json.dumps(['🟢 15min即刻入场信号'], ensure_ascii=False),
is_meme=0,
status='active',
current_price=100.0,
max_price=104.0,
min_price=98.0,
pnl_pct=0.0,
max_pnl_pct=4.0,
max_drawdown_pct=-1.0,
hit_tp1_time='',
hit_tp2_time='',
stopped_out_time='',
expired_time='',
last_track_time=datetime.now().strftime('%Y-%m-%dT%H:%M:%S'),
entry_plan_json=json.dumps({
'entry_price': 100.0,
'entry_action': '可即刻买入',
'risk_reward_ok': True,
'stop_loss': 95.0,
'stop_pct': -5.0,
'tp1': 110.0,
'tp2': 118.0,
'rr1': 2.0,
'rr2': 3.6,
'entry_trigger_confirmed': True,
}, ensure_ascii=False),
action_status='可即刻买入',
direction='多头启动',
)
defaults.update(kwargs)
conn = sqlite3.connect(self.db_path)
conn.execute(
'''
INSERT INTO recommendation (
symbol, rec_time, rec_state, rec_score, entry_price, stop_loss, tp1, tp2,
sector, signals, is_meme, status, current_price, max_price, min_price,
pnl_pct, max_pnl_pct, max_drawdown_pct, hit_tp1_time, hit_tp2_time,
stopped_out_time, expired_time, last_track_time, entry_plan_json,
action_status, direction
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''',
(
defaults['symbol'], defaults['rec_time'], defaults['rec_state'], defaults['rec_score'],
defaults['entry_price'], defaults['stop_loss'], defaults['tp1'], defaults['tp2'],
defaults['sector'], defaults['signals'], defaults['is_meme'], defaults['status'],
defaults['current_price'], defaults['max_price'], defaults['min_price'], defaults['pnl_pct'],
defaults['max_pnl_pct'], defaults['max_drawdown_pct'], defaults['hit_tp1_time'], defaults['hit_tp2_time'],
defaults['stopped_out_time'], defaults['expired_time'], defaults['last_track_time'], defaults['entry_plan_json'],
defaults['action_status'], defaults['direction'],
),
)
conn.commit()
conn.close()
def _get_active(self):
rows = altcoin_db.get_active_recommendations_deduped()
self.assertGreaterEqual(len(rows), 1)
return next(r for r in rows if r['symbol'] == 'AAA/USDT')
def test_buy_now_status_for_immediate_entry(self):
self._insert_rec()
row = self._get_active()
self.assertEqual(row['execution_status'], 'buy_now')
self.assertEqual(row['execution_label'], '🟢 现在可买')
self.assertEqual(row['initial_action'], '可即刻买入')
self.assertIn('推荐时就是可即刻买入', row['execution_reason'])
def test_low_score_buy_now_downgrades_before_dashboard(self):
self._insert_rec(symbol='DBG/USDT', rec_score=8)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
row = next(r for r in rows if r['symbol'] == 'DBG/USDT')
self.assertEqual(row['execution_status'], 'observe')
self.assertEqual(row['action_status'], '观察')
self.assertIn('信号不足', row['execution_reason'])
self.assertIn('entry_quality_gate', row['entry_plan'])
def test_stale_buy_now_downgrades_before_dashboard(self):
self._insert_rec(rec_time='2026-04-29T10:00:00', last_track_time='2026-04-29T10:05:00')
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
row = next(r for r in rows if r['symbol'] == 'AAA/USDT')
self.assertNotEqual(row['execution_status'], 'buy_now')
self.assertIn(row['action_status'], ('观察', '等回踩'))
self.assertTrue(row.get('entry_window_alert'))
def test_wait_pullback_status_for_wait_action(self):
self._insert_rec(
symbol='BBB/USDT',
action_status='等回踩',
current_price=104.0,
entry_plan_json=json.dumps({
'entry_price': 100.0,
'entry_action': '等回踩',
'risk_reward_ok': True,
}, ensure_ascii=False),
)
rows = altcoin_db.get_active_recommendations_deduped()
target = next(r for r in rows if r['symbol'] == 'BBB/USDT')
self.assertEqual(target['execution_status'], 'wait_pullback')
self.assertEqual(target['execution_label'], '🟡 等回踩,不追高')
self.assertIn('等待回踩', target['execution_reason'])
def test_wait_pullback_downbreak_degrades_to_observe(self):
self._insert_rec(
symbol='BBB/USDT',
action_status='等回踩',
current_price=98.0,
entry_plan_json=json.dumps({
'entry_price': 100.0,
'entry_action': '等回踩',
'risk_reward_ok': True,
'stop_loss': 95.0,
'tp1': 110.0,
'rr1': 2.0,
}, ensure_ascii=False),
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
target = next(r for r in rows if r['symbol'] == 'BBB/USDT')
self.assertEqual(target['action_status'], '观察')
self.assertEqual(target['execution_status'], 'observe')
self.assertIn('回踩参考已下破', target['execution_reason'])
def test_wait_pullback_at_reference_promotes_to_buy_now_when_rr_ok(self):
self._insert_rec(
symbol='BBB/USDT',
action_status='等回踩',
current_price=99.9,
entry_plan_json=json.dumps({
'entry_price': 100.0,
'entry_action': '等回踩',
'risk_reward_ok': True,
'stop_loss': 95.0,
'tp1': 110.0,
'rr1': 2.0,
}, ensure_ascii=False),
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
target = next(r for r in rows if r['symbol'] == 'BBB/USDT')
self.assertEqual(target['action_status'], '可即刻买入')
self.assertEqual(target['execution_status'], 'buy_now')
self.assertTrue(target['entry_plan']['entry_trigger_confirmed'])
def test_invalid_status_for_decay(self):
self._insert_rec(
symbol='CCC/USDT',
action_status='衰减',
current_price=107.0,
max_pnl_pct=8.0,
entry_plan_json=json.dumps({
'entry_price': 100.0,
'entry_action': '可即刻买入',
'risk_reward_ok': True,
}, ensure_ascii=False),
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
target = next(r for r in rows if r['symbol'] == 'CCC/USDT')
self.assertEqual(target['execution_status'], 'invalid')
self.assertEqual(target['execution_label'], '🔴 已失效,勿追')
self.assertIn('趋势衰减', target['execution_reason'])
def test_completed_status_for_take_profit(self):
self._insert_rec(
symbol='DDD/USDT',
status='hit_tp1',
action_status='止盈1',
current_price=111.0,
pnl_pct=11.0,
max_pnl_pct=12.0,
entry_plan_json=json.dumps({
'entry_price': 100.0,
'entry_action': '可即刻买入',
'risk_reward_ok': True,
}, ensure_ascii=False),
)
all_rows = altcoin_db.get_all_recommendations(limit=10)
target = next(r for r in all_rows if r['symbol'] == 'DDD/USDT')
self.assertEqual(target['execution_status'], 'completed')
self.assertEqual(target['execution_label'], '✅ 已兑现,仅观察')
self.assertIn('止盈', target['execution_reason'])
stats = altcoin_db.get_stats()
self.assertEqual(stats['active_count'], 0)
self.assertIsNone(stats['leaderboard']['top_gainer'])
def test_create_recommendation_skips_duplicate_symbol_state_within_window(self):
with patch.object(altcoin_db, 'get_meta', return_value={'strategy_version': 'v1.2'}), \
patch.object(altcoin_db, 'datetime') as mock_datetime:
mock_datetime.now.return_value = unittest.mock.Mock(isoformat=lambda: '2026-04-30T10:00:00')
rec_id_1 = altcoin_db.create_recommendation(
symbol='AAA/USDT', rec_state='加速', rec_score=8, entry_price=100.0,
sector='AI', signals=['4H 连续4K多头加速'], is_meme=0, direction='多头启动'
)
rec_id_2 = altcoin_db.create_recommendation(
symbol='AAA/USDT', rec_state='加速', rec_score=9, entry_price=101.0,
sector='AI', signals=['4H 连续4K多头加速'], is_meme=0, direction='多头启动'
)
self.assertEqual(rec_id_1, rec_id_2)
conn = sqlite3.connect(self.db_path)
count = conn.execute("SELECT COUNT(*) FROM recommendation WHERE symbol='AAA/USDT'").fetchone()[0]
conn.close()
self.assertEqual(count, 1)
def test_create_recommendation_allows_new_state_transition_within_window(self):
with patch.object(altcoin_db, 'get_meta', return_value={'strategy_version': 'v1.2'}), \
patch.object(altcoin_db, 'datetime') as mock_datetime:
mock_datetime.now.return_value = unittest.mock.Mock(isoformat=lambda: '2026-04-30T10:00:00')
rec_id_1 = altcoin_db.create_recommendation(
symbol='AAA/USDT', rec_state='蓄力', rec_score=5, entry_price=100.0,
sector='AI', signals=['4H 3静K蓄力'], is_meme=0, direction='多头启动'
)
rec_id_2 = altcoin_db.create_recommendation(
symbol='AAA/USDT', rec_state='加速', rec_score=9, entry_price=103.0,
sector='AI', signals=['4H 连续4K多头加速'], is_meme=0, direction='多头启动'
)
self.assertNotEqual(rec_id_1, rec_id_2)
conn = sqlite3.connect(self.db_path)
count = conn.execute("SELECT COUNT(*) FROM recommendation WHERE symbol='AAA/USDT'").fetchone()[0]
conn.close()
self.assertEqual(count, 2)
def test_risk_reward_false_blocks_buy_now(self):
self._insert_rec(
symbol='EEE/USDT',
action_status='可即刻买入',
current_price=0.0758,
entry_price=0.0758,
signals=json.dumps(['1H 起爆点↑(强度56×)', '⚠️ 等回踩降权(-3分)'], ensure_ascii=False),
entry_plan_json=json.dumps({
'entry_price': 0.072,
'entry_action': '等回踩',
'risk_reward_ok': False,
'rr1': 0.4,
'stop_loss': 0.07,
}, ensure_ascii=False),
)
rows = altcoin_db.get_active_recommendations_deduped(actionable_only=False)
target = next(r for r in rows if r['symbol'] == 'EEE/USDT')
self.assertNotEqual(target['execution_status'], 'buy_now')
self.assertIn(target['action_status'], ('等回踩', '观察'))
self.assertIn('entry_quality_gate', target['entry_plan'])
def test_update_action_status_refuses_buy_now_when_rr_bad(self):
self._insert_rec(
symbol='FFF/USDT',
action_status='持有',
current_price=0.0758,
entry_price=0.0758,
entry_plan_json=json.dumps({
'entry_price': 0.072,
'entry_action': '等回踩',
'risk_reward_ok': False,
'rr1': 0.4,
}, ensure_ascii=False),
)
conn = sqlite3.connect(self.db_path)
rec_id = conn.execute("SELECT id FROM recommendation WHERE symbol='FFF/USDT'").fetchone()[0]
conn.close()
altcoin_db.update_recommendation_action_status(rec_id, '可即刻买入')
conn = sqlite3.connect(self.db_path)
row = conn.execute("SELECT action_status, entry_plan_json FROM recommendation WHERE id=?", (rec_id,)).fetchone()
conn.close()
self.assertNotEqual(row[0], '可即刻买入')
self.assertIn(row[0], ('等回踩', '观察'))
self.assertIn('entry_quality_gate', json.loads(row[1]))
def test_create_recommendation_stores_stable_signal_codes(self):
rec_id = altcoin_db.create_recommendation(
symbol='SIG/USDT',
rec_state='爆发',
rec_score=12,
entry_price=1.0,
signals=['1H 量价齐飞K(量3.7x)', '1H 量价齐飞K(量9.1x)', '15min 回踩确认'],
entry_plan={'entry_action': '可即刻买入', 'entry_price': 1.0, 'risk_reward_ok': True, 'rr1': 2.0},
direction='多头启动',
)
conn = sqlite3.connect(self.db_path)
row = conn.execute("SELECT signal_codes_json, signal_labels_json FROM recommendation WHERE id=?", (rec_id,)).fetchone()
conn.close()
codes = json.loads(row[0])
labels = json.loads(row[1])
self.assertEqual(codes.count('vp_fly_1h_current'), 1)
self.assertIn('pullback_15m_confirm', codes)
self.assertEqual(len(labels), 3)
if __name__ == '__main__':
unittest.main()