2 Commits

Author SHA1 Message Date
34b988bc69 fix chart and trade actions 2025-06-24 23:21:33 +03:00
5243c65fb6 cicd 2025-06-24 23:08:38 +03:00
2 changed files with 286 additions and 52 deletions

168
.github/workflows/ci-cd.yml vendored Normal file
View 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.)

View File

@ -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)