Commit 6132f5c0 authored by xeron56's avatar xeron56

Add CUDA device compatibility checks to TimesFM backend

- Implemented a function to check if the current CUDA device is supported by the installed PyTorch version.
- Added warnings for unsupported CUDA devices, informing users that the CPU will be used instead.
- Updated the TimesFMBackend class to utilize the new compatibility checks and handle GPU names appropriately.
- Introduced unit tests to validate the behavior of the CUDA compatibility function with various simulated CUDA environments.
parent 02b92e82
...@@ -86,7 +86,7 @@ DSE_PASSWORD= ...@@ -86,7 +86,7 @@ DSE_PASSWORD=
#TRADINGAGENTS_SOCIAL_MEDIA_ENABLED=false #TRADINGAGENTS_SOCIAL_MEDIA_ENABLED=false
#TRADINGAGENTS_MACRO_DATA_ENABLED=false #TRADINGAGENTS_MACRO_DATA_ENABLED=false
#TRADINGAGENTS_PREDICTION_MARKETS_ENABLED=false #TRADINGAGENTS_PREDICTION_MARKETS_ENABLED=false
#TRADINGAGENTS_DSE_GATEWAY_URL=https://gateway.dohasecurities.com.bd # TRADINGAGENTS_DSE_GATEWAY_URL=https://gateway.dohasecurities.com.bd
#TRADINGAGENTS_DSE_REQUEST_TIMEOUT=30 #TRADINGAGENTS_DSE_REQUEST_TIMEOUT=30
#TRADINGAGENTS_DSE_VERIFY_SSL=true #TRADINGAGENTS_DSE_VERIFY_SSL=true
#TRADINGAGENTS_DSE_BENCHMARK_TICKER=DSEX #TRADINGAGENTS_DSE_BENCHMARK_TICKER=DSEX
......
This diff is collapsed.
...@@ -79,24 +79,88 @@ ...@@ -79,24 +79,88 @@
<section class="chart-card"> <section class="chart-card">
<header> <header>
<div> <div>
<p class="eyebrow">HISTORICAL BACKTEST + FORWARD PATH</p> <p class="eyebrow">{{ chartMode() === 'future' ? 'FORWARD FORECAST' : 'HISTORICAL BACKTEST' }}</p>
<h2>Actual candles against {{ report.model.name }}</h2> <h2>
{{ chartMode() === 'future'
? 'Recent actual candles + ' + report.model.name + ' future'
: 'Hidden actuals against ' + report.model.name + ' backtest' }}
</h2>
</div>
<div class="chart-controls">
<div class="chart-mode-switch" role="group" aria-label="Forecast chart view">
<button
type="button"
[attr.aria-pressed]="chartMode() === 'future'"
[class.active]="chartMode() === 'future'"
(click)="selectChartMode('future')"
>Future forecast</button>
<button
type="button"
[attr.aria-pressed]="chartMode() === 'backtest'"
[class.active]="chartMode() === 'backtest'"
(click)="selectChartMode('backtest')"
>Backtest</button>
</div> </div>
<div class="legend" aria-label="Chart legend"> <div class="legend" aria-label="Chart legend">
@if (chartMode() === 'future') {
<span><i class="candle"></i>Actual OHLC</span> <span><i class="candle"></i>Actual OHLC</span>
<span><i class="backtest"></i>Held-out prediction</span>
<span><i class="future"></i>Future forecast</span> <span><i class="future"></i>Future forecast</span>
@if (showFutureInterval()) {
<span><i class="interval future-interval"></i>Q10–Q90</span>
}
<span><i class="live"></i>Live actual</span> <span><i class="live"></i>Live actual</span>
} @else {
<span><i class="actual-line"></i>Actual close</span>
<span><i class="backtest"></i>Backtest prediction</span>
@if (showBacktestInterval()) {
<span><i class="interval"></i>Q10–Q90</span>
}
}
</div>
</div> </div>
</header> </header>
<div #predictionChart class="prediction-chart" role="img" [attr.aria-label]="'DSE actual and ' + report.model.name + ' forecast chart'"></div> <div class="chart-context-note">
<p>
@if (chartMode() === 'future') {
Uses the latest real prices. The separate historical backtest is excluded from this scale.
} @else {
The model uses the first {{ report.data.context_points | number }} bars as context, then predicts all
{{ report.data.holdout_points | number }} hidden bars. Compare predicted and actual closing prices directly.
}
</p>
<button
type="button"
class="interval-toggle"
[class.active]="intervalVisible()"
[attr.aria-pressed]="intervalVisible()"
(click)="toggleInterval()"
>{{ intervalVisible() ? 'Hide' : 'Show' }} 80% uncertainty</button>
</div>
<div
#predictionChart
id="prediction-chart"
class="prediction-chart"
role="img"
[attr.aria-label]="chartMode() === 'future'
? 'Recent DSE actual prices and ' + report.model.name + ' future forecast chart'
: 'DSE hidden actual prices and ' + report.model.name + ' historical backtest chart'"
></div>
<footer> <footer>
<span>Context</span> @if (chartMode() === 'future') {
<span>Recent actual</span>
<strong>{{ futureHistoryStart(report) | date: 'mediumDate' }} → {{ report.data.last_timestamp | date: 'mediumDate' }}</strong>
<span>Forecast window</span>
<strong>{{ report.future[0].time | date: 'mediumDate' }} → {{ report.future[report.future.length - 1].time | date: 'mediumDate' }}</strong>
<span>Horizon</span>
<strong>{{ report.future.length }} bars</strong>
} @else {
<span>Model context</span>
<strong>{{ report.data.first_timestamp | date: 'mediumDate' }} → {{ report.history[report.data.context_points - 1].time | date: 'mediumDate' }}</strong> <strong>{{ report.data.first_timestamp | date: 'mediumDate' }} → {{ report.history[report.data.context_points - 1].time | date: 'mediumDate' }}</strong>
<span>Hidden test</span> <span>Line comparison</span>
<strong>{{ report.backtest[0].time | date: 'mediumDate' }} → {{ report.data.last_timestamp | date: 'mediumDate' }}</strong> <strong>{{ report.backtest[0].time | date: 'mediumDate' }} → {{ report.data.last_timestamp | date: 'mediumDate' }}</strong>
<span>Next forecast</span> <span>Scored bars</span>
<strong>{{ report.future.length }} bars</strong> <strong>{{ report.backtest.length }}</strong>
}
</footer> </footer>
</section> </section>
......
...@@ -179,8 +179,73 @@ ...@@ -179,8 +179,73 @@
.legend span { display: inline-flex; align-items: center; gap: 0.35rem; } .legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
.legend i { width: 1.25rem; height: 0.16rem; border-radius: 1rem; background: #34d399; } .legend i { width: 1.25rem; height: 0.16rem; border-radius: 1rem; background: #34d399; }
.legend .backtest { background: var(--amber); } .legend .backtest { background: var(--amber); }
.legend .actual-line { background: #34d399; }
.legend .future { background: var(--violet); } .legend .future { background: var(--violet); }
.legend .live { background: var(--cyan); } .legend .live { background: var(--cyan); }
.legend .interval {
height: 0;
border-top: 2px dashed rgb(245 158 11 / 65%);
border-radius: 0;
background: transparent;
}
.legend .future-interval { border-color: rgb(167 139 250 / 70%); }
.chart-controls { display: grid; justify-items: end; gap: 0.7rem; }
.chart-mode-switch {
display: inline-grid;
grid-template-columns: repeat(2, 1fr);
padding: 0.2rem;
border: 1px solid #334155;
border-radius: 0.7rem;
background: #0b1220;
}
.chart-mode-switch button,
.interval-toggle {
border: 0;
color: var(--muted);
background: transparent;
font: inherit;
font-size: 0.76rem;
font-weight: 700;
cursor: pointer;
}
.chart-mode-switch button {
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
}
.chart-mode-switch button.active {
color: #f8fafc;
background: #253146;
box-shadow: 0 0 0 1px rgb(148 163 184 / 12%);
}
.chart-mode-switch button:focus-visible,
.interval-toggle:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
.chart-context-note {
display: flex;
min-height: 3rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 1.25rem;
border-bottom: 1px solid var(--line);
color: var(--muted);
background: #0d1420;
font-size: 0.76rem;
}
.chart-context-note p { margin: 0; line-height: 1.5; }
.interval-toggle {
flex: 0 0 auto;
padding: 0.45rem 0.7rem;
border: 1px solid #475569;
border-radius: 0.55rem;
}
.interval-toggle.active {
border-color: #7c3aed;
color: #ddd6fe;
background: rgb(124 58 237 / 14%);
}
.prediction-chart { width: 100%; min-height: 520px; } .prediction-chart { width: 100%; min-height: 520px; }
.chart-card > footer { .chart-card > footer {
display: grid; display: grid;
...@@ -239,7 +304,9 @@ td { color: #dbe5f3; font-size: 0.82rem; } ...@@ -239,7 +304,9 @@ td { color: #dbe5f3; font-size: 0.82rem; }
.accuracy-card { flex-basis: auto; } .accuracy-card { flex-basis: auto; }
.metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.chart-card > header, .table-card > header, .prediction-footer { align-items: flex-start; flex-direction: column; } .chart-card > header, .table-card > header, .prediction-footer { align-items: flex-start; flex-direction: column; }
.chart-controls { justify-items: start; }
.legend { justify-content: flex-start; } .legend { justify-content: flex-start; }
.chart-context-note { align-items: flex-start; flex-direction: column; }
.prediction-chart { min-height: 420px; } .prediction-chart { min-height: 420px; }
.live-comparison { grid-template-columns: 1fr; } .live-comparison { grid-template-columns: 1fr; }
.table-card > header > span { text-align: left; } .table-card > header > span { text-align: left; }
......
...@@ -32,6 +32,10 @@ import { ...@@ -32,6 +32,10 @@ import {
} from './timesfm-prediction.model'; } from './timesfm-prediction.model';
import { TimesFmPredictionService } from './timesfm-prediction.service'; import { TimesFmPredictionService } from './timesfm-prediction.service';
type PredictionChartMode = 'future' | 'backtest';
const FUTURE_HISTORY_BARS = 30;
@Component({ @Component({
selector: 'app-timesfm-prediction', selector: 'app-timesfm-prediction',
standalone: true, standalone: true,
...@@ -51,13 +55,15 @@ export class TimesFmPredictionComponent ...@@ -51,13 +55,15 @@ export class TimesFmPredictionComponent
); );
readonly livePrice = signal<number | null>(null); readonly livePrice = signal<number | null>(null);
readonly liveMatches = signal<LiveForecastMatch[]>([]); readonly liveMatches = signal<LiveForecastMatch[]>([]);
readonly chartMode = signal<PredictionChartMode>('future');
readonly showFutureInterval = signal(false);
readonly showBacktestInterval = signal(false);
readonly latestLiveMatch = computed(() => this.liveMatches().at(-1) ?? null); readonly latestLiveMatch = computed(() => this.liveMatches().at(-1) ?? null);
readonly recentBacktest = computed(() => this.result()?.backtest.slice(-12) ?? []); readonly recentBacktest = computed(() => this.result()?.backtest.slice(-12) ?? []);
readonly subscriptions: Subscription[] = []; readonly subscriptions: Subscription[] = [];
private chart: IChartApi | null = null; private chart: IChartApi | null = null;
private liveSeries: ISeriesApi<'Line'> | null = null; private liveSeries: ISeriesApi<'Line'> | null = null;
private resizeObserver: ResizeObserver | null = null;
private stompClient: Client | null = null; private stompClient: Client | null = null;
private chartReady = false; private chartReady = false;
...@@ -107,7 +113,6 @@ export class TimesFmPredictionComponent ...@@ -107,7 +113,6 @@ export class TimesFmPredictionComponent
ngOnDestroy(): void { ngOnDestroy(): void {
this.subscriptions.forEach((subscription) => subscription.unsubscribe()); this.subscriptions.forEach((subscription) => subscription.unsubscribe());
void this.stompClient?.deactivate(); void this.stompClient?.deactivate();
this.resizeObserver?.disconnect();
this.chart?.remove(); this.chart?.remove();
} }
...@@ -133,13 +138,38 @@ export class TimesFmPredictionComponent ...@@ -133,13 +138,38 @@ export class TimesFmPredictionComponent
return point.time; return point.time;
} }
selectChartMode(mode: PredictionChartMode): void {
if (this.chartMode() === mode) return;
this.chartMode.set(mode);
this.renderChart();
}
toggleInterval(): void {
if (this.chartMode() === 'future') {
this.showFutureInterval.update((visible) => !visible);
} else {
this.showBacktestInterval.update((visible) => !visible);
}
this.renderChart();
}
intervalVisible(): boolean {
return this.chartMode() === 'future'
? this.showFutureInterval()
: this.showBacktestInterval();
}
futureHistoryStart(result: TimesFmPredictionResult): string {
return result.history.at(-FUTURE_HISTORY_BARS)?.time ?? result.data.first_timestamp;
}
private renderChart(): void { private renderChart(): void {
const result = this.result(); const result = this.result();
const container = this.chartContainer?.nativeElement; const container = this.chartContainer?.nativeElement;
if (!result || !container || !this.chartReady) return; if (!result || !container || !this.chartReady) return;
this.resizeObserver?.disconnect();
this.chart?.remove(); this.chart?.remove();
this.liveSeries = null;
this.chart = createChart(container, { this.chart = createChart(container, {
autoSize: true, autoSize: true,
height: 520, height: 520,
...@@ -164,15 +194,24 @@ export class TimesFmPredictionComponent ...@@ -164,15 +194,24 @@ export class TimesFmPredictionComponent
}, },
}); });
const candles = this.chart.addCandlestickSeries({ if (this.chartMode() === 'future') {
this.renderFutureSeries(result);
} else {
this.renderBacktestSeries(result);
}
this.chart.timeScale().fitContent();
}
private renderFutureSeries(result: TimesFmPredictionResult): void {
const candles = this.chart?.addCandlestickSeries({
upColor: '#34d399', upColor: '#34d399',
downColor: '#f87171', downColor: '#f87171',
borderVisible: false, borderVisible: false,
wickUpColor: '#34d399', wickUpColor: '#34d399',
wickDownColor: '#f87171', wickDownColor: '#f87171',
}); });
candles.setData( candles?.setData(
result.history.map((point) => ({ result.history.slice(-FUTURE_HISTORY_BARS).map((point) => ({
time: this.chartTime(point.time), time: this.chartTime(point.time),
open: point.open, open: point.open,
high: point.high, high: point.high,
...@@ -181,49 +220,7 @@ export class TimesFmPredictionComponent ...@@ -181,49 +220,7 @@ export class TimesFmPredictionComponent
})), })),
); );
const predicted = this.chart.addLineSeries({ const future = this.chart?.addLineSeries({
color: '#f59e0b',
lineWidth: 2,
title: 'Held-out prediction',
priceLineVisible: false,
});
predicted.setData(
result.backtest.map((point) => ({
time: this.chartTime(point.time),
value: point.predicted,
})),
);
const lower = this.chart.addLineSeries({
color: 'rgba(245, 158, 11, 0.48)',
lineWidth: 1,
lineStyle: LineStyle.Dashed,
title: 'Q10',
priceLineVisible: false,
lastValueVisible: false,
});
const upper = this.chart.addLineSeries({
color: 'rgba(245, 158, 11, 0.48)',
lineWidth: 1,
lineStyle: LineStyle.Dashed,
title: 'Q90',
priceLineVisible: false,
lastValueVisible: false,
});
lower.setData(
result.backtest.map((point) => ({
time: this.chartTime(point.time),
value: point.q10,
})),
);
upper.setData(
result.backtest.map((point) => ({
time: this.chartTime(point.time),
value: point.q90,
})),
);
const future = this.chart.addLineSeries({
color: '#a78bfa', color: '#a78bfa',
lineWidth: 3, lineWidth: 3,
lineStyle: LineStyle.Dashed, lineStyle: LineStyle.Dashed,
...@@ -231,7 +228,7 @@ export class TimesFmPredictionComponent ...@@ -231,7 +228,7 @@ export class TimesFmPredictionComponent
priceLineVisible: false, priceLineVisible: false,
}); });
const futureStart = result.history.at(-1); const futureStart = result.history.at(-1);
future.setData([ future?.setData([
...(futureStart ...(futureStart
? [ ? [
{ {
...@@ -246,16 +243,91 @@ export class TimesFmPredictionComponent ...@@ -246,16 +243,91 @@ export class TimesFmPredictionComponent
})), })),
]); ]);
this.liveSeries = this.chart.addLineSeries({ if (this.showFutureInterval()) {
this.addIntervalSeries(result.future, '#a78bfa');
}
this.liveSeries =
this.chart?.addLineSeries({
color: '#22d3ee', color: '#22d3ee',
lineWidth: 2, lineWidth: 2,
title: 'Live actual', title: 'Live actual',
priceLineVisible: true, priceLineVisible: true,
lastValueVisible: true, lastValueVisible: true,
}) ?? null;
const latestMatch = this.latestLiveMatch();
if (latestMatch) {
this.liveSeries?.update({
time: this.chartTime(latestMatch.targetTime),
value: latestMatch.actual,
}); });
this.chart.timeScale().fitContent(); }
this.resizeObserver = new ResizeObserver(() => this.chart?.timeScale().fitContent()); }
this.resizeObserver.observe(container);
private renderBacktestSeries(result: TimesFmPredictionResult): void {
const contextEnd = result.history[result.data.context_points - 1];
const anchor = contextEnd
? [{ time: this.chartTime(contextEnd.time), value: contextEnd.close }]
: [];
const actual = this.chart?.addLineSeries({
color: '#34d399',
lineWidth: 3,
title: 'Actual close',
priceLineVisible: false,
});
actual?.setData([
...anchor,
...result.backtest.map((point) => ({
time: this.chartTime(point.time),
value: point.actual,
})),
]);
const predicted = this.chart?.addLineSeries({
color: '#f59e0b',
lineWidth: 3,
lineStyle: LineStyle.Dashed,
title: 'Backtest prediction',
priceLineVisible: false,
});
predicted?.setData([
...anchor,
...result.backtest.map((point) => ({
time: this.chartTime(point.time),
value: point.predicted,
})),
]);
if (this.showBacktestInterval()) {
this.addIntervalSeries(result.backtest, '#f59e0b');
}
}
private addIntervalSeries(
points: Array<{ time: string; q10: number; q90: number }>,
color: string,
): void {
const intervalOptions = {
color,
lineWidth: 1 as const,
lineStyle: LineStyle.Dashed,
priceLineVisible: false,
lastValueVisible: false,
};
const lower = this.chart?.addLineSeries({ ...intervalOptions, title: 'Q10' });
const upper = this.chart?.addLineSeries({ ...intervalOptions, title: 'Q90' });
lower?.setData(
points.map((point) => ({
time: this.chartTime(point.time),
value: point.q10,
})),
);
upper?.setData(
points.map((point) => ({
time: this.chartTime(point.time),
value: point.q90,
})),
);
} }
private connectLiveFeed(result: TimesFmPredictionResult): void { private connectLiveFeed(result: TimesFmPredictionResult): void {
......
This diff is collapsed.
This diff is collapsed.
...@@ -8,6 +8,7 @@ import io ...@@ -8,6 +8,7 @@ import io
import math import math
import os import os
import sys import sys
import warnings
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Any from typing import Any
...@@ -82,6 +83,44 @@ def _load_archived_timesfm() -> ModuleType: ...@@ -82,6 +83,44 @@ def _load_archived_timesfm() -> ModuleType:
return module return module
def _cuda_device_is_supported(torch: ModuleType, device_index: int = 0) -> bool:
"""Return whether this PyTorch build contains code usable by the CUDA device."""
if not torch.cuda.is_available():
return False
compiled_arches = torch.cuda.get_arch_list()
if not compiled_arches:
# ROCm builds and some custom CUDA builds do not expose an architecture
# list. In that case availability is the best signal PyTorch provides.
return True
device_major, device_minor = torch.cuda.get_device_capability(device_index)
device_arch = device_major * 10 + device_minor
sm_arches: list[int] = []
ptx_arches: list[int] = []
for arch in compiled_arches:
kind, separator, version = arch.partition("_")
if not separator:
continue
numeric_version = "".join(character for character in version if character.isdigit())
if not numeric_version:
continue
if kind == "sm":
sm_arches.append(int(numeric_version))
elif kind == "compute":
ptx_arches.append(int(numeric_version))
# CUDA cubins are compatible with later minor revisions in the same major
# architecture. PTX can be JIT-compiled for newer CUDA architectures.
if any(
compiled // 10 == device_arch // 10 and compiled <= device_arch
for compiled in sm_arches
):
return True
return any(compiled <= device_arch for compiled in ptx_arches)
class TimesFMBackend: class TimesFMBackend:
"""One TimesFM 2.0 500M model instance configured for one prediction run.""" """One TimesFM 2.0 500M model instance configured for one prediction run."""
...@@ -111,7 +150,24 @@ class TimesFMBackend: ...@@ -111,7 +150,24 @@ class TimesFMBackend:
) from exc ) from exc
timesfm = _load_archived_timesfm() timesfm = _load_archived_timesfm()
use_cuda = torch.cuda.is_available() gpu_name = None
with warnings.catch_warnings():
# PyTorch emits several long compatibility warnings while querying
# an unsupported device. Replace them with one actionable warning.
warnings.simplefilter("ignore", UserWarning)
cuda_available = torch.cuda.is_available()
if cuda_available:
gpu_name = torch.cuda.get_device_name(0)
use_cuda = _cuda_device_is_supported(torch)
capability = torch.cuda.get_device_capability(0) if cuda_available else None
if cuda_available and not use_cuda:
device_arch = f"sm_{capability[0]}{capability[1]}" if capability else "unknown"
warnings.warn(
f"{gpu_name or 'CUDA device'} ({device_arch}) is not supported by this "
"PyTorch build; TimesFM is using the CPU instead.",
RuntimeWarning,
stacklevel=2,
)
torch.set_float32_matmul_precision("high") torch.set_float32_matmul_precision("high")
desired_context = max(INPUT_PATCH, min(context_points, MODEL_CONTEXT_LIMIT)) desired_context = max(INPUT_PATCH, min(context_points, MODEL_CONTEXT_LIMIT))
...@@ -138,7 +194,7 @@ class TimesFMBackend: ...@@ -138,7 +194,7 @@ class TimesFMBackend:
) )
if use_cuda: if use_cuda:
self.device = "cuda:0" self.device = "cuda:0"
self.gpu_name = torch.cuda.get_device_name(0) self.gpu_name = gpu_name
else: else:
self.device = "cpu" self.device = "cpu"
self.gpu_name = None self.gpu_name = None
......
...@@ -6,6 +6,7 @@ import numpy as np ...@@ -6,6 +6,7 @@ import numpy as np
import pytest import pytest
from dohasecuritiesstockai.timesfm_forecasting.cli import _lookback_start from dohasecuritiesstockai.timesfm_forecasting.cli import _lookback_start
from dohasecuritiesstockai.timesfm_forecasting.forecaster import _cuda_device_is_supported
from dohasecuritiesstockai.timesfm_forecasting.market_data import ( from dohasecuritiesstockai.timesfm_forecasting.market_data import (
MarketCandle, MarketCandle,
fetch_dse_candles, fetch_dse_candles,
...@@ -48,6 +49,27 @@ class LinearForecastBackend: ...@@ -48,6 +49,27 @@ class LinearForecastBackend:
return point, quantiles, 1 return point, quantiles, 1
class FakeCuda:
def __init__(self, *, available: bool, capability=(0, 0), arches=()):
self.available = available
self.capability = capability
self.arches = list(arches)
def is_available(self):
return self.available
def get_device_capability(self, _device_index):
return self.capability
def get_arch_list(self):
return self.arches
class FakeTorch:
def __init__(self, cuda):
self.cuda = cuda
def _linear_candles(count: int) -> list[MarketCandle]: def _linear_candles(count: int) -> list[MarketCandle]:
start = datetime(2024, 1, 1, tzinfo=timezone.utc) start = datetime(2024, 1, 1, tzinfo=timezone.utc)
return [ return [
...@@ -91,6 +113,36 @@ def test_lookback_is_separate_from_candle_resolution(): ...@@ -91,6 +113,36 @@ def test_lookback_is_separate_from_candle_resolution():
assert _lookback_start(datetime(2026, 8, 10).date(), "max") is None assert _lookback_start(datetime(2026, 8, 10).date(), "max") is None
def test_timesfm_rejects_cuda_device_older_than_compiled_pytorch_arches():
torch = FakeTorch(
FakeCuda(
available=True,
capability=(6, 0),
arches=("sm_70", "sm_75", "sm_80", "sm_86", "sm_90"),
)
)
assert not _cuda_device_is_supported(torch)
def test_timesfm_accepts_compatible_cuda_cubin_or_forward_compatible_ptx():
same_major = FakeTorch(
FakeCuda(available=True, capability=(8, 9), arches=("sm_80", "sm_86"))
)
forward_ptx = FakeTorch(
FakeCuda(available=True, capability=(9, 0), arches=("sm_86", "compute_86"))
)
assert _cuda_device_is_supported(same_major)
assert _cuda_device_is_supported(forward_ptx)
def test_timesfm_treats_cuda_as_unavailable_when_pytorch_does():
torch = FakeTorch(FakeCuda(available=False))
assert not _cuda_device_is_supported(torch)
def test_fetch_dse_candles_normalizes_envelope_and_ohlcv_arrays(): def test_fetch_dse_candles_normalizes_envelope_and_ohlcv_arrays():
first = int(datetime(2025, 1, 1, tzinfo=timezone.utc).timestamp()) first = int(datetime(2025, 1, 1, tzinfo=timezone.utc).timestamp())
client = FakeDSEClient( client = FakeDSEClient(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment