import json import os import sqlite3 import sys import tempfile import unittest from unittest.mock import patch PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) import altcoin_db class RecommendationHistoryBase(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() class RecommendationHistoryGroupingTests(RecommendationHistoryBase): def test_get_all_recommendations_exposes_each_history_group(self): self._insert_rec(symbol='AAA/USDT', action_status='可即刻买入', status='active') self._insert_rec( symbol='BBB/USDT', action_status='等回踩', status='active', entry_plan_json=json.dumps({'entry_action': '等回踩', 'entry_price': 100.0}, ensure_ascii=False), ) self._insert_rec( symbol='CCC/USDT', action_status='持有', status='active', entry_plan_json=json.dumps({'entry_action': '继续观察', 'entry_price': 100.0}, ensure_ascii=False), ) self._insert_rec(symbol='DDD/USDT', action_status='衰减', status='active') self._insert_rec(symbol='EEE/USDT', action_status='止盈1', status='hit_tp1') rows = altcoin_db.get_all_recommendations(limit=20) mapping = {row['symbol']: row['execution_status'] for row in rows} self.assertEqual(mapping['AAA/USDT'], 'buy_now') self.assertEqual(mapping['BBB/USDT'], 'wait_pullback') self.assertEqual(mapping['CCC/USDT'], 'observe') self.assertEqual(mapping['DDD/USDT'], 'invalid') self.assertEqual(mapping['EEE/USDT'], 'completed') def test_get_all_recommendations_can_drive_history_summary_counts(self): self._insert_rec(symbol='AAA/USDT', action_status='可即刻买入', status='active') self._insert_rec(symbol='BBB/USDT', action_status='可即刻买入', status='active') self._insert_rec( symbol='CCC/USDT', action_status='等回踩', status='active', entry_plan_json=json.dumps({'entry_action': '等回踩', 'entry_price': 100.0}, ensure_ascii=False), ) self._insert_rec( symbol='DDD/USDT', action_status='持有', status='active', entry_plan_json=json.dumps({'entry_action': '继续观察', 'entry_price': 100.0}, ensure_ascii=False), ) self._insert_rec(symbol='EEE/USDT', action_status='衰减', status='active') self._insert_rec(symbol='FFF/USDT', action_status='止盈2', status='hit_tp2') rows = altcoin_db.get_all_recommendations(limit=20) counts = {} for row in rows: key = row['execution_status'] counts[key] = counts.get(key, 0) + 1 self.assertEqual(counts, { 'buy_now': 2, 'wait_pullback': 1, 'observe': 1, 'invalid': 1, 'completed': 1, }) class DecisionModeHistoryTests(RecommendationHistoryBase): def test_get_all_recommendations_supports_decision_only_history_mode(self): self._insert_rec(symbol='BUY/USDT', action_status='可即刻买入', status='active') self._insert_rec( symbol='WAIT/USDT', action_status='等回踩', status='active', entry_plan_json=json.dumps({'entry_action': '等回踩', 'entry_price': 100.0}, ensure_ascii=False), ) self._insert_rec( symbol='OBS/USDT', action_status='持有', status='active', entry_plan_json=json.dumps({'entry_action': '继续观察', 'entry_price': 100.0}, ensure_ascii=False), ) self._insert_rec(symbol='INV/USDT', action_status='衰减', status='active') self._insert_rec(symbol='TP/USDT', action_status='止盈1', status='hit_tp1') self._insert_rec(symbol='STOP/USDT', action_status='止损', status='stopped_out') rows = altcoin_db.get_all_recommendations(limit=20, decision_only=True) mapping = {row['symbol']: row['execution_status'] for row in rows} self.assertEqual(set(mapping.keys()), {'TP/USDT', 'STOP/USDT'}) self.assertEqual(mapping['TP/USDT'], 'completed') self.assertEqual(mapping['STOP/USDT'], 'invalid') if __name__ == '__main__': unittest.main()