feat: add backtest progress tracking and fix stop functionality
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user