Thang6822 commited on
Commit
d57df59
Β·
1 Parent(s): 0fc4a41

fix: TimesFM 2.5 working end-to-end

Browse files
Files changed (3) hide show
  1. .gitignore +1 -0
  2. backend/main.py +64 -50
  3. run.bat +61 -17
.gitignore CHANGED
@@ -11,3 +11,4 @@ dist/
11
  scratch/
12
  *.spec
13
  data/
 
 
11
  scratch/
12
  *.spec
13
  data/
14
+ libs/
backend/main.py CHANGED
@@ -4486,33 +4486,35 @@ def market_status_now() -> List[Dict[str, Any]]:
4486
 
4487
 
4488
  # ──────────────────────────────────────────────────────────────────────────────
4489
- # Kronos Forecaster (identical to v3, with CLIP_DEFAULT fix retained)
 
4490
  # ──────────────────────────────────────────────────────────────────────────────
4491
  class TimesFMForecaster:
4492
  """Async wrapper around Google TimesFM 2.5 (200M, PyTorch).
4493
 
4494
- External contract identical to the old KronosForecaster / new TimesFMForecaster:
4495
- await forecaster.forecast(df, horizon=N, ...) -> {
4496
- p10, p50, p90, # np.ndarray of shape (horizon,)
4497
- model_name, context_length,
4498
- input_semantics, output_semantics
4499
- }
4500
  Input: OHLCV DataFrame; OHLC4 = (O+H+L+C)/4 computed internally.
4501
  Output: p10/p50/p90 future OHLC4 values for `horizon` future bars.
4502
  """
4503
- MAX_CONTEXT = 15_360 # TimesFM 2.5 supports up to 16 384 ctx
4504
  MODEL_HF_ID = "google/timesfm-2.5-200m-pytorch"
4505
- # quantile_forecast tensor indices (batch, horizon, 10)
4506
- # timesfm returns: [mean, q10, q20, q30, q40, q50, q60, q70, q80, q90]
4507
- _Q10 = 1
4508
- _Q50 = 5 # median
4509
- _Q90 = 9
 
 
4510
 
4511
  def __init__(self) -> None:
4512
  self._model: Optional[Any] = None
4513
  self._loaded = False
4514
  self._lock: Optional[asyncio.Lock] = None
4515
  self._predict_lock: Optional[asyncio.Lock] = None
 
4516
 
4517
  async def _get_lock(self) -> asyncio.Lock:
4518
  if self._lock is None:
@@ -4533,11 +4535,34 @@ class TimesFMForecaster:
4533
  if self._model is None:
4534
  return "not_loaded"
4535
  try:
4536
- params = list(self._model.parameters())
4537
- return str(params[0].device) if params else "cpu"
4538
  except Exception:
4539
  return "cpu"
4540
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4541
  async def _lazy_load(self) -> None:
4542
  if self._loaded:
4543
  return
@@ -4548,37 +4573,21 @@ class TimesFMForecaster:
4548
  if not TIMESFM_AVAILABLE:
4549
  raise HTTPException(
4550
  status_code=503,
4551
- detail="TimesFM not installed. Run: pip install timesfm[torch]",
4552
  )
4553
  try:
4554
- device = (
4555
- "cuda" if torch.cuda.is_available()
4556
- else "mps" if (hasattr(torch.backends, "mps") and torch.backends.mps.is_available())
4557
- else "cpu"
4558
- )
4559
- logger.info("[TimesFM] Loading %s on %s ...", self.MODEL_HF_ID, device)
4560
-
4561
  model = await asyncio.to_thread(
4562
  timesfm.TimesFM_2p5_200M_torch.from_pretrained,
4563
  self.MODEL_HF_ID,
4564
  )
