feat: add backtest progress tracking and fix stop functionality

This commit is contained in:
shokollm
2026-04-10 10:43:04 +00:00
parent a601ebb08b
commit 922ef89c1e
5 changed files with 70 additions and 4 deletions

View File

@@ -169,6 +169,11 @@ def get_backtest(
status_code=status.HTTP_404_NOT_FOUND, detail="Backtest not found" status_code=status.HTTP_404_NOT_FOUND, detail="Backtest not found"
) )
# Add progress from running engine if available
if backtest.status == "running" and run_id in running_backtests:
engine = running_backtests[run_id]
backtest.progress = engine.progress
return backtest return backtest
@@ -226,7 +231,12 @@ def stop_backtest(
if run_id in running_backtests: if run_id in running_backtests:
engine = running_backtests[run_id] engine = running_backtests[run_id]
asyncio.create_task(engine.stop()) engine.running = False # Direct sync access to running flag
backtest.status = "stopped"
backtest.ended_at = datetime.utcnow()
db.commit()
elif backtest.status == "running":
# Engine already finished but status not updated
backtest.status = "stopped" backtest.status = "stopped"
backtest.ended_at = datetime.utcnow() backtest.ended_at = datetime.utcnow()
db.commit() db.commit()

View File

@@ -90,6 +90,7 @@ class BacktestResponse(BaseModel):
status: str status: str
config: dict config: dict
result: Optional[dict] result: Optional[dict]
progress: Optional[int] = None
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -31,6 +31,8 @@ class BacktestEngine:
self.entry_time: Optional[int] = None self.entry_time: Optional[int] = None
self.trades: List[Dict[str, Any]] = [] self.trades: List[Dict[str, Any]] = []
self.running = False self.running = False
self.progress = 0
self.total_klines = 0
async def run(self) -> Dict[str, Any]: async def run(self) -> Dict[str, Any]:
self.running = True self.running = True
@@ -98,10 +100,13 @@ class BacktestEngine:
return self.results return self.results
async def _process_klines(self, klines: List[Dict[str, Any]]): async def _process_klines(self, klines: List[Dict[str, Any]]):
self.total_klines = len(klines)
for i, kline in enumerate(klines): for i, kline in enumerate(klines):
if not self.running: if not self.running:
break break
self.progress = int((i / self.total_klines) * 100) if self.total_klines > 0 else 0
price = float(kline.get("close", 0)) price = float(kline.get("close", 0))
if price <= 0: if price <= 0:
continue continue
@@ -384,6 +389,8 @@ class BacktestEngine:
async def stop(self): async def stop(self):
self.running = False self.running = False
self.progress = 0
self.total_klines = 0
self.status = "stopped" self.status = "stopped"
self._calculate_metrics() self._calculate_metrics()
@@ -393,4 +400,13 @@ class BacktestEngine:
"status": self.status, "status": self.status,
"results": self.results, "results": self.results,
"signals": self.signals, "signals": self.signals,
"progress": self.progress,
"total_klines": self.total_klines,
}
def get_status(self) -> Dict[str, Any]:
return {
"status": self.status,
"progress": self.progress,
"total_klines": self.total_klines,
} }

View File

@@ -62,9 +62,10 @@ export interface Backtest {
bot_id: string; bot_id: string;
started_at: string; started_at: string;
ended_at: string | null; ended_at: string | null;
status: 'running' | 'completed' | 'failed'; status: 'running' | 'completed' | 'failed' | 'stopped';
config: BacktestConfig; config: BacktestConfig;
result: BacktestResult | null; result: BacktestResult | null;
progress?: number;
} }
export interface BacktestConfig { export interface BacktestConfig {

View File

@@ -33,12 +33,12 @@
await loadBot(); await loadBot();
await loadBacktests(); await loadBacktests();
// Poll for backtest updates every 5 seconds if any are running // Poll for backtest updates every 2 seconds if any are running
const pollInterval = setInterval(async () => { const pollInterval = setInterval(async () => {
if ($backtestStore.backtestHistory.some(b => b.status === 'running')) { if ($backtestStore.backtestHistory.some(b => b.status === 'running')) {
await loadBacktests(); await loadBacktests();
} }
}, 5000); }, 2000);
return () => clearInterval(pollInterval); return () => clearInterval(pollInterval);
} }
@@ -201,6 +201,12 @@
</div> </div>
{/if} {/if}
{#if backtest.status === 'running'} {#if backtest.status === 'running'}
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" style="width: {backtest.progress ?? 0}%"></div>
</div>
<span class="progress-text">{backtest.progress ?? 0}%</span>
</div>
<button onclick={() => stopBacktest(backtest.id)} class="btn btn-danger">Stop</button> <button onclick={() => stopBacktest(backtest.id)} class="btn btn-danger">Stop</button>
{/if} {/if}
</div> </div>
@@ -400,6 +406,11 @@
color: #fca5a5; color: #fca5a5;
} }
.status-stopped {
background: rgba(251, 191, 36, 0.2);
color: #fbbf24;
}
.backtest-date { .backtest-date {
color: #888; color: #888;
font-size: 0.85rem; font-size: 0.85rem;
@@ -435,6 +446,33 @@
color: #ef4444; color: #ef4444;
} }
.progress-container {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.progress-bar {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.85rem;
color: #888;
min-width: 40px;
}
.btn-danger { .btn-danger {
margin-top: 0.75rem; margin-top: 0.75rem;
width: auto; width: auto;