-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
194 lines (153 loc) · 7.03 KB
/
Copy pathmain.py
File metadata and controls
194 lines (153 loc) · 7.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""
===================================
StockLens AI Analysis System - Web entrypoint
===================================
Single responsibility: boot the FastAPI web server (and the optional bot
stream clients). All analysis / backtest workflows are now triggered
through the web UI and REST API; this module no longer owns CLI flags
for ad-hoc analysis or scheduled batch jobs.
Usage:
python main.py # Start the web server on WEBUI_HOST:WEBUI_PORT
"""
import os
from stocklens.config.settings import setup_env
setup_env()
# NOTE: tickbridge used to receive a config provider via
# `tickbridge.config.settings.register_config_provider(...)` so the SDK
# could read STOCKLENS_* env vars directly. Now that tickbridge runs as
# an out-of-process daemon (P5 onward) it owns its own configuration —
# StockLens just reads ``TICKBRIDGE_URL`` to know where to talk to it
# (handled by ``stocklens.data_service.get_data_client``).
# Optional proxy bootstrap (kept for local development convenience).
if os.getenv("USE_PROXY", "false").lower() == "true":
proxy_host = os.getenv("PROXY_HOST", "127.0.0.1")
proxy_port = os.getenv("PROXY_PORT", "10809")
proxy_url = f"http://{proxy_host}:{proxy_port}"
os.environ["http_proxy"] = proxy_url
os.environ["https_proxy"] = proxy_url
import logging
import sys
import time
from datetime import datetime
from stocklens.config.settings import Config, get_config
from stocklens.utils.logging_setup import setup_logging
logger = logging.getLogger(__name__)
def start_api_server(host: str, port: int, config: Config) -> None:
"""Start the FastAPI service on a background thread.
Args:
host: bind address.
port: TCP port.
config: loaded application config (used for log level only).
"""
import sys
import threading
import uvicorn
# Pick the fastest available asyncio event loop. ``uvloop`` is a Cython
# implementation that's 2-4x faster than the stdlib loop on POSIX and
# is the official recommendation for production uvicorn deployments.
# On Windows uvloop is unavailable; uvicorn auto-falls-back to "asyncio".
loop_name = "auto"
if sys.platform != "win32":
try:
import uvloop # noqa: F401 presence check
loop_name = "uvloop"
except ImportError:
loop_name = "asyncio"
def run_server() -> None:
level_name = (config.log_level or "INFO").lower()
uvicorn.run(
"api.app:app",
host=host,
port=port,
log_level=level_name,
log_config=None,
loop=loop_name,
)
thread = threading.Thread(target=run_server, daemon=True)
thread.start()
logger.info(f"FastAPI service started: http://{host}:{port} (loop={loop_name})")
def start_bot_stream_clients(config: Config) -> None:
"""Start optional bot stream clients (DingTalk / Feishu) when enabled."""
if config.dingtalk_stream_enabled:
try:
from bot.platforms import DINGTALK_STREAM_AVAILABLE, start_dingtalk_stream_background
if DINGTALK_STREAM_AVAILABLE:
if start_dingtalk_stream_background():
logger.info("[Main] Dingtalk Stream client started in background.")
else:
logger.warning("[Main] Dingtalk Stream client failed to start.")
else:
logger.warning("[Main] Dingtalk Stream enabled but SDK is missing.")
logger.warning("[Main] Run: pip install dingtalk-stream")
except Exception as exc: # noqa: BLE001 — outermost guard for optional bot
logger.error(f"[Main] Failed to start Dingtalk Stream client: {exc}")
if getattr(config, "feishu_stream_enabled", False):
try:
from bot.platforms import FEISHU_SDK_AVAILABLE, start_feishu_stream_background
if FEISHU_SDK_AVAILABLE:
if start_feishu_stream_background():
logger.info("[Main] Feishu Stream client started in background.")
else:
logger.warning("[Main] Feishu Stream client failed to start.")
else:
logger.warning("[Main] Feishu Stream enabled but SDK is missing.")
logger.warning("[Main] Run: pip install lark-oapi")
except Exception as exc: # noqa: BLE001 — outermost guard for optional bot
logger.error(f"[Main] Failed to start Feishu Stream client: {exc}")
def _resolve_bind_address(config: Config) -> tuple[str, int]:
"""Pick host/port for the web server.
Priority: ``WEBUI_HOST`` / ``WEBUI_PORT`` env vars > defaults (``0.0.0.0:8000``).
The web server is the project's primary entrypoint, so we bind to all
interfaces by default (rather than the config object's ``127.0.0.1``
default, which exists for backward compatibility).
"""
host = os.getenv("WEBUI_HOST") or "0.0.0.0"
raw_port = os.getenv("WEBUI_PORT")
if raw_port:
try:
port = int(raw_port)
except ValueError:
logger.warning(f"Invalid WEBUI_PORT={raw_port!r}, falling back to config.")
port = int(getattr(config, "webui_port", 8000) or 8000)
else:
port = int(getattr(config, "webui_port", 8000) or 8000)
return host, port
def main() -> int:
"""Boot the web server. Blocks the main thread until interrupted."""
config = get_config()
setup_logging(log_prefix="stock_analysis", debug=False, log_dir=config.log_dir)
logger.info("=" * 60)
logger.info("StockLens AI Analysis System — Web mode")
logger.info(f"Boot time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
logger.info("=" * 60)
# Initialise the process-wide kvcache CacheManager (idempotent).
# Per-namespace tuning lives in stocklens/utils/cache_bootstrap.py.
from stocklens.utils.cache_bootstrap import configure as _configure_cache
_configure_cache()
# Validate core scalar env vars (port, log level, paths, ...) via
# pydantic-settings before any business code runs. Issues are logged
# as warnings; the legacy Config keeps loading regardless.
from stocklens.config.env_validator import validate_env_at_boot
for issue in validate_env_at_boot():
logger.warning("[env] %s", issue)
for warning in config.validate():
logger.warning(warning)
host, port = _resolve_bind_address(config)
try:
start_api_server(host=host, port=port, config=config)
except Exception as exc: # noqa: BLE001 — fail-fast logging before exit
logger.exception(f"Failed to start FastAPI service: {exc}")
return 1
start_bot_stream_clients(config)
logger.info(f"Web UI: http://{host}:{port}")
logger.info(f"API docs: http://{host}:{port}/docs")
logger.info("All analysis / backtest workflows are now triggered through the web UI.")
logger.info("Press Ctrl+C to exit...")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
logger.info("Interrupted by user, shutting down.")
return 0
if __name__ == "__main__":
sys.exit(main())