Spaces:
Sleeping
Sleeping
Thang6822 commited on
Commit Β·
d57df59
1
Parent(s): 0fc4a41
fix: TimesFM 2.5 working end-to-end
Browse files- .gitignore +1 -0
- backend/main.py +64 -50
- 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 |
-
#
|
|
|
|
| 4490 |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 4491 |
class TimesFMForecaster:
|
| 4492 |
"""Async wrapper around Google TimesFM 2.5 (200M, PyTorch).
|
| 4493 |
|
| 4494 |
-
|
| 4495 |
-
|
| 4496 |
-
|
| 4497 |
-
|
| 4498 |
-
|
| 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 |
-
#
|
| 4506 |
-
|
| 4507 |
-
|
| 4508 |
-
|
| 4509 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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.
|
| 4552 |
)
|
| 4553 |
try:
|
| 4554 |
-
|
| 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 |
-
#
|
| 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
|
| 4592 |
x_timestamp=None,
|
| 4593 |
y_timestamp=None,
|
| 4594 |
-
sample_count: int =
|
| 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 |
-
#
|
| 4615 |
point_forecast, quantile_forecast = await asyncio.to_thread(
|
| 4616 |
self._model.forecast,
|
| 4617 |
-
|
| 4618 |
-
|
| 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": "
|
| 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
|
| 3 |
COLOR 0B
|
|
|
|
| 4 |
|
| 5 |
-
echo
|
| 6 |
-
echo
|
| 7 |
-
echo
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
echo
|
| 14 |
-
echo
|
| 15 |
pause
|
| 16 |
-
exit /b
|
| 17 |
)
|
| 18 |
|
| 19 |
-
|
|
|
|
| 20 |
call venv\Scripts\activate
|
| 21 |
|
| 22 |
-
|
| 23 |
-
echo [
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|
|
|
|
| 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 |
)
|