refactor: 缠论引擎包化与 Web 分层(ECR-001)
将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务; 前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,448 @@
|
||||
import ccxt
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import mplfinance as mpf
|
||||
from talib import MACD, SMA
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
import datetime as dt
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
filename='chanlun_trading.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Configuration (user to modify)
|
||||
BINANCE_API_KEY = 'your_api_key' # Replace with your Binance API key
|
||||
BINANCE_API_SECRET = 'your_api_secret' # Replace with your Binance API secret
|
||||
SIMULATION_MODE = True # Set to False for live trading
|
||||
|
||||
# 1. Fetch K-line data from Binance (multi-timeframe support)
|
||||
def fetch_binance_data(symbol='BTC/USDT', timeframe='5m', limit=500):
|
||||
try:
|
||||
exchange = ccxt.binance({
|
||||
'apiKey': BINANCE_API_KEY if not SIMULATION_MODE else '',
|
||||
'secret': BINANCE_API_SECRET if not SIMULATION_MODE else '',
|
||||
'enableRateLimit': True,
|
||||
'options': {'defaultType': 'spot'}
|
||||
})
|
||||
since = exchange.parse8601((datetime.now(dt.UTC) - timedelta(days=7)).isoformat())
|
||||
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since, limit)
|
||||
df = pd.DataFrame(ohlcv, columns=['Date', 'Open', 'High', 'Low', 'Close', 'Volume'])
|
||||
df['Date'] = pd.to_datetime(df['Date'], unit='ms')
|
||||
df.set_index('Date', inplace=True)
|
||||
logging.info(f"Fetched {len(df)} K-lines for {symbol} ({timeframe})")
|
||||
return df
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to fetch data: {e}")
|
||||
raise
|
||||
|
||||
# 2. K-line merging (vectorized)
|
||||
def merge_kline(df):
|
||||
try:
|
||||
df = df.copy()
|
||||
merged_data = []
|
||||
trend = np.sign(df['Close'].diff().shift(-1)) # 1: up, -1: down, 0: neutral
|
||||
|
||||
# Detect inclusion
|
||||
is_included = ((df['High'].shift(-1) <= df['High']) & (df['Low'].shift(-1) >= df['Low'])) | \
|
||||
((df['High'].shift(-1) >= df['High']) & (df['Low'].shift(-1) <= df['Low']))
|
||||
|
||||
i = 0
|
||||
while i < len(df) - 1:
|
||||
if is_included.iloc[i]:
|
||||
current_k = df.iloc[i]
|
||||
next_k = df.iloc[i + 1]
|
||||
high = max(current_k['High'], next_k['High'])
|
||||
low = min(current_k['Low'], next_k['Low'])
|
||||
open_price = current_k['Open']
|
||||
close_price = next_k['Close'] if trend.iloc[i] >= 0 else next_k['Close']
|
||||
volume = current_k['Volume'] + next_k['Volume']
|
||||
|
||||
merged_data.append({
|
||||
'Date': next_k.name,
|
||||
'Open': open_price,
|
||||
'High': high,
|
||||
'Low': low,
|
||||
'Close': close_price,
|
||||
'Volume': volume
|
||||
})
|
||||
i += 2
|
||||
else:
|
||||
current_k = df.iloc[i]
|
||||
merged_data.append({
|
||||
'Date': current_k.name,
|
||||
'Open': current_k['Open'],
|
||||
'High': current_k['High'],
|
||||
'Low': current_k['Low'],
|
||||
'Close': current_k['Close'],
|
||||
'Volume': current_k['Volume']
|
||||
})
|
||||
i += 1
|
||||
|
||||
if i == len(df) - 1:
|
||||
last_k = df.iloc[i]
|
||||
merged_data.append({
|
||||
'Date': last_k.name,
|
||||
'Open': last_k['Open'],
|
||||
'High': last_k['High'],
|
||||
'Low': last_k['Low'],
|
||||
'Close': last_k['Close'],
|
||||
'Volume': last_k['Volume']
|
||||
})
|
||||
|
||||
merged_df = pd.DataFrame(merged_data)
|
||||
merged_df['Date'] = pd.to_datetime(merged_df['Date'])
|
||||
merged_df.set_index('Date', inplace=True)
|
||||
logging.info(f"Merged K-lines: {len(df)} -> {len(merged_df)}")
|
||||
return merged_df
|
||||
except Exception as e:
|
||||
logging.error(f"K-line merging failed: {e}")
|
||||
raise
|
||||
|
||||
# 3. Detect fractals (vectorized)
|
||||
def detect_fractals(df):
|
||||
try:
|
||||
df = df.copy()
|
||||
df['is_top'] = (df['High'] > df['High'].shift(1)) & (df['High'] > df['High'].shift(-1)) & \
|
||||
(df['High'] > df['High'].shift(2)) & (df['High'] > df['High'].shift(-2))
|
||||
df['is_bottom'] = (df['Low'] < df['Low'].shift(1)) & (df['Low'] < df['Low'].shift(-1)) & \
|
||||
(df['Low'] < df['Low'].shift(2)) & (df['Low'] < df['Low'].shift(-2))
|
||||
df['is_top'] = df['is_top'].fillna(False)
|
||||
df['is_bottom'] = df['is_bottom'].fillna(False)
|
||||
logging.info(f"Detected {df['is_top'].sum()} top fractals and {df['is_bottom'].sum()} bottom fractals")
|
||||
return df
|
||||
except Exception as e:
|
||||
logging.error(f"Fractal detection failed: {e}")
|
||||
raise
|
||||
|
||||
# 4. Detect strokes
|
||||
def detect_strokes(df):
|
||||
try:
|
||||
strokes = []
|
||||
last_fractal = None
|
||||
last_price = None
|
||||
last_index = None
|
||||
|
||||
for i in range(len(df)):
|
||||
if df['is_top'].iloc[i] or df['is_bottom'].iloc[i]:
|
||||
current_fractal = 'top' if df['is_top'].iloc[i] else 'bottom'
|
||||
current_price = df['High'].iloc[i] if current_fractal == 'top' else df['Low'].iloc[i]
|
||||
|
||||
if last_fractal is None:
|
||||
last_fractal = current_fractal
|
||||
last_price = current_price
|
||||
last_index = df.index[i]
|
||||
continue
|
||||
|
||||
if (last_fractal == 'top' and current_fractal == 'bottom' and current_price < last_price) or \
|
||||
(last_fractal == 'bottom' and current_fractal == 'top' and current_price > last_price):
|
||||
strokes.append({
|
||||
'start_time': last_index,
|
||||
'end_time': df.index[i],
|
||||
'start_price': last_price,
|
||||
'end_price': current_price,
|
||||
'type': 'down' if current_fractal == 'bottom' else 'up',
|
||||
'volume': df['Volume'].loc[last_index:df.index[i]].sum()
|
||||
})
|
||||
|
||||
last_fractal = current_fractal
|
||||
last_price = current_price
|
||||
last_index = df.index[i]
|
||||
|
||||
logging.info(f"Detected {len(strokes)} strokes")
|
||||
return strokes
|
||||
except Exception as e:
|
||||
logging.error(f"Stroke detection failed: {e}")
|
||||
raise
|
||||
|
||||
# 5. Detect segments
|
||||
def detect_segments(strokes):
|
||||
try:
|
||||
segments = []
|
||||
if len(strokes) < 3:
|
||||
return segments
|
||||
|
||||
i = 0
|
||||
while i < len(strokes) - 2:
|
||||
stroke1, stroke2, stroke3 = strokes[i], strokes[i+1], strokes[i+2]
|
||||
|
||||
if stroke1['type'] == 'up' and stroke2['type'] == 'down' and stroke3['type'] == 'up':
|
||||
if stroke3['end_price'] > stroke1['end_price']:
|
||||
segments.append({
|
||||
'start_time': stroke1['start_time'],
|
||||
'end_time': stroke3['end_time'],
|
||||
'start_price': stroke1['start_price'],
|
||||
'end_price': stroke3['end_price'],
|
||||
'type': 'up'
|
||||
})
|
||||
i += 3
|
||||
else:
|
||||
i += 1
|
||||
elif stroke1['type'] == 'down' and stroke2['type'] == 'up' and stroke3['type'] == 'down':
|
||||
if stroke3['end_price'] < stroke1['end_price']:
|
||||
segments.append({
|
||||
'start_time': stroke1['start_time'],
|
||||
'end_time': stroke3['end_time'],
|
||||
'start_price': stroke1['start_price'],
|
||||
'end_price': stroke3['end_price'],
|
||||
'type': 'down'
|
||||
})
|
||||
i += 3
|
||||
else:
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
logging.info(f"Detected {len(segments)} segments")
|
||||
return segments
|
||||
except Exception as e:
|
||||
logging.error(f"Segment detection failed: {e}")
|
||||
raise
|
||||
|
||||
# 6. Detect pivots (midlines)
|
||||
def detect_pivots(strokes):
|
||||
try:
|
||||
pivots = []
|
||||
if len(strokes) < 3:
|
||||
return pivots
|
||||
|
||||
for i in range(len(strokes) - 2):
|
||||
s1, s2, s3 = strokes[i:i+3]
|
||||
high = min(s1['start_price'], s1['end_price'], s2['start_price'], s2['end_price'],
|
||||
s3['start_price'], s3['end_price'])
|
||||
low = max(s1['start_price'], s1['end_price'], s2['start_price'], s2['end_price'],
|
||||
s3['start_price'], s3['end_price'])
|
||||
|
||||
if high > low:
|
||||
pivots.append({
|
||||
'start_time': s1['start_time'],
|
||||
'end_time': s3['end_time'],
|
||||
'high': high,
|
||||
'low': low
|
||||
})
|
||||
|
||||
logging.info(f"Detected {len(pivots)} pivots")
|
||||
return pivots
|
||||
except Exception as e:
|
||||
logging.error(f"Pivot detection failed: {e}")
|
||||
raise
|
||||
|
||||
# 7. Analyze higher timeframe (30m)
|
||||
def analyze_higher_timeframe(df_30m):
|
||||
try:
|
||||
df_30m = detect_fractals(df_30m)
|
||||
strokes_30m = detect_strokes(df_30m)
|
||||
|
||||
if not strokes_30m:
|
||||
return 'neutral'
|
||||
|
||||
last_stroke = strokes_30m[-1]
|
||||
logging.info(f"30m trend: {last_stroke['type']}")
|
||||
return last_stroke['type']
|
||||
except Exception as e:
|
||||
logging.error(f"Higher timeframe analysis failed: {e}")
|
||||
raise
|
||||
|
||||
# 8. Back-divergence detection (enhanced)
|
||||
def detect_back_divergence(df, strokes, higher_trend):
|
||||
try:
|
||||
macd, signal, hist = MACD(df['Close'], fastperiod=12, slowperiod=26, signalperiod=9)
|
||||
sma20 = SMA(df['Close'], timeperiod=20)
|
||||
df['macd'] = macd
|
||||
df['hist'] = hist
|
||||
df['sma20'] = sma20
|
||||
df['buy_signal'] = False
|
||||
df['sell_signal'] = False
|
||||
|
||||
stroke_metrics = []
|
||||
for stroke in strokes:
|
||||
start_idx = df.index.get_loc(stroke['start_time'])
|
||||
end_idx = df.index.get_loc(stroke['end_time'])
|
||||
hist_segment = df['hist'].iloc[start_idx:end_idx+1]
|
||||
price_change = abs(stroke['end_price'] - stroke['start_price'])
|
||||
hist_area = sum(abs(h) for h in hist_segment if not np.isnan(h))
|
||||
volume = stroke['volume']
|
||||
stroke_metrics.append({
|
||||
'start_time': stroke['start_time'],
|
||||
'end_time': stroke['end_time'],
|
||||
'type': stroke['type'],
|
||||
'price_change': price_change,
|
||||
'hist_area': hist_area,
|
||||
'volume': volume
|
||||
})
|
||||
|
||||
for i in range(2, len(stroke_metrics)):
|
||||
current_stroke = stroke_metrics[i]
|
||||
prev_stroke = stroke_metrics[i-2]
|
||||
|
||||
if current_stroke['type'] != prev_stroke['type']:
|
||||
continue
|
||||
|
||||
current_end_idx = df.index.get_loc(current_stroke['end_time'])
|
||||
|
||||
# Uptrend back-divergence (sell signal)
|
||||
if current_stroke['type'] == 'up':
|
||||
price_increase = df['High'].loc[current_stroke['end_time']] > df['High'].loc[prev_stroke['end_time']]
|
||||
hist_decrease = current_stroke['hist_area'] < prev_stroke['hist_area']
|
||||
volume_decrease = current_stroke['volume'] < prev_stroke['volume']
|
||||
is_top_fractal = df['is_top'].loc[current_stroke['end_time']]
|
||||
hist_positive = df['hist'].iloc[current_end_idx] > 0 or \
|
||||
(df['hist'].iloc[current_end_idx] < 0 and df['hist'].iloc[current_end_idx-1] > 0)
|
||||
sma_trend = df['Close'].iloc[current_end_idx] > df['sma20'].iloc[current_end_idx]
|
||||
trend_match = higher_trend in ['up', 'neutral']
|
||||
|
||||
if price_increase and hist_decrease and volume_decrease and is_top_fractal and \
|
||||
hist_positive and sma_trend and trend_match:
|
||||
df.loc[df.index[current_end_idx], 'sell_signal'] = True
|
||||
|
||||
# Downtrend back-divergence (buy signal)
|
||||
elif current_stroke['type'] == 'down':
|
||||
price_decrease = df['Low'].loc[current_stroke['end_time']] < df['Low'].loc[prev_stroke['end_time']]
|
||||
hist_decrease = current_stroke['hist_area'] < prev_stroke['hist_area']
|
||||
volume_decrease = current_stroke['volume'] < prev_stroke['volume']
|
||||
is_bottom_fractal = df['is_bottom'].loc[current_stroke['end_time']]
|
||||
hist_negative = df['hist'].iloc[current_end_idx] < 0 or \
|
||||
(df['hist'].iloc[current_end_idx] > 0 and df['hist'].iloc[current_end_idx-1] < 0)
|
||||
sma_trend = df['Close'].iloc[current_end_idx] < df['sma20'].iloc[current_end_idx]
|
||||
trend_match = higher_trend in ['down', 'neutral']
|
||||
|
||||
if price_decrease and hist_decrease and volume_decrease and is_bottom_fractal and \
|
||||
hist_negative and sma_trend and trend_match:
|
||||
df.loc[df.index[current_end_idx], 'buy_signal'] = True
|
||||
|
||||
logging.info(f"Detected {df['buy_signal'].sum()} buy signals and {df['sell_signal'].sum()} sell signals")
|
||||
return df
|
||||
except Exception as e:
|
||||
logging.error(f"Back-divergence detection failed: {e}")
|
||||
raise
|
||||
|
||||
# 9. Execute trade
|
||||
def execute_trade(exchange, symbol, signal, amount=0.001):
|
||||
try:
|
||||
if SIMULATION_MODE:
|
||||
msg = f"[SIMULATION] {'Buy' if signal == 'buy' else 'Sell'} {amount} {symbol} at {datetime.now(dt.UTC)}"
|
||||
print(msg)
|
||||
logging.info(msg)
|
||||
return
|
||||
|
||||
if signal == 'buy':
|
||||
order = exchange.create_market_buy_order(symbol, amount)
|
||||
msg = f"Buy order executed: {order}"
|
||||
print(msg)
|
||||
logging.info(msg)
|
||||
elif signal == 'sell':
|
||||
order = exchange.create_market_sell_order(symbol, amount)
|
||||
msg = f"Sell order executed: {order}"
|
||||
print(msg)
|
||||
logging.info(msg)
|
||||
except Exception as e:
|
||||
msg = f"Trade execution failed: {e}"
|
||||
print(msg)
|
||||
logging.error(msg)
|
||||
|
||||
# 10. Plot chart
|
||||
def plot_chart(df, strokes, segments, pivots):
|
||||
try:
|
||||
# Initialize additional plots
|
||||
apds = []
|
||||
alines = [] # For line segments
|
||||
|
||||
# Plot strokes as line segments
|
||||
for stroke in strokes:
|
||||
alines.append([(stroke['start_time'], stroke['start_price']),
|
||||
(stroke['end_time'], stroke['end_price'])])
|
||||
|
||||
# Plot segments as line segments
|
||||
for segment in segments:
|
||||
alines.append([(segment['start_time'], segment['start_price']),
|
||||
(segment['end_time'], segment['end_price'])])
|
||||
|
||||
# Plot pivots as horizontal lines
|
||||
for pivot in pivots:
|
||||
alines.append([(pivot['start_time'], pivot['high']),
|
||||
(pivot['end_time'], pivot['high'])])
|
||||
alines.append([(pivot['start_time'], pivot['low']),
|
||||
(pivot['end_time'], pivot['low'])])
|
||||
|
||||
# Add alines to plot (single color for simplicity, can customize)
|
||||
if alines:
|
||||
apds.append(mpf.make_addplot(
|
||||
None, # No y-data needed for alines
|
||||
alines=alines,
|
||||
type='line',
|
||||
color=['blue' if i < len(strokes) else 'purple' if i < len(strokes) + len(segments) else 'orange'
|
||||
for i in range(len(alines))],
|
||||
linestyle=['--' if i < len(strokes) else '-' if i < len(strokes) + len(segments) else ':'
|
||||
for i in range(len(alines))]
|
||||
))
|
||||
|
||||
# Plot buy/sell signals
|
||||
buy_signals = df[df['buy_signal']]['Close']
|
||||
sell_signals = df[df['sell_signal']]['Close']
|
||||
apds.append(mpf.make_addplot(buy_signals, type='scatter', markersize=100, marker='^', color='green'))
|
||||
apds.append(mpf.make_addplot(sell_signals, type='scatter', markersize=100, marker='v', color='red'))
|
||||
|
||||
# Plot K-line chart
|
||||
mpf.plot(df, type='candle', addplot=apds, title='Chanlun Advanced Analysis', style='yahoo')
|
||||
logging.info("Chart plotted successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Chart plotting failed: {e}")
|
||||
raise
|
||||
|
||||
# 11. Main function
|
||||
def main():
|
||||
try:
|
||||
# Initialize exchange
|
||||
exchange = ccxt.binance({
|
||||
'apiKey': BINANCE_API_KEY if not SIMULATION_MODE else '',
|
||||
'secret': BINANCE_API_SECRET if not SIMULATION_MODE else '',
|
||||
'enableRateLimit': True,
|
||||
'options': {'defaultType': 'spot'}
|
||||
})
|
||||
|
||||
# Fetch data
|
||||
df_5m = fetch_binance_data(symbol='BTC/USDT', timeframe='5m', limit=500)
|
||||
df_30m = fetch_binance_data(symbol='BTC/USDT', timeframe='30m', limit=200)
|
||||
|
||||
# Merge 5m K-lines
|
||||
df_5m = merge_kline(df_5m)
|
||||
|
||||
# Detect fractals, strokes, segments, pivots
|
||||
df_5m = detect_fractals(df_5m)
|
||||
strokes = detect_strokes(df_5m)
|
||||
segments = detect_segments(strokes)
|
||||
pivots = detect_pivots(strokes)
|
||||
|
||||
# Analyze 30m trend
|
||||
higher_trend = analyze_higher_timeframe(df_30m)
|
||||
print(f"30m Trend: {higher_trend}")
|
||||
|
||||
# Detect back-divergence
|
||||
df_5m = detect_back_divergence(df_5m, strokes, higher_trend)
|
||||
|
||||
# Plot chart
|
||||
plot_chart(df_5m, strokes, segments, pivots)
|
||||
|
||||
# Output and execute trades
|
||||
print("Buy Signals:")
|
||||
buy_signals = df_5m[df_5m['buy_signal']][['Close']]
|
||||
print(buy_signals)
|
||||
for idx, row in buy_signals.iterrows():
|
||||
execute_trade(exchange, 'BTC/USDT', 'buy', amount=0.001)
|
||||
|
||||
print("Sell Signals:")
|
||||
sell_signals = df_5m[df_5m['sell_signal']][['Close']]
|
||||
print(sell_signals)
|
||||
for idx, row in sell_signals.iterrows():
|
||||
execute_trade(exchange, 'BTC/USDT', 'sell', amount=0.001)
|
||||
|
||||
logging.info("Main function completed successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Main function failed: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user