4565
- # Apply inference config (must run after loading weights)
4566
- model.compile(
4567
- timesfm.ForecastConfig(
4568
- max_context=self.MAX_CONTEXT,
4569
- normalize_inputs=True,
4570
- use_continuous_quantile_head=True,
4571
- force_flip_invariance=True,
4572
- infer_is_positive=True,
4573
- fix_quantile_crossing=True,
4574
- )
4575
- )
4576
-
4577
  self._model = model
 
4578
  self._loaded = True
4579
  STARTUP_STATE["timesfm"]["loaded"] = True
4580
- STARTUP_STATE["timesfm"]["device"] = device
4581
- logger.info("[TimesFM] Ready on %s", device)
4582
  except Exception as ex:
4583
  STARTUP_STATE["timesfm"]["last_error"] = str(ex)
4584
  logger.error("[TimesFM] Init failed: %s", ex, exc_info=True)
@@ -4588,13 +4597,14 @@ class TimesFMForecaster:
4588
  self,
4589
  df: pd.DataFrame,
4590
  horizon: int,
4591
- # legacy kwargs from callers β€” absorbed and ignored
4592
  x_timestamp=None,
4593
  y_timestamp=None,
4594
- sample_count: int = 10,
4595
  ) -> Dict[str, Any]:
4596
  await self._lazy_load()
4597
  assert self._model is not None
 
4598
  try:
4599
  # Build single OHLC4 series: (O+H+L+C)/4
4600
  ohlc4 = (
@@ -4603,24 +4613,26 @@ class TimesFMForecaster:
4603
  .astype(np.float32)
4604
  .values
4605
  )
4606
-
4607
- # Clip to model context limit (take the most-recent N bars)
4608
  context_len = min(len(ohlc4), self.MAX_CONTEXT)
4609
  ohlc4_ctx = ohlc4[-context_len:]
4610
 
 
 
 
 
4611
  t0 = time.time()
4612
  predict_lock = await self._get_predict_lock()
4613
  async with predict_lock:
4614
- # model.forecast() expects list[np.ndarray]
4615
  point_forecast, quantile_forecast = await asyncio.to_thread(
4616
  self._model.forecast,
4617
- inputs=[ohlc4_ctx],
4618
- horizon=horizon,
4619
  )
4620
  elapsed = time.time() - t0
4621
  logger.info(
4622
- "[TimesFM] %.2fs | horizon=%d ctx=%d",
4623
- elapsed, horizon, context_len,
4624
  )
4625
 
4626
  if torch is not None and torch.cuda.is_available():
@@ -4628,9 +4640,9 @@ class TimesFMForecaster:
4628
 
4629
  # point_forecast: (1, horizon)
4630
  # quantile_forecast: (1, horizon, 10)
4631
- p10 = quantile_forecast[0, :, self._Q10].astype(float)
4632
- p50 = quantile_forecast[0, :, self._Q50].astype(float)
4633
- p90 = quantile_forecast[0, :, self._Q90].astype(float)
4634
 
4635
  return {
4636
  "p10": p10,
@@ -4648,7 +4660,7 @@ class TimesFMForecaster:
4648
  "volume_mode": "omitted",
4649
  "amount_mode": "omitted",
4650
  "adapter_mode": "timesfm_native",
4651
- "normalization": "timesfm_internal",
4652
  },
