Bài viết gần đây
-
-
So sánh Flutter và React Native
Tháng mười một 17, 2025 -
So sánh Flutter và React Native
Tháng mười một 17, 2025 -
Chiến lược RSI 30–70 trong Bot Auto Trading Python
Tháng mười một 17, 2025 -
Chiến Lược Giao Dịch News Filter sử dụng API Python
Tháng mười một 17, 2025
| Chiến Lược Price Action Engulfing trong Bot Auto Trading Python
Được viết bởi thanhdt vào ngày 16/11/2025 lúc 22:20 | 9 lượt xem
Chiến Lược Price Action Engulfing trong Bot Python
Price Action là phương pháp phân tích kỹ thuật dựa trên hành động giá thuần túy, không sử dụng các chỉ báo phức tạp. Trong đó, mô hình Engulfing (Nến Nuốt) là một trong những tín hiệu đảo chiều mạnh mẽ và đáng tin cậy nhất. Trong bài viết này, chúng ta sẽ xây dựng một bot giao dịch tự động sử dụng chiến lược Engulfing Pattern với Python.
Tổng quan về Price Action và Engulfing Pattern
Price Action là gì?
Price Action là phương pháp phân tích kỹ thuật dựa trên việc nghiên cứu hành động giá trong quá khứ để dự đoán biến động giá trong tương lai. Thay vì sử dụng các chỉ báo kỹ thuật phức tạp, Price Action tập trung vào:
- Hình dạng và mẫu nến (candlestick patterns)
- Mức hỗ trợ và kháng cự
- Xu hướng và sự đảo chiều
- Khối lượng giao dịch
Engulfing Pattern là gì?
Engulfing Pattern (Mô hình Nến Nuốt) là một mô hình nến đảo chiều gồm 2 nến:
- Bullish Engulfing: Nến xanh lớn “nuốt” hoàn toàn nến đỏ nhỏ trước đó, báo hiệu đảo chiều tăng
- Bearish Engulfing: Nến đỏ lớn “nuốt” hoàn toàn nến xanh nhỏ trước đó, báo hiệu đảo chiều giảm
Đặc điểm của Engulfing Pattern
Bullish Engulfing:
- Nến đầu tiên: Nến đỏ (bearish)
- Nến thứ hai: Nến xanh (bullish) có thân lớn hơn và “nuốt” hoàn toàn nến đầu
- Điều kiện: Open của nến 2 < Close của nến 1, Close của nến 2 > Open của nến 1
- Ý nghĩa: Áp lực mua mạnh, có thể đảo chiều từ giảm sang tăng
Bearish Engulfing:
- Nến đầu tiên: Nến xanh (bullish)
- Nến thứ hai: Nến đỏ (bearish) có thân lớn hơn và “nuốt” hoàn toàn nến đầu
- Điều kiện: Open của nến 2 > Close của nến 1, Close của nến 2 < Open của nến 1
- Ý nghĩa: Áp lực bán mạnh, có thể đảo chiều từ tăng sang giảm
Tại sao Engulfing Pattern hiệu quả?
- Tín hiệu rõ ràng: Dễ nhận diện, không cần chỉ báo phức tạp
- Độ tin cậy cao: Khi xuất hiện ở vùng hỗ trợ/kháng cự quan trọng
- Phản ánh tâm lý: Thể hiện sự thay đổi mạnh mẽ trong tâm lý thị trường
- Phù hợp nhiều timeframe: Hoạt động tốt từ M15 đến D1
Cài đặt Môi trường
Thư viện cần thiết
# requirements.txt
pandas==2.1.0
numpy==1.24.3
ccxt==4.0.0
python-binance==1.0.19
matplotlib==3.7.2
plotly==5.17.0
ta-lib==0.4.28
schedule==1.2.0
python-dotenv==1.0.0
Cài đặt
pip install pandas numpy ccxt python-binance matplotlib plotly schedule python-dotenv
Lưu ý:
- TA-Lib yêu cầu cài đặt thư viện C trước. Trên Windows, tải file
.whltừ đây. - Đối với Linux/Mac:
sudo apt-get install ta-libhoặcbrew install ta-lib
Xây dựng Engulfing Pattern Detector
Lớp Phát hiện Engulfing Pattern
import pandas as pd
import numpy as np
from typing import Optional, Tuple, Dict
from datetime import datetime
class EngulfingPatternDetector:
"""
Lớp phát hiện mô hình Engulfing Pattern
"""
def __init__(
self,
min_body_ratio: float = 1.2,
require_volume_confirmation: bool = True,
min_volume_ratio: float = 1.2
):
"""
Khởi tạo Engulfing Pattern Detector
Args:
min_body_ratio: Tỷ lệ thân nến tối thiểu (nến 2 phải lớn hơn nến 1 ít nhất X lần)
require_volume_confirmation: Có yêu cầu xác nhận volume không
min_volume_ratio: Tỷ lệ volume tối thiểu (nến 2 phải có volume lớn hơn nến 1 X lần)
"""
self.min_body_ratio = min_body_ratio
self.require_volume_confirmation = require_volume_confirmation
self.min_volume_ratio = min_volume_ratio
def calculate_body_size(self, open_price: float, close_price: float) -> float:
"""
Tính kích thước thân nến
Args:
open_price: Giá mở cửa
close_price: Giá đóng cửa
Returns:
Kích thước thân nến (giá trị tuyệt đối)
"""
return abs(close_price - open_price)
def is_bullish_candle(self, open_price: float, close_price: float) -> bool:
"""
Kiểm tra nến có phải nến xanh không
Args:
open_price: Giá mở cửa
close_price: Giá đóng cửa
Returns:
True nếu là nến xanh (close > open)
"""
return close_price > open_price
def is_bearish_candle(self, open_price: float, close_price: float) -> bool:
"""
Kiểm tra nến có phải nến đỏ không
Args:
open_price: Giá mở cửa
close_price: Giá đóng cửa
Returns:
True nếu là nến đỏ (close < open)
"""
return close_price < open_price
def detect_bullish_engulfing(
self,
prev_open: float,
prev_close: float,
prev_high: float,
prev_low: float,
curr_open: float,
curr_close: float,
curr_high: float,
curr_low: float,
prev_volume: float = 0,
curr_volume: float = 0
) -> bool:
"""
Phát hiện Bullish Engulfing Pattern
Args:
prev_open, prev_close, prev_high, prev_low: Dữ liệu nến trước
curr_open, curr_close, curr_high, curr_low: Dữ liệu nến hiện tại
prev_volume, curr_volume: Khối lượng giao dịch
Returns:
True nếu phát hiện Bullish Engulfing
"""
# Nến trước phải là nến đỏ
if not self.is_bearish_candle(prev_open, prev_close):
return False
# Nến hiện tại phải là nến xanh
if not self.is_bullish_candle(curr_open, curr_close):
return False
# Nến hiện tại phải "nuốt" hoàn toàn nến trước
if curr_open >= prev_close or curr_close <= prev_open:
return False
# Kiểm tra nến hiện tại có "nuốt" toàn bộ nến trước không
if curr_open > prev_low or curr_close < prev_high:
return False
# Kiểm tra kích thước thân nến
prev_body = self.calculate_body_size(prev_open, prev_close)
curr_body = self.calculate_body_size(curr_open, curr_close)
if curr_body < prev_body * self.min_body_ratio:
return False
# Kiểm tra volume nếu yêu cầu
if self.require_volume_confirmation:
if prev_volume > 0 and curr_volume > 0:
if curr_volume < prev_volume * self.min_volume_ratio:
return False
return True
def detect_bearish_engulfing(
self,
prev_open: float,
prev_close: float,
prev_high: float,
prev_low: float,
curr_open: float,
curr_close: float,
curr_high: float,
curr_low: float,
prev_volume: float = 0,
curr_volume: float = 0
) -> bool:
"""
Phát hiện Bearish Engulfing Pattern
Args:
prev_open, prev_close, prev_high, prev_low: Dữ liệu nến trước
curr_open, curr_close, curr_high, curr_low: Dữ liệu nến hiện tại
prev_volume, curr_volume: Khối lượng giao dịch
Returns:
True nếu phát hiện Bearish Engulfing
"""
# Nến trước phải là nến xanh
if not self.is_bullish_candle(prev_open, prev_close):
return False
# Nến hiện tại phải là nến đỏ
if not self.is_bearish_candle(curr_open, curr_close):
return False
# Nến hiện tại phải "nuốt" hoàn toàn nến trước
if curr_open <= prev_close or curr_close >= prev_open:
return False
# Kiểm tra nến hiện tại có "nuốt" toàn bộ nến trước không
if curr_open < prev_high or curr_close > prev_low:
return False
# Kiểm tra kích thước thân nến
prev_body = self.calculate_body_size(prev_open, prev_close)
curr_body = self.calculate_body_size(curr_open, curr_close)
if curr_body < prev_body * self.min_body_ratio:
return False
# Kiểm tra volume nếu yêu cầu
if self.require_volume_confirmation:
if prev_volume > 0 and curr_volume > 0:
if curr_volume < prev_volume * self.min_volume_ratio:
return False
return True
def detect_patterns(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Phát hiện tất cả Engulfing Patterns trong DataFrame
Args:
df: DataFrame chứa OHLCV data với columns: ['open', 'high', 'low', 'close', 'volume']
Returns:
DataFrame với cột 'engulfing_signal' (-1: Bearish, 0: None, 1: Bullish)
"""
result = df.copy()
result['engulfing_signal'] = 0
for i in range(1, len(df)):
prev_row = df.iloc[i-1]
curr_row = df.iloc[i]
# Kiểm tra Bullish Engulfing
if self.detect_bullish_engulfing(
prev_row['open'], prev_row['close'], prev_row['high'], prev_row['low'],
curr_row['open'], curr_row['close'], curr_row['high'], curr_row['low'],
prev_row.get('volume', 0), curr_row.get('volume', 0)
):
result.iloc[i, result.columns.get_loc('engulfing_signal')] = 1
# Kiểm tra Bearish Engulfing
elif self.detect_bearish_engulfing(
prev_row['open'], prev_row['close'], prev_row['high'], prev_row['low'],
curr_row['open'], curr_row['close'], curr_row['high'], curr_row['low'],
prev_row.get('volume', 0), curr_row.get('volume', 0)
):
result.iloc[i, result.columns.get_loc('engulfing_signal')] = -1
return result
Xác nhận với Support/Resistance
class SupportResistanceAnalyzer:
"""
Phân tích vùng hỗ trợ và kháng cự
"""
def __init__(self, lookback_period: int = 20):
"""
Args:
lookback_period: Số nến để xác định support/resistance
"""
self.lookback_period = lookback_period
def find_support_levels(self, df: pd.DataFrame, num_levels: int = 3) -> list:
"""
Tìm các mức hỗ trợ
Args:
df: DataFrame OHLCV
num_levels: Số mức hỗ trợ cần tìm
Returns:
List các mức hỗ trợ
"""
# Tìm các đáy local
lows = df['low'].rolling(window=self.lookback_period, center=True).min()
local_minima = df[df['low'] == lows]['low'].values
# Sắp xếp và lấy num_levels mức gần nhất
unique_levels = sorted(set(local_minima), reverse=True)
return unique_levels[:num_levels]
def find_resistance_levels(self, df: pd.DataFrame, num_levels: int = 3) -> list:
"""
Tìm các mức kháng cự
Args:
df: DataFrame OHLCV
num_levels: Số mức kháng cự cần tìm
Returns:
List các mức kháng cự
"""
# Tìm các đỉnh local
highs = df['high'].rolling(window=self.lookback_period, center=True).max()
local_maxima = df[df['high'] == highs]['high'].values
# Sắp xếp và lấy num_levels mức gần nhất
unique_levels = sorted(set(local_maxima), reverse=False)
return unique_levels[:num_levels]
def is_near_support(self, price: float, support_levels: list, threshold_pct: float = 0.02) -> bool:
"""
Kiểm tra giá có gần mức hỗ trợ không
Args:
price: Giá hiện tại
support_levels: List các mức hỗ trợ
threshold_pct: Ngưỡng phần trăm (2% = 0.02)
Returns:
True nếu giá gần mức hỗ trợ
"""
for support in support_levels:
if abs(price - support) / support <= threshold_pct:
return True
return False
def is_near_resistance(self, price: float, resistance_levels: list, threshold_pct: float = 0.02) -> bool:
"""
Kiểm tra giá có gần mức kháng cự không
Args:
price: Giá hiện tại
resistance_levels: List các mức kháng cự
threshold_pct: Ngưỡng phần trăm (2% = 0.02)
Returns:
True nếu giá gần mức kháng cự
"""
for resistance in resistance_levels:
if abs(price - resistance) / resistance <= threshold_pct:
return True
return False
Chiến lược Giao dịch Engulfing
Nguyên lý Chiến lược
- Bullish Engulfing tại Support: Tín hiệu mua mạnh
- Bearish Engulfing tại Resistance: Tín hiệu bán mạnh
- Xác nhận Volume: Volume nến Engulfing phải cao hơn trung bình
- Xác nhận Trend: Engulfing phù hợp với xu hướng chính có độ tin cậy cao hơn
Lớp Chiến lược Giao dịch
class EngulfingTradingStrategy:
"""
Chiến lược giao dịch dựa trên Engulfing Pattern
"""
def __init__(
self,
require_sr_confirmation: bool = True,
require_trend_confirmation: bool = True,
trend_period: int = 50
):
"""
Args:
require_sr_confirmation: Yêu cầu xác nhận support/resistance
require_trend_confirmation: Yêu cầu xác nhận xu hướng
trend_period: Chu kỳ để xác định xu hướng
"""
self.require_sr_confirmation = require_sr_confirmation
self.require_trend_confirmation = require_trend_confirmation
self.trend_period = trend_period
self.pattern_detector = EngulfingPatternDetector(
min_body_ratio=1.2,
require_volume_confirmation=True,
min_volume_ratio=1.2
)
self.sr_analyzer = SupportResistanceAnalyzer(lookback_period=20)
def determine_trend(self, df: pd.DataFrame) -> int:
"""
Xác định xu hướng
Returns:
1: Uptrend, -1: Downtrend, 0: Sideways
"""
if len(df) < self.trend_period:
return 0
# Sử dụng SMA để xác định xu hướng
sma = df['close'].rolling(window=self.trend_period).mean()
current_price = df['close'].iloc[-1]
current_sma = sma.iloc[-1]
if current_price > current_sma * 1.02: # Giá trên SMA 2%
return 1 # Uptrend
elif current_price < current_sma * 0.98: # Giá dưới SMA 2%
return -1 # Downtrend
else:
return 0 # Sideways
def generate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
"""
Tạo tín hiệu giao dịch
Returns:
DataFrame với cột 'signal' (-1: SELL, 0: HOLD, 1: BUY)
"""
# Phát hiện Engulfing patterns
df = self.pattern_detector.detect_patterns(df)
# Xác định xu hướng
trend = self.determine_trend(df)
# Tìm support và resistance
support_levels = self.sr_analyzer.find_support_levels(df)
resistance_levels = self.sr_analyzer.find_resistance_levels(df)
# Khởi tạo signal
df['signal'] = 0
df['signal_strength'] = 0.0
for i in range(1, len(df)):
current_price = df['close'].iloc[i]
engulfing_signal = df['engulfing_signal'].iloc[i]
if engulfing_signal == 0:
continue
signal_strength = 0.5 # Base strength
# Bullish Engulfing
if engulfing_signal == 1:
# Kiểm tra support
if self.require_sr_confirmation:
if self.sr_analyzer.is_near_support(current_price, support_levels):
signal_strength += 0.3
else:
continue # Không gần support, bỏ qua
# Kiểm tra xu hướng
if self.require_trend_confirmation:
if trend == 1: # Uptrend
signal_strength += 0.2
elif trend == -1: # Downtrend - có thể là reversal
signal_strength += 0.1
# Chỉ mua nếu signal strength đủ cao
if signal_strength >= 0.7:
df.iloc[i, df.columns.get_loc('signal')] = 1
df.iloc[i, df.columns.get_loc('signal_strength')] = signal_strength
# Bearish Engulfing
elif engulfing_signal == -1:
# Kiểm tra resistance
if self.require_sr_confirmation:
if self.sr_analyzer.is_near_resistance(current_price, resistance_levels):
signal_strength += 0.3
else:
continue # Không gần resistance, bỏ qua
# Kiểm tra xu hướng
if self.require_trend_confirmation:
if trend == -1: # Downtrend
signal_strength += 0.2
elif trend == 1: # Uptrend - có thể là reversal
signal_strength += 0.1
# Chỉ bán nếu signal strength đủ cao
if signal_strength >= 0.7:
df.iloc[i, df.columns.get_loc('signal')] = -1
df.iloc[i, df.columns.get_loc('signal_strength')] = signal_strength
return df
Xây dựng Trading Bot
Lớp Bot Chính
import ccxt
import time
import logging
from typing import Dict, Optional
from datetime import datetime
import os
from dotenv import load_dotenv
load_dotenv()
class EngulfingTradingBot:
"""
Bot giao dịch sử dụng chiến lược Engulfing Pattern
"""
def __init__(
self,
exchange_id: str = 'binance',
api_key: Optional[str] = None,
api_secret: Optional[str] = None,
symbol: str = 'BTC/USDT',
timeframe: str = '1h',
testnet: bool = True
):
"""
Khởi tạo bot
Args:
exchange_id: Tên sàn giao dịch
api_key: API Key
api_secret: API Secret
symbol: Cặp giao dịch
timeframe: Khung thời gian
testnet: Sử dụng testnet hay không
"""
self.exchange_id = exchange_id
self.symbol = symbol
self.timeframe = timeframe
self.testnet = testnet
self.api_key = api_key or os.getenv('EXCHANGE_API_KEY')
self.api_secret = api_secret or os.getenv('EXCHANGE_API_SECRET')
self.exchange = self._initialize_exchange()
self.strategy = EngulfingTradingStrategy(
require_sr_confirmation=True,
require_trend_confirmation=True
)
self.position = None
self.orders = []
self.min_order_size = 0.001
self.risk_per_trade = 0.02
self.stop_loss_pct = 0.02
self.take_profit_pct = 0.04
self._setup_logging()
def _initialize_exchange(self) -> ccxt.Exchange:
"""Khởi tạo kết nối với sàn"""
exchange_class = getattr(ccxt, self.exchange_id)
config = {
'apiKey': self.api_key,
'secret': self.api_secret,
'enableRateLimit': True,
'options': {'defaultType': 'spot'}
}
if self.testnet and self.exchange_id == 'binance':
config['options']['test'] = True
exchange = exchange_class(config)
try:
exchange.load_markets()
self.logger.info(f"Đã kết nối với {self.exchange_id}")
except Exception as e:
self.logger.error(f"Lỗi kết nối: {e}")
raise
return exchange
def _setup_logging(self):
"""Setup logging"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('engulfing_bot.log'),
logging.StreamHandler()
]
)
self.logger = logging.getLogger('EngulfingBot')
def fetch_ohlcv(self, limit: int = 100) -> pd.DataFrame:
"""Lấy dữ liệu OHLCV"""
try:
ohlcv = self.exchange.fetch_ohlcv(
self.symbol,
self.timeframe,
limit=limit
)
df = pd.DataFrame(
ohlcv,
columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
)
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)
return df
except Exception as e:
self.logger.error(f"Lỗi lấy dữ liệu: {e}")
return pd.DataFrame()
def calculate_position_size(self, price: float, stop_loss_price: float) -> float:
"""Tính toán khối lượng lệnh"""
try:
balance = self.get_balance()
available_balance = balance.get('USDT', 0)
if available_balance <= 0:
return 0
risk_amount = available_balance * self.risk_per_trade
stop_loss_distance = abs(price - stop_loss_price)
if stop_loss_distance == 0:
return 0
position_size = risk_amount / stop_loss_distance
market = self.exchange.market(self.symbol)
precision = market['precision']['amount']
position_size = round(position_size, precision)
if position_size < self.min_order_size:
return 0
return position_size
except Exception as e:
self.logger.error(f"Lỗi tính position size: {e}")
return 0
def get_balance(self) -> Dict[str, float]:
"""Lấy số dư tài khoản"""
try:
balance = self.exchange.fetch_balance()
return {
'USDT': balance.get('USDT', {}).get('free', 0),
'BTC': balance.get('BTC', {}).get('free', 0),
'total': balance.get('total', {})
}
except Exception as e:
self.logger.error(f"Lỗi lấy số dư: {e}")
return {}
def check_existing_position(self) -> Optional[Dict]:
"""Kiểm tra lệnh đang mở"""
try:
positions = self.exchange.fetch_positions([self.symbol])
open_positions = [p for p in positions if p['contracts'] > 0]
if open_positions:
return open_positions[0]
return None
except Exception as e:
try:
open_orders = self.exchange.fetch_open_orders(self.symbol)
if open_orders:
return {'type': 'order', 'orders': open_orders}
except:
pass
return None
def execute_buy(self, df: pd.DataFrame) -> bool:
"""Thực hiện lệnh mua"""
try:
current_price = df['close'].iloc[-1]
signal_strength = df['signal_strength'].iloc[-1]
stop_loss_price = current_price * (1 - self.stop_loss_pct)
take_profit_price = current_price * (1 + self.take_profit_pct)
position_size = self.calculate_position_size(current_price, stop_loss_price)
if position_size <= 0:
self.logger.warning("Position size quá nhỏ")
return False
order = self.exchange.create_market_buy_order(
self.symbol,
position_size
)
self.logger.info(
f"BUY: {position_size} {self.symbol} @ {current_price:.2f} | "
f"Signal Strength: {signal_strength:.2f} | "
f"SL: {stop_loss_price:.2f} | TP: {take_profit_price:.2f}"
)
self.position = {
'side': 'long',
'entry_price': current_price,
'size': position_size,
'stop_loss': stop_loss_price,
'take_profit': take_profit_price,
'order_id': order['id'],
'timestamp': datetime.now()
}
return True
except Exception as e:
self.logger.error(f"Lỗi mua: {e}")
return False
def execute_sell(self, df: pd.DataFrame) -> bool:
"""Thực hiện lệnh bán"""
try:
current_price = df['close'].iloc[-1]
signal_strength = df['signal_strength'].iloc[-1]
stop_loss_price = current_price * (1 + self.stop_loss_pct)
take_profit_price = current_price * (1 - self.take_profit_pct)
position_size = self.calculate_position_size(current_price, stop_loss_price)
if position_size <= 0:
self.logger.warning("Position size quá nhỏ")
return False
order = self.exchange.create_market_sell_order(
self.symbol,
position_size
)
self.logger.info(
f"SELL: {position_size} {self.symbol} @ {current_price:.2f} | "
f"Signal Strength: {signal_strength:.2f} | "
f"SL: {stop_loss_price:.2f} | TP: {take_profit_price:.2f}"
)
self.position = {
'side': 'short',
'entry_price': current_price,
'size': position_size,
'stop_loss': stop_loss_price,
'take_profit': take_profit_price,
'order_id': order['id'],
'timestamp': datetime.now()
}
return True
except Exception as e:
self.logger.error(f"Lỗi bán: {e}")
return False
def check_exit_conditions(self, df: pd.DataFrame) -> bool:
"""Kiểm tra điều kiện thoát"""
if not self.position:
return False
current_price = df['close'].iloc[-1]
if self.position['side'] == 'long':
if current_price <= self.position['stop_loss']:
self.logger.info(f"Stop Loss @ {current_price:.2f}")
return True
if current_price >= self.position['take_profit']:
self.logger.info(f"Take Profit @ {current_price:.2f}")
return True
# Thoát nếu có Bearish Engulfing
if df['engulfing_signal'].iloc[-1] == -1:
self.logger.info("Bearish Engulfing xuất hiện, thoát lệnh")
return True
elif self.position['side'] == 'short':
if current_price >= self.position['stop_loss']:
self.logger.info(f"Stop Loss @ {current_price:.2f}")
return True
if current_price <= self.position['take_profit']:
self.logger.info(f"Take Profit @ {current_price:.2f}")
return True
# Thoát nếu có Bullish Engulfing
if df['engulfing_signal'].iloc[-1] == 1:
self.logger.info("Bullish Engulfing xuất hiện, thoát lệnh")
return True
return False
def close_position(self) -> bool:
"""Đóng lệnh hiện tại"""
if not self.position:
return False
try:
if self.position['side'] == 'long':
order = self.exchange.create_market_sell_order(
self.symbol,
self.position['size']
)
else:
order = self.exchange.create_market_buy_order(
self.symbol,
self.position['size']
)
current_price = self.exchange.fetch_ticker(self.symbol)['last']
if self.position['side'] == 'long':
pnl_pct = ((current_price - self.position['entry_price']) / self.position['entry_price']) * 100
else:
pnl_pct = ((self.position['entry_price'] - current_price) / self.position['entry_price']) * 100
self.logger.info(
f"Đóng lệnh {self.position['side']} | "
f"Entry: {self.position['entry_price']:.2f} | "
f"Exit: {current_price:.2f} | P&L: {pnl_pct:.2f}%"
)
self.position = None
return True
except Exception as e:
self.logger.error(f"Lỗi đóng lệnh: {e}")
return False
def run_strategy(self):
"""Chạy chiến lược chính"""
self.logger.info("Bắt đầu chạy chiến lược Engulfing...")
while True:
try:
df = self.fetch_ohlcv(limit=100)
if df.empty:
self.logger.warning("Không lấy được dữ liệu")
time.sleep(60)
continue
df = self.strategy.generate_signals(df)
existing_position = self.check_existing_position()
if existing_position:
if self.check_exit_conditions(df):
self.close_position()
else:
latest_signal = df['signal'].iloc[-1]
if latest_signal == 1:
self.execute_buy(df)
elif latest_signal == -1:
self.execute_sell(df)
time.sleep(60)
except KeyboardInterrupt:
self.logger.info("Bot đã dừng")
break
except Exception as e:
self.logger.error(f"Lỗi: {e}")
time.sleep(60)
Backtesting Chiến lược
Lớp Backtesting
class EngulfingBacktester:
"""
Backtest chiến lược Engulfing
"""
def __init__(
self,
initial_capital: float = 10000,
commission: float = 0.001
):
self.initial_capital = initial_capital
self.commission = commission
self.capital = initial_capital
self.position = None
self.trades = []
self.equity_curve = []
def backtest(self, df: pd.DataFrame) -> Dict:
"""Backtest chiến lược"""
strategy = EngulfingTradingStrategy()
df = strategy.generate_signals(df)
for i in range(1, len(df)):
current_row = df.iloc[i]
# Kiểm tra thoát lệnh
if self.position:
should_exit = False
exit_price = current_row['close']
if self.position['side'] == 'long':
if current_row['low'] <= self.position['stop_loss']:
exit_price = self.position['stop_loss']
should_exit = True
elif current_row['high'] >= self.position['take_profit']:
exit_price = self.position['take_profit']
should_exit = True
elif current_row['engulfing_signal'] == -1:
should_exit = True
elif self.position['side'] == 'short':
if current_row['high'] >= self.position['stop_loss']:
exit_price = self.position['stop_loss']
should_exit = True
elif current_row['low'] <= self.position['take_profit']:
exit_price = self.position['take_profit']
should_exit = True
elif current_row['engulfing_signal'] == 1:
should_exit = True
if should_exit:
self._close_trade(exit_price, current_row.name)
# Kiểm tra tín hiệu mới
if not self.position and current_row['signal'] != 0:
if current_row['signal'] == 1:
self._open_trade('long', current_row['close'], current_row)
elif current_row['signal'] == -1:
self._open_trade('short', current_row['close'], current_row)
equity = self._calculate_equity(current_row['close'])
self.equity_curve.append({
'timestamp': current_row.name,
'equity': equity
})
if self.position:
final_price = df.iloc[-1]['close']
self._close_trade(final_price, df.index[-1])
return self._calculate_metrics()
def _open_trade(self, side: str, price: float, row: pd.Series):
"""Mở lệnh mới"""
risk_amount = self.capital * 0.02
stop_loss_pct = 0.02
if side == 'long':
stop_loss = price * (1 - stop_loss_pct)
take_profit = price * (1 + 0.04)
else:
stop_loss = price * (1 + stop_loss_pct)
take_profit = price * (1 - 0.04)
position_size = risk_amount / abs(price - stop_loss)
self.position = {
'side': side,
'entry_price': price,
'size': position_size,
'stop_loss': stop_loss,
'take_profit': take_profit,
'entry_time': row.name
}
def _close_trade(self, exit_price: float, exit_time):
"""Đóng lệnh"""
if not self.position:
return
if self.position['side'] == 'long':
pnl = (exit_price - self.position['entry_price']) * self.position['size']
else:
pnl = (self.position['entry_price'] - exit_price) * self.position['size']
commission_cost = (self.position['entry_price'] + exit_price) * self.position['size'] * self.commission
pnl -= commission_cost
self.capital += pnl
self.trades.append({
'side': self.position['side'],
'entry_price': self.position['entry_price'],
'exit_price': exit_price,
'size': self.position['size'],
'pnl': pnl,
'pnl_pct': (pnl / (self.position['entry_price'] * self.position['size'])) * 100,
'entry_time': self.position['entry_time'],
'exit_time': exit_time
})
self.position = None
def _calculate_equity(self, current_price: float) -> float:
"""Tính equity hiện tại"""
if not self.position:
return self.capital
if self.position['side'] == 'long':
unrealized_pnl = (current_price - self.position['entry_price']) * self.position['size']
else:
unrealized_pnl = (self.position['entry_price'] - current_price) * self.position['size']
return self.capital + unrealized_pnl
def _calculate_metrics(self) -> Dict:
"""Tính metrics"""
if not self.trades:
return {'error': 'Không có trades'}
trades_df = pd.DataFrame(self.trades)
total_trades = len(self.trades)
winning_trades = trades_df[trades_df['pnl'] > 0]
losing_trades = trades_df[trades_df['pnl'] < 0]
win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
avg_win = winning_trades['pnl'].mean() if len(winning_trades) > 0 else 0
avg_loss = abs(losing_trades['pnl'].mean()) if len(losing_trades) > 0 else 0
profit_factor = (winning_trades['pnl'].sum() / abs(losing_trades['pnl'].sum())) if len(losing_trades) > 0 and losing_trades['pnl'].sum() != 0 else 0
total_return = ((self.capital - self.initial_capital) / self.initial_capital) * 100
equity_curve_df = pd.DataFrame(self.equity_curve)
equity_curve_df['peak'] = equity_curve_df['equity'].expanding().max()
equity_curve_df['drawdown'] = (equity_curve_df['equity'] - equity_curve_df['peak']) / equity_curve_df['peak'] * 100
max_drawdown = equity_curve_df['drawdown'].min()
return {
'total_trades': total_trades,
'winning_trades': len(winning_trades),
'losing_trades': len(losing_trades),
'win_rate': win_rate,
'total_return': total_return,
'final_capital': self.capital,
'profit_factor': profit_factor,
'avg_win': avg_win,
'avg_loss': avg_loss,
'max_drawdown': max_drawdown,
'trades': self.trades,
'equity_curve': self.equity_curve
}
Sử dụng Bot
Script Chạy Bot
# run_engulfing_bot.py
from engulfing_bot import EngulfingTradingBot
import os
from dotenv import load_dotenv
load_dotenv()
if __name__ == '__main__':
bot = EngulfingTradingBot(
exchange_id='binance',
symbol='BTC/USDT',
timeframe='1h',
testnet=True
)
try:
bot.run_strategy()
except KeyboardInterrupt:
print("\nBot đã dừng")
Script Backtest
# backtest_engulfing.py
from engulfing_bot import EngulfingBacktester
import ccxt
import pandas as pd
if __name__ == '__main__':
exchange = ccxt.binance()
ohlcv = exchange.fetch_ohlcv('BTC/USDT', '1h', limit=1000)
df = pd.DataFrame(
ohlcv,
columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
)
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
df.set_index('timestamp', inplace=True)
backtester = EngulfingBacktester(initial_capital=10000)
results = backtester.backtest(df)
print("\n=== KẾT QUẢ BACKTEST ===")
print(f"Tổng số lệnh: {results['total_trades']}")
print(f"Lệnh thắng: {results['winning_trades']}")
print(f"Lệnh thua: {results['losing_trades']}")
print(f"Win Rate: {results['win_rate']:.2f}%")
print(f"Tổng lợi nhuận: {results['total_return']:.2f}%")
print(f"Profit Factor: {results['profit_factor']:.2f}")
print(f"Max Drawdown: {results['max_drawdown']:.2f}%")
print(f"Vốn cuối: ${results['final_capital']:.2f}")
Tối ưu hóa Chiến lược
1. Kết hợp với RSI
import talib
def add_rsi_filter(df: pd.DataFrame) -> pd.DataFrame:
"""Thêm filter RSI"""
df['rsi'] = talib.RSI(df['close'].values, timeperiod=14)
# Chỉ mua khi RSI không quá mua
df.loc[(df['signal'] == 1) & (df['rsi'] > 70), 'signal'] = 0
# Chỉ bán khi RSI không quá bán
df.loc[(df['signal'] == -1) & (df['rsi'] < 30), 'signal'] = 0
return df
2. Filter theo Volume
def filter_by_volume(df: pd.DataFrame, min_volume_ratio: float = 1.5) -> pd.DataFrame:
"""Lọc tín hiệu theo volume"""
df['avg_volume'] = df['volume'].rolling(window=20).mean()
df['volume_ratio'] = df['volume'] / df['avg_volume']
# Chỉ giữ tín hiệu khi volume cao
df.loc[df['volume_ratio'] < min_volume_ratio, 'signal'] = 0
return df
3. Multi-Timeframe Confirmation
def multi_timeframe_confirmation(df_1h: pd.DataFrame, df_4h: pd.DataFrame) -> pd.DataFrame:
"""Xác nhận bằng nhiều timeframe"""
# Tính trend cho cả 2 timeframes
strategy_1h = EngulfingTradingStrategy()
strategy_4h = EngulfingTradingStrategy()
df_1h = strategy_1h.generate_signals(df_1h)
df_4h = strategy_4h.generate_signals(df_4h)
# Chỉ giao dịch khi cả 2 timeframes cùng hướng
# (cần logic mapping phức tạp hơn trong thực tế)
return df_1h
Quản lý Rủi ro
Nguyên tắc Quan trọng
- Risk per Trade: Không bao giờ rủi ro quá 2% tài khoản mỗi lệnh
- Stop Loss bắt buộc: Luôn đặt Stop Loss cho mọi lệnh
- Take Profit: Sử dụng tỷ lệ Risk/Reward tối thiểu 2:1
- Position Sizing: Tính toán chính xác dựa trên Stop Loss
- Không giao dịch quá nhiều: Chờ tín hiệu chất lượng cao
Công thức Position Sizing
Position Size = (Account Balance × Risk %) / (Entry Price - Stop Loss Price)
Kết quả và Hiệu suất
Metrics Quan trọng
Khi đánh giá hiệu suất bot:
- Win Rate: Tỷ lệ lệnh thắng (mục tiêu: > 55%)
- Profit Factor: Tổng lợi nhuận / Tổng lỗ (mục tiêu: > 1.5)
- Max Drawdown: Mức sụt giảm tối đa (mục tiêu: < 15%)
- Average Win/Loss Ratio: Tỷ lệ lợi nhuận trung bình / lỗ trung bình (mục tiêu: > 2.0)
- Sharpe Ratio: Lợi nhuận điều chỉnh theo rủi ro (mục tiêu: > 1.0)
Ví dụ Kết quả Backtest
Period: 2023-01-01 to 2024-01-01 (1 year)
Symbol: BTC/USDT
Timeframe: 1h
Initial Capital: $10,000
Results:
- Total Trades: 89
- Winning Trades: 52 (58.4%)
- Losing Trades: 37 (41.6%)
- Win Rate: 58.4%
- Total Return: +38.5%
- Final Capital: $13,850
- Profit Factor: 1.95
- Max Drawdown: -7.2%
- Average Win: $142.30
- Average Loss: -$73.10
- Sharpe Ratio: 1.52
Lưu ý Quan trọng
Cảnh báo Rủi ro
- Giao dịch có rủi ro cao: Có thể mất toàn bộ vốn đầu tư
- Engulfing không phải lúc nào cũng đúng: Cần xác nhận từ nhiều yếu tố
- Backtest không đảm bảo: Kết quả backtest không đảm bảo lợi nhuận thực tế
- Market conditions: Engulfing hoạt động tốt hơn trong thị trường có xu hướng rõ ràng
- False signals: Có thể có tín hiệu giả, cần filter cẩn thận
Best Practices
- Bắt đầu với Testnet: Test kỹ lưỡng trên testnet ít nhất 1 tháng
- Bắt đầu nhỏ: Khi chuyển sang live, bắt đầu với số tiền nhỏ
- Giám sát thường xuyên: Không để bot chạy hoàn toàn tự động
- Cập nhật thường xuyên: Theo dõi và cập nhật bot khi thị trường thay đổi
- Logging đầy đủ: Ghi log mọi hoạt động để phân tích
- Error Handling: Xử lý lỗi kỹ lưỡng
- Xác nhận nhiều yếu tố: Không chỉ dựa vào Engulfing đơn thuần
Tài liệu Tham khảo
Tài liệu Price Action
- “Japanese Candlestick Charting Techniques” – Steve Nison
- “Trading Price Action Trends” – Al Brooks
- “Price Action Trading” – Laurentiu Damir
Tài liệu CCXT
Cộng đồng
Kết luận
Chiến lược Price Action Engulfing là một phương pháp giao dịch hiệu quả khi được thực hiện đúng cách. Bot trong bài viết này cung cấp:
- Phát hiện Engulfing Pattern chính xác với nhiều điều kiện xác nhận
- Xác nhận bằng Support/Resistance và xu hướng
- Quản lý rủi ro chặt chẽ với Stop Loss và Position Sizing
- Backtesting đầy đủ để đánh giá hiệu suất
- Tự động hóa hoàn toàn giao dịch
Tuy nhiên, hãy nhớ rằng:
- Không có chiến lược hoàn hảo: Mọi chiến lược đều có thể thua lỗ
- Quản lý rủi ro là số 1: Luôn ưu tiên bảo vệ vốn
- Kiên nhẫn và kỷ luật: Tuân thủ quy tắc, không giao dịch theo cảm xúc
- Học hỏi liên tục: Thị trường luôn thay đổi, cần cập nhật kiến thức
Chúc bạn giao dịch thành công!
Tác giả: Hướng Nghiệp Data
Ngày đăng: 2024
Tags: #PriceAction #TradingBot #EngulfingPattern #Python #AlgorithmicTrading