Coverage for src / monte_neo / cli / menu / data.py: 0%

78 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-28 16:27 +0200

1"""Data download workflow.""" 

2 

3from __future__ import annotations 

4 

5from datetime import datetime, timedelta 

6from typing import TYPE_CHECKING 

7 

8import questionary 

9from rich.console import Console 

10from rich.panel import Panel 

11 

12from monte_neo.cli.menu.symbol_selector import SymbolSelector 

13from monte_neo.cli.styles import CUSTOM_STYLE 

14from monte_neo.data.downloader import BinanceDownloader 

15from monte_neo.utils.logger import get_logger 

16 

17if TYPE_CHECKING: 

18 from monte_neo.cli.menu.main import InteractiveMenu 

19 

20logger = get_logger(__name__) 

21console = Console() 

22 

23 

24def download_data_workflow(menu: InteractiveMenu) -> None: 

25 """Download market data workflow.""" 

26 console.print("\n[bold cyan]📊 Download Market Data[/]\n") 

27 

28 # Select symbol 

29 if not menu._cached_symbols: 

30 try: 

31 menu.progress.start(100, "Fetching available symbols from Binance...") 

32 downloader = BinanceDownloader() 

33 symbols = downloader.get_available_symbols() 

34 

35 # Prioritize USDT pairs 

36 usdt_pairs = [s for s in symbols if s.endswith("USDT")] 

37 other_pairs = [s for s in symbols if not s.endswith("USDT")] 

38 menu._cached_symbols = sorted(usdt_pairs) + sorted(other_pairs) 

39 

40 menu.progress.update(100, 100, "Done") 

41 menu.progress.stop() 

42 except Exception as e: 

43 logger.error(f"Error fetching symbols: {e}") 

44 menu.progress.stop() 

45 console.print(f"[red]Error fetching symbols: {e}. Using default.[/]") 

46 menu._cached_symbols = [menu._selected_symbol] 

47 

48 # Use the new multi-column searchable selector 

49 selector = SymbolSelector(menu._cached_symbols, style=CUSTOM_STYLE) 

50 symbol = selector.ask() 

51 

52 if not symbol: 

53 return 

54 

55 # Select timeframe 

56 timeframe = questionary.select( 

57 "Timeframe:", 

58 choices=["1m", "5m", "15m", "30m", "1h", "4h", "1d"], 

59 default=menu._selected_timeframe, 

60 style=CUSTOM_STYLE, 

61 ).ask() 

62 

63 if not timeframe: 

64 return 

65 

66 _process_download(menu, symbol, timeframe) 

67 

68 

69def _process_download(menu: InteractiveMenu, symbol: str, timeframe: str) -> None: 

70 # Check if exists 

71 info = menu.storage.get_info(symbol, timeframe) 

72 if info: 

73 console.print(f"\n[yellow]⚠ Data for {symbol} {timeframe} already exists.[/]") 

74 

75 start = info.get('start_date') 

76 end = info.get('end_date') 

77 

78 # Format dates if they exist 

79 start_str = str(start).split(".")[0] if start else "Unknown" 

80 end_str = str(end).split(".")[0] if end else "Unknown" 

81 range_str = f"{start_str} ➜ {end_str}" 

82 

83 console.print(Panel( 

84 f"Rows: {info.get('rows', 0):,}\n" 

85 f"Range: {range_str}\n" 

86 f"Size: {info.get('size_mb', 0):.2f} MB", 

87 title="Existing Data", 

88 border_style="yellow" 

89 )) 

90 

91 if not questionary.confirm( 

92 "Overwrite existing data?", default=False, style=CUSTOM_STYLE 

93 ).ask(): 

94 return 

95 

96 # Select period 

97 choices = [ 

98 {"name": "30 days", "value": 30}, 

99 {"name": "90 days", "value": 90}, 

100 {"name": "180 days", "value": 180}, 

101 {"name": "365 days (1 year)", "value": 365}, 

102 {"name": "730 days (2 years)", "value": 730}, 

103 {"name": "1095 days (3 years)", "value": 1095}, 

104 {"name": "1825 days (5 years)", "value": 1825}, 

105 {"name": "3650 days (10 years)", "value": 3650}, 

106 {"name": "Custom days...", "value": "custom"}, 

107 ] 

108 

109 days_val = questionary.select( 

110 "Historical period:", 

111 choices=choices, 

112 style=CUSTOM_STYLE, 

113 ).ask() 

114 

115 if not days_val: 

116 return 

117 

118 if days_val == "custom": 

119 days_str = questionary.text("Enter number of days:").ask() 

120 try: 

121 days = int(days_str) 

122 except (ValueError, TypeError): 

123 console.print("[red]Invalid number of days.[/]") 

124 return 

125 else: 

126 days = days_val 

127 

128 menu._selected_symbol = symbol 

129 menu._selected_timeframe = timeframe 

130 

131 try: 

132 downloader = BinanceDownloader() 

133 end_date = datetime.now() 

134 start_date = end_date - timedelta(days=days) 

135 

136 menu.progress.start(100, f"Downloading {symbol}...") 

137 data = downloader.download( 

138 symbol, timeframe, start_date, end_date, menu.progress.update 

139 ) 

140 menu.progress.update(100, 100, "Done") 

141 menu.progress.stop() 

142 

143 menu.storage.save(data, symbol, timeframe) 

144 

145 console.print(f"[green]✓ Downloaded {len(data)} candles[/]") 

146 console.print(f"[dim]Saved to: data/raw/{symbol}_{timeframe}.parquet[/]\n") 

147 

148 except Exception as e: 

149 menu.progress.stop() 

150 console.print(f"[red]✗ Download failed: {e}[/]\n") 

151 logger.error(f"Download failed for {symbol}: {e}")