4653
  "output_semantics": {
4654
  "forecast_channel": "ohlc4",
@@ -4662,6 +4674,8 @@ class TimesFMForecaster:
4662
  raise HTTPException(status_code=500, detail=f"TimesFM prediction failed: {ex}")
4663
 
4664
 
 
 
4665
  forecaster = TimesFMForecaster()
4666
 
4667
 
 
4486
 
4487
 
4488
  # ──────────────────────────────────────────────────────────────────────────────
4489
+ # ──────────────────────────────────────────────────────────────────────────────
4490
+ # TimesFM 2.5 Forecaster
4491
  # ──────────────────────────────────────────────────────────────────────────────
4492
  class TimesFMForecaster:
4493
  """Async wrapper around Google TimesFM 2.5 (200M, PyTorch).
4494
 
4495
+ model.forecast(horizon, inputs) β†’ (point_forecast, quantile_forecast)
4496
+ point_forecast: np.ndarray (batch, horizon)
4497
+ quantile_forecast: np.ndarray (batch, horizon, 10)
4498
+ index mapping: [mean, q10, q20, q30, q40, q50, q60, q70, q80, q90]
4499
+
 
4500
  Input: OHLCV DataFrame; OHLC4 = (O+H+L+C)/4 computed internally.
4501
  Output: p10/p50/p90 future OHLC4 values for `horizon` future bars.
4502
  """
 
4503
  MODEL_HF_ID = "google/timesfm-2.5-200m-pytorch"
4504
+ # Keep context comfortably within 16384 limit; leaves room for any horizon
4505
+ MAX_CONTEXT = 8_192
4506
+ MAX_HORIZON = 300 # hard cap β€” matches API limit of horizon<=300
4507
+ # quantile axis indices in the (batch, horizon, 10) tensor
4508
+ _Q10 = 1 # 10th percentile
4509
+ _Q50 = 5 # 50th percentile (median)
4510
+ _Q90 = 9 # 90th percentile
4511
 
4512
  def __init__(self) -> None:
4513
  self._model: Optional[Any] = None
4514
  self._loaded = False
4515
  self._lock: Optional[asyncio.Lock] = None
4516
  self._predict_lock: Optional[asyncio.Lock] = None
4517
+ self._compiled_horizon: Optional[int] = None # last compiled max_horizon
4518
 
4519
  async def _get_lock(self) -> asyncio.Lock:
4520
  if self._lock is None:
 
4535
  if self._model is None:
4536
  return "not_loaded"
4537
  try:
4538
+ return str(self._model.model.device)
 
4539
  except Exception:
4540
  return "cpu"
4541
 
4542
+ def _compile(self, horizon: int) -> None:
4543
+ """(Re-)compile with a max_horizon that covers `horizon`."""
4544
+ import math
4545
+ output_patch = 128 # TimesFM 2.5 output_patch_len
4546
+ # max_horizon must be a multiple of output_patch_len
4547
+ max_h = math.ceil(horizon / output_patch) * output_patch
4548
+ max_h = max(max_h, output_patch)
4549
+ max_h = min(max_h, self.MAX_HORIZON)
4550
+ # context must be multiple of input_patch_len=32
4551
+ ctx = self.MAX_CONTEXT # already a multiple of 32
4552
+ self._model.compile(
4553
+ timesfm.ForecastConfig(
4554
+ max_context=ctx,
4555
+ max_horizon=max_h,
4556
+ normalize_inputs=True,
4557
+ use_continuous_quantile_head=True,
4558
+ force_flip_invariance=True,
4559
+ infer_is_positive=True,
4560
+ fix_quantile_crossing=True,
4561
+ )
4562
+ )
4563
+ self._compiled_horizon = max_h
4564
+ logger.info("[TimesFM] Compiled: ctx=%d max_horizon=%d", ctx, max_h)
4565
+
4566
  async def _lazy_load(self) -> None:
4567
  if self._loaded:
4568
  return
 
4573
  if not TIMESFM_AVAILABLE:
4574
  raise HTTPException(
4575
  status_code=503,
4576
+ detail="TimesFM not installed. Clone https://github.com/google-research/timesfm and pip install -e .[torch]",
4577
  )
4578
  try:
4579
+ logger.info("[TimesFM] Loading %s ...", self.MODEL_HF_ID)
 
 
 
 
 
 
4580
  model = await asyncio.to_thread(
4581
  timesfm.TimesFM_2p5_200M_torch.from_pretrained,
4582
  self.MODEL_HF_ID,
4583
  )
4584
+ # Compile once with the default max_horizon
 
 
 
 
 
 
 
 
 
 
 
4585
  self._model = model
4586
+ self._compile(self.MAX_HORIZON)
4587
  self._loaded = True
4588
  STARTUP_STATE["timesfm"]["loaded"] = True
4589
+ STARTUP_STATE["timesfm"]["device"] = self.device
4590
+ logger.info("[TimesFM] Ready on %s", self.device)
4591
  except Exception as ex:
4592
  STARTUP_STATE["timesfm"]["last_error"] = str(ex)
4593
  logger.error("[TimesFM] Init failed: %s", ex, exc_info=True)
 
4597
  self,
4598
  df: pd.DataFrame,
4599
  horizon: int,
4600
+ # legacy kwargs β€” accepted and ignored for API compatibility
4601
  x_timestamp=None,
4602
  y_timestamp=None,
4603
+ sample_count: int = 0,
4604
  ) -> Dict[str, Any]:
4605
  await self._lazy_load()
4606
  assert self._model is not None
4607
+
4608
  try:
4609
  # Build single OHLC4 series: (O+H+L+C)/4
4610
  ohlc4 = (
 
4613
  .astype(np.float32)
4614
  .values
4615
  )
 
 
4616
  context_len = min(len(ohlc4), self.MAX_CONTEXT)
4617
  ohlc4_ctx = ohlc4[-context_len:]
4618
 
4619
+ # Re-compile if horizon exceeds current compiled max_horizon
4620
+ if self._compiled_horizon is None or horizon > self._compiled_horizon:
4621
+ self._compile(horizon)
4622
+
4623
  t0 = time.time()
4624
  predict_lock = await self._get_predict_lock()
4625
  async with predict_lock:
4626
+ # TimesFM API: forecast(horizon: int, inputs: list[np.ndarray])
4627
  point_forecast, quantile_forecast = await asyncio.to_thread(
4628
  self._model.forecast,
4629
+ horizon,
4630
+ [ohlc4_ctx],
4631
  )
4632
  elapsed = time.time() - t0
4633
  logger.info(
4634
+ "[TimesFM] %.2fs | horizon=%d ctx=%d device=%s",
4635
+ elapsed, horizon, context_len, self.device,
4636
  )
4637
 
4638
  if torch is not None and torch.cuda.is_available():
 
4640
 
4641
  # point_forecast: (1, horizon)
4642
  # quantile_forecast: (1, horizon, 10)
4643
+ p10 = quantile_forecast[0, :horizon, self._Q10].astype(float)
4644
+ p50 = quantile_forecast[0, :horizon, self._Q50].astype(float)
4645
+ p90 = quantile_forecast[0, :horizon, self._Q90].astype(float)
4646
 
4647
  return {
4648
  "p10": p10,
 
4660
  "volume_mode": "omitted",
4661
  "amount_mode": "omitted",
4662
  "adapter_mode": "timesfm_native",
4663
+ "normalization": "timesfm_internal_revin",
4664
  },
4665
  "output_semantics": {
4666
  "forecast_channel": "ohlc4",
 
4674
  raise HTTPException(status_code=500, detail=f"TimesFM prediction failed: {ex}")
4675
 
4676
 
4677
+
4678
+
4679
  forecaster = TimesFMForecaster()
4680
 
4681
 
run.bat CHANGED
@@ -1,33 +1,77 @@
1
  @echo off
2
- TITLE Kronos AI Trading Terminal v6.0
3
  COLOR 0B
 
4
 
5
- echo ====================================================
6
- echo KRONOS AI TRADING TERMINAL - STARTUP
7
- echo ====================================================
 
 
 
8
 
9
- echo [1/3] Preparing launcher...
10
-
11
- :: Check if virtual environment exists
12
- if not exist "venv\" (
13
- echo [ERROR] Virtual environment 'venv' not found.
14
- echo Please create it using: python -m venv venv
15
  pause
16
- exit /b
17
  )
18
 
19
- echo [2/3] Activating Virtual Environment...
 
20
  call venv\Scripts\activate
21
 
22
- echo [3/3] Launching AI Trading Terminal...
23
- echo [INFO] Application will open in your browser automatically.
24
- echo [INFO] Press CTRL+C in this window to stop the server.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- :: Run the app with dynamic port selection
27
  python -m backend.launcher
28
 
29
  if %ERRORLEVEL% neq 0 (
30
  echo.
31
- echo [ERROR] Backend failed to start.
 
32
  pause
33
  )
 
1
  @echo off
2
+ TITLE Kronos AI Trading Terminal
3
  COLOR 0B
4
+ chcp 65001 >nul 2>&1
5
 
6
+ echo.
7
+ echo ╔══════════════════════════════════════════════════════╗
8
+ echo β•‘ KRONOS AI TRADING TERMINAL β€” STARTUP β•‘
9
+ echo β•‘ Powered by Google TimesFM 2.5 (200M params) β•‘
10
+ echo β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
11
+ echo.
12
 
13
+ :: ── [1/4] Check virtual environment ─────────────────────────────────────────
14
+ echo [1/4] Checking virtual environment...
15
+ if not exist "venv\Scripts\python.exe" (
16
+ echo [ERROR] Virtual environment not found.
17
+ echo Run: python -m venv venv
18
+ echo Then: venv\Scripts\pip install -r requirements.txt
19
  pause
20
+ exit /b 1
21
  )
22
 
23
+ :: ── [2/4] Activate venv ──────────────────────────────────────────────────────
24
+ echo [2/4] Activating virtual environment...
25
  call venv\Scripts\activate
26
 
27
+ :: ── [3/4] Ensure TimesFM is installed ───────────────────────────────────────
28
+ echo [3/4] Checking TimesFM dependency...
29
+ venv\Scripts\python.exe -c "import timesfm; timesfm.TimesFM_2p5_200M_torch" >nul 2>&1
30
+ if %ERRORLEVEL% neq 0 (
31
+ echo [INFO] TimesFM not found. Installing from local clone...
32
+ if exist "libs\timesfm\pyproject.toml" (
33
+ echo [INFO] Installing from libs\timesfm ...
34
+ venv\Scripts\python.exe -m pip install -e "libs\timesfm[torch]" --quiet
35
+ if %ERRORLEVEL% neq 0 (
36
+ echo [ERROR] TimesFM install failed from libs\timesfm.
37
+ pause
38
+ exit /b 1
39
+ )
40
+ ) else (
41
+ echo [INFO] Cloning TimesFM from GitHub...
42
+ git clone --depth=1 https://github.com/google-research/timesfm.git libs\timesfm
43
+ if %ERRORLEVEL% neq 0 (
44
+ echo [ERROR] git clone failed. Check your internet connection.
45
+ pause
46
+ exit /b 1
47
+ )
48
+ echo [INFO] Installing TimesFM...
49
+ venv\Scripts\python.exe -m pip install -e "libs\timesfm[torch]" --quiet
50
+ if %ERRORLEVEL% neq 0 (
51
+ echo [ERROR] TimesFM install failed.
52
+ pause
53
+ exit /b 1
54
+ )
55
+ )
56
+ echo [OK] TimesFM installed successfully.
57
+ ) else (
58
+ echo [OK] TimesFM is ready.
59
+ )
60
+
61
+ :: ── [4/4] Launch server ──────────────────────────────────────────────────────
62
+ echo [4/4] Launching Kronos AI Trading Terminal...
63
+ echo.
64
+ echo [INFO] Server will start at http://127.0.0.1:8000
65
+ echo [INFO] Browser will open automatically in ~3 seconds.
66
+ echo [INFO] TimesFM model downloads on FIRST forecast request (~800MB).
67
+ echo [INFO] Press CTRL+C to stop the server.
68
+ echo.
69
 
 
70
  python -m backend.launcher
71
 
72
  if %ERRORLEVEL% neq 0 (
73
  echo.
74
+ echo [ERROR] Server exited with an error.
75
+ echo Check the log above for details.
76
  pause
77
  )