Compare commits
2 Commits
9d843b7550
...
34b988bc69
Author | SHA1 | Date | |
---|---|---|---|
34b988bc69 | |||
5243c65fb6 |
168
.github/workflows/ci-cd.yml
vendored
Normal file
168
.github/workflows/ci-cd.yml
vendored
Normal file
@ -0,0 +1,168 @@
|
||||
name: CI/CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.9, 3.10, 3.11]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Cache pip packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pytest pytest-cov flake8 black isort
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# Stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# Exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
- name: Check code formatting with black
|
||||
run: |
|
||||
black --check --diff .
|
||||
|
||||
- name: Check import sorting with isort
|
||||
run: |
|
||||
isort --check-only --diff .
|
||||
|
||||
- name: Run tests with pytest
|
||||
run: |
|
||||
pytest --cov=. --cov-report=xml --cov-report=html
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: unittests
|
||||
name: codecov-umbrella
|
||||
|
||||
security-scan:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install safety bandit
|
||||
|
||||
- name: Run safety check
|
||||
run: |
|
||||
safety check
|
||||
|
||||
- name: Run bandit security scan
|
||||
run: |
|
||||
bandit -r . -f json -o bandit-report.json
|
||||
bandit -r . -f txt
|
||||
|
||||
build-and-deploy:
|
||||
needs: [test, security-scan]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Build application
|
||||
run: |
|
||||
# Add your build steps here
|
||||
echo "Building application..."
|
||||
# python setup.py build
|
||||
|
||||
- name: Create deployment package
|
||||
run: |
|
||||
# Create a deployment package
|
||||
tar -czf gogo2-deployment.tar.gz . --exclude='.git' --exclude='__pycache__' --exclude='*.pyc'
|
||||
|
||||
- name: Upload deployment artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deployment-package
|
||||
path: gogo2-deployment.tar.gz
|
||||
|
||||
docker-build:
|
||||
needs: [test, security-scan]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/gogo2:latest
|
||||
${{ secrets.DOCKER_USERNAME }}/gogo2:${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
notify:
|
||||
needs: [build-and-deploy, docker-build]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Notify on success
|
||||
if: ${{ needs.build-and-deploy.result == 'success' && needs.docker-build.result == 'success' }}
|
||||
run: |
|
||||
echo "🎉 Deployment successful!"
|
||||
# Add notification logic here (Slack, email, etc.)
|
||||
|
||||
- name: Notify on failure
|
||||
if: ${{ needs.build-and-deploy.result == 'failure' || needs.docker-build.result == 'failure' }}
|
||||
run: |
|
||||
echo "❌ Deployment failed!"
|
||||
# Add notification logic here (Slack, email, etc.)
|
170
web/dashboard.py
170
web/dashboard.py
@ -1209,11 +1209,11 @@ class TradingDashboard:
|
||||
if time.time() - cache_time < 3: # Use cached chart if < 3s old for faster updates
|
||||
price_chart = getattr(self, '_cached_price_chart', None)
|
||||
else:
|
||||
price_chart = self._create_price_chart_optimized_v2(symbol)
|
||||
price_chart = self._create_price_chart(symbol)
|
||||
self._cached_price_chart = price_chart
|
||||
self._cached_chart_data_time = time.time()
|
||||
else:
|
||||
price_chart = self._create_price_chart_optimized_v2(symbol)
|
||||
price_chart = self._create_price_chart(symbol)
|
||||
self._cached_price_chart = price_chart
|
||||
self._cached_chart_data_time = time.time()
|
||||
except Exception as e:
|
||||
@ -1343,13 +1343,29 @@ class TradingDashboard:
|
||||
"""Clear trade history and reset session stats"""
|
||||
if n_clicks and n_clicks > 0:
|
||||
try:
|
||||
# Clear both closed trades and session stats (they're the same now)
|
||||
# Store current position status before clearing
|
||||
has_position = bool(self.current_position)
|
||||
position_info = ""
|
||||
if has_position:
|
||||
side = self.current_position.get('side', 'UNKNOWN')
|
||||
price = self.current_position.get('price', 0)
|
||||
size = self.current_position.get('size', 0)
|
||||
position_info = f" (Current {side} position preserved: {size:.6f} @ ${price:.2f})"
|
||||
|
||||
# Clear trade history and session stats
|
||||
self.clear_closed_trades_history()
|
||||
logger.info("DASHBOARD: Trade history and session stats cleared by user")
|
||||
return [html.P("Trade history cleared", className="text-success text-center")]
|
||||
|
||||
logger.info(f"DASHBOARD: Trade history cleared by user{position_info}")
|
||||
|
||||
# Provide detailed feedback to user
|
||||
feedback_message = "✅ Trade history and session stats cleared"
|
||||
if has_position:
|
||||
feedback_message += f" • Current {self.current_position.get('side', 'UNKNOWN')} position preserved"
|
||||
|
||||
return [html.P(feedback_message, className="text-success text-center")]
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing trade history: {e}")
|
||||
return [html.P(f"Error clearing history: {str(e)}", className="text-danger text-center")]
|
||||
return [html.P(f"❌ Error clearing history: {str(e)}", className="text-danger text-center")]
|
||||
return dash.no_update
|
||||
|
||||
# Leverage slider callback
|
||||
@ -1426,7 +1442,7 @@ class TradingDashboard:
|
||||
'price': current_price,
|
||||
'size': 0.001, # Small test size (max 1 lot)
|
||||
'confidence': 1.0, # Manual trades have 100% confidence
|
||||
'timestamp': datetime.now(),
|
||||
'timestamp': self._now_local(), # Use local timezone for consistency with manual decisions
|
||||
'source': 'MANUAL_BUY',
|
||||
'mexc_executed': False, # Mark as manual/test trade
|
||||
'usd_size': current_price * 0.001
|
||||
@ -1464,7 +1480,7 @@ class TradingDashboard:
|
||||
'price': current_price,
|
||||
'size': 0.001, # Small test size (max 1 lot)
|
||||
'confidence': 1.0, # Manual trades have 100% confidence
|
||||
'timestamp': datetime.now(),
|
||||
'timestamp': self._now_local(), # Use local timezone for consistency with manual decisions
|
||||
'source': 'MANUAL_SELL',
|
||||
'mexc_executed': False, # Mark as manual/test trade
|
||||
'usd_size': current_price * 0.001
|
||||
@ -1643,15 +1659,17 @@ class TradingDashboard:
|
||||
logger.debug(f"[CHART] Added Williams pivot points using {actual_timeframe} data")
|
||||
except Exception as e:
|
||||
logger.debug(f"Error adding Williams pivot points to chart: {e}")
|
||||
# Continue without pivot points if there's an error
|
||||
|
||||
# Add moving averages if we have enough data
|
||||
# Add moving averages if we have enough data - FIXED pandas warnings
|
||||
if len(df) >= 20:
|
||||
# 20-period SMA
|
||||
df['sma_20'] = df['close'].rolling(window=20).mean()
|
||||
# 20-period SMA - use .copy() to avoid SettingWithCopyWarning
|
||||
df_with_sma = df.copy()
|
||||
df_with_sma.loc[:, 'sma_20'] = df_with_sma['close'].rolling(window=20).mean()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df.index,
|
||||
y=df['sma_20'],
|
||||
x=df_with_sma.index,
|
||||
y=df_with_sma['sma_20'],
|
||||
name='SMA 20',
|
||||
line=dict(color='#ff1493', width=1),
|
||||
opacity=0.8,
|
||||
@ -1661,12 +1679,14 @@ class TradingDashboard:
|
||||
)
|
||||
|
||||
if len(df) >= 50:
|
||||
# 50-period SMA
|
||||
df['sma_50'] = df['close'].rolling(window=50).mean()
|
||||
# 50-period SMA - use .copy() to avoid SettingWithCopyWarning
|
||||
if 'df_with_sma' not in locals():
|
||||
df_with_sma = df.copy()
|
||||
df_with_sma.loc[:, 'sma_50'] = df_with_sma['close'].rolling(window=50).mean()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=df.index,
|
||||
y=df['sma_50'],
|
||||
x=df_with_sma.index,
|
||||
y=df_with_sma['sma_50'],
|
||||
name='SMA 50',
|
||||
line=dict(color='#ffa500', width=1),
|
||||
opacity=0.8,
|
||||
@ -1686,7 +1706,7 @@ class TradingDashboard:
|
||||
hovertemplate='<b>Volume: %{y:.0f}</b><br>%{x}<extra></extra>'
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
)
|
||||
|
||||
# Mark recent trading decisions with proper markers
|
||||
if self.recent_decisions and df is not None and not df.empty:
|
||||
@ -1705,9 +1725,13 @@ class TradingDashboard:
|
||||
# Convert decision timestamp to match chart timezone if needed
|
||||
if isinstance(decision_time, datetime):
|
||||
if decision_time.tzinfo is not None:
|
||||
decision_time_utc = decision_time.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
# Decision has timezone info, convert to local timezone first, then UTC for comparison
|
||||
decision_time_local = decision_time.astimezone(self.timezone)
|
||||
decision_time_utc = decision_time_local.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
decision_time_utc = decision_time
|
||||
# Decision is naive datetime, assume it's already in local timezone
|
||||
decision_time_local = self.timezone.localize(decision_time)
|
||||
decision_time_utc = decision_time_local.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
continue
|
||||
|
||||
@ -1723,10 +1747,10 @@ class TradingDashboard:
|
||||
decision_time_pd = pd.to_datetime(decision_time_utc)
|
||||
if chart_start_utc <= decision_time_pd <= chart_end_utc:
|
||||
signal_type = decision.get('signal_type', 'UNKNOWN')
|
||||
if decision['action'] == 'BUY':
|
||||
buy_decisions.append((decision, signal_type))
|
||||
elif decision['action'] == 'SELL':
|
||||
sell_decisions.append((decision, signal_type))
|
||||
if decision['action'] == 'BUY':
|
||||
buy_decisions.append((decision, signal_type))
|
||||
elif decision['action'] == 'SELL':
|
||||
sell_decisions.append((decision, signal_type))
|
||||
|
||||
|
||||
|
||||
@ -1844,23 +1868,35 @@ class TradingDashboard:
|
||||
if not entry_time or not exit_time:
|
||||
continue
|
||||
|
||||
# Convert times to UTC for comparison
|
||||
if isinstance(entry_time, datetime):
|
||||
entry_time_utc = entry_time.astimezone(timezone.utc).replace(tzinfo=None) if entry_time.tzinfo else entry_time
|
||||
else:
|
||||
continue
|
||||
# Convert times to UTC for comparison - FIXED timezone handling
|
||||
try:
|
||||
if isinstance(entry_time, datetime):
|
||||
# If naive datetime, assume it's in local timezone
|
||||
if entry_time.tzinfo is None:
|
||||
entry_time_utc = self.timezone.localize(entry_time).astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
entry_time_utc = entry_time.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
continue
|
||||
|
||||
if isinstance(exit_time, datetime):
|
||||
# If naive datetime, assume it's in local timezone
|
||||
if exit_time.tzinfo is None:
|
||||
exit_time_utc = self.timezone.localize(exit_time).astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
exit_time_utc = exit_time.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
else:
|
||||
continue
|
||||
|
||||
if isinstance(exit_time, datetime):
|
||||
exit_time_utc = exit_time.astimezone(timezone.utc).replace(tzinfo=None) if exit_time.tzinfo else exit_time
|
||||
else:
|
||||
# Check if trade overlaps with chart timeframe
|
||||
entry_time_pd = pd.to_datetime(entry_time_utc)
|
||||
exit_time_pd = pd.to_datetime(exit_time_utc)
|
||||
|
||||
if (chart_start_utc <= entry_time_pd <= chart_end_utc) or (chart_start_utc <= exit_time_pd <= chart_end_utc):
|
||||
chart_trades.append(trade)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error processing trade timestamps: {e}")
|
||||
continue
|
||||
|
||||
# Check if trade overlaps with chart timeframe
|
||||
entry_time_pd = pd.to_datetime(entry_time_utc)
|
||||
exit_time_pd = pd.to_datetime(exit_time_utc)
|
||||
|
||||
if (chart_start_utc <= entry_time_pd <= chart_end_utc) or (chart_start_utc <= exit_time_pd <= chart_end_utc):
|
||||
chart_trades.append(trade)
|
||||
|
||||
# Minimal logging - only show count
|
||||
if len(chart_trades) > 0:
|
||||
@ -2415,7 +2451,7 @@ class TradingDashboard:
|
||||
'symbol': symbol,
|
||||
'price': current_price,
|
||||
'confidence': confidence,
|
||||
'timestamp': datetime.now(timezone.utc), # Use UTC to match candle data
|
||||
'timestamp': self._now_local(), # Use local timezone for consistency with manual decisions
|
||||
'size': 0.1, # Will be adjusted by confidence in processing
|
||||
'reason': f'Scalping BUY: momentum={momentum:.6f}, trend={trend_strength:.6f}, conf={confidence:.3f}'
|
||||
}
|
||||
@ -2434,7 +2470,7 @@ class TradingDashboard:
|
||||
'symbol': symbol,
|
||||
'price': current_price,
|
||||
'confidence': confidence,
|
||||
'timestamp': datetime.now(timezone.utc), # Use UTC to match candle data
|
||||
'timestamp': self._now_local(), # Use local timezone for consistency with manual decisions
|
||||
'size': 0.1, # Will be adjusted by confidence in processing
|
||||
'reason': f'Scalping SELL: momentum={momentum:.6f}, trend={trend_strength:.6f}, conf={confidence:.3f}'
|
||||
}
|
||||
@ -2451,7 +2487,7 @@ class TradingDashboard:
|
||||
if not decision:
|
||||
return
|
||||
|
||||
current_time = datetime.now(timezone.utc) # Use UTC for consistency
|
||||
current_time = self._now_local() # Use local timezone for consistency
|
||||
|
||||
# Get fee structure from config (fallback to hardcoded values)
|
||||
try:
|
||||
@ -2998,14 +3034,22 @@ class TradingDashboard:
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Convert datetime objects to strings for JSON serialization
|
||||
# Convert datetime objects to strings for JSON serialization with timezone info
|
||||
trades_for_json = []
|
||||
for trade in self.closed_trades:
|
||||
trade_copy = trade.copy()
|
||||
if isinstance(trade_copy.get('entry_time'), datetime):
|
||||
trade_copy['entry_time'] = trade_copy['entry_time'].isoformat()
|
||||
# Ensure timezone is set before saving
|
||||
dt = trade_copy['entry_time']
|
||||
if dt.tzinfo is None:
|
||||
dt = self.timezone.localize(dt)
|
||||
trade_copy['entry_time'] = dt.isoformat()
|
||||
if isinstance(trade_copy.get('exit_time'), datetime):
|
||||
trade_copy['exit_time'] = trade_copy['exit_time'].isoformat()
|
||||
# Ensure timezone is set before saving
|
||||
dt = trade_copy['exit_time']
|
||||
if dt.tzinfo is None:
|
||||
dt = self.timezone.localize(dt)
|
||||
trade_copy['exit_time'] = dt.isoformat()
|
||||
if isinstance(trade_copy.get('duration'), timedelta):
|
||||
trade_copy['duration'] = str(trade_copy['duration'])
|
||||
trades_for_json.append(trade_copy)
|
||||
@ -3031,12 +3075,20 @@ class TradingDashboard:
|
||||
trades_data = json.load(f)
|
||||
logger.info(f"LOAD_TRADES: Raw data loaded: {len(trades_data)} trades")
|
||||
|
||||
# Convert string dates back to datetime objects
|
||||
# Convert string dates back to datetime objects with proper timezone handling
|
||||
for trade in trades_data:
|
||||
if isinstance(trade.get('entry_time'), str):
|
||||
trade['entry_time'] = datetime.fromisoformat(trade['entry_time'])
|
||||
dt = datetime.fromisoformat(trade['entry_time'])
|
||||
# If loaded datetime is naive, assume it's in local timezone (Sofia)
|
||||
if dt.tzinfo is None:
|
||||
dt = self.timezone.localize(dt)
|
||||
trade['entry_time'] = dt
|
||||
if isinstance(trade.get('exit_time'), str):
|
||||
trade['exit_time'] = datetime.fromisoformat(trade['exit_time'])
|
||||
dt = datetime.fromisoformat(trade['exit_time'])
|
||||
# If loaded datetime is naive, assume it's in local timezone (Sofia)
|
||||
if dt.tzinfo is None:
|
||||
dt = self.timezone.localize(dt)
|
||||
trade['exit_time'] = dt
|
||||
if isinstance(trade.get('duration'), str):
|
||||
# Parse duration string back to timedelta
|
||||
duration_parts = trade['duration'].split(':')
|
||||
@ -3054,16 +3106,30 @@ class TradingDashboard:
|
||||
self.closed_trades = []
|
||||
|
||||
def clear_closed_trades_history(self):
|
||||
"""Clear closed trades history and remove file"""
|
||||
"""Clear closed trades history and reset session stats (but keep current positions)"""
|
||||
try:
|
||||
# Clear closed trades history only
|
||||
self.closed_trades = []
|
||||
|
||||
# Reset session statistics (but NOT current position)
|
||||
self.total_realized_pnl = 0.0
|
||||
self.total_fees = 0.0
|
||||
self.session_pnl = 0.0
|
||||
|
||||
# Clear recent decisions related to closed trades but keep current position decisions
|
||||
# Keep only the last few decisions that might be related to current open position
|
||||
if self.recent_decisions:
|
||||
# Keep last 5 decisions in case they're related to current position
|
||||
self.recent_decisions = self.recent_decisions[-5:] if len(self.recent_decisions) > 5 else self.recent_decisions
|
||||
|
||||
# Remove file if it exists
|
||||
from pathlib import Path
|
||||
if Path('closed_trades_history.json').exists():
|
||||
Path('closed_trades_history.json').unlink()
|
||||
|
||||
logger.info("Cleared closed trades history")
|
||||
# Log what was preserved
|
||||
position_status = "PRESERVED" if self.current_position else "NONE"
|
||||
logger.info(f"Cleared closed trades history - Current position: {position_status}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error clearing closed trades history: {e}")
|
||||
@ -6318,8 +6384,8 @@ class TradingDashboard:
|
||||
logger.error(f"Optimized chart error: {e}")
|
||||
return self._create_empty_chart(f"{symbol} Chart", f"Chart Error: {str(e)}")
|
||||
|
||||
def _create_price_chart_optimized(self, symbol, current_price):
|
||||
"""Optimized chart creation with minimal data fetching"""
|
||||
def _get_williams_pivot_points_for_chart(self, df: pd.DataFrame, chart_df: pd.DataFrame = None) -> List[Dict]:
|
||||
"""Get Williams pivot points for chart display"""
|
||||
try:
|
||||
# Use minimal data for chart
|
||||
df = self.data_provider.get_historical_data(symbol, '1m', limit=20, refresh=False)
|
||||
|
Reference in New Issue
Block a user