alphax/tests/test_recommendation_execution_status.py
2026-05-13 22:49:47 +08:00

249 lines
10 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 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='2026-04-29T10:00:00',
rec_state='加速',
rec_score=8,
entry_price=100.0,
stop_loss=95.0,
tp1=110.0,
tp2=118.0,
sector='AI',
signals='[]',
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='2026-04-29T10:05:00',
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,
}, 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_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_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]))
if __name__ == '__main__':
unittest.main()