Files
DinQuant/backend_api_python/app/services/strategy_compiler.py
T
TIANHE f43312a858 creat
Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
2025-12-29 03:06:49 +08:00

689 lines
29 KiB
Python

import json
from typing import Dict, Any, List
class StrategyCompiler:
def compile(self, config: Dict[str, Any]) -> str:
"""
Compiles the strategy configuration JSON into executable Python code.
"""
# Extract configurations
name = config.get('name', 'Generated Strategy')
entry_rules = config.get('entry_rules', [])
position_config = config.get('position_config', {})
pyramiding_rules = config.get('pyramiding_rules', {})
risk_management = config.get('risk_management', {})
# 1. Imports and Setup
code = self._get_header(name)
# 2. Parameters (Variables)
code += self._get_parameters(position_config, pyramiding_rules, risk_management)
# 3. Indicators Calculation
code += self._get_indicators_calculation(entry_rules)
# 4. Signal Logic (Entry Conditions)
code += self._get_entry_logic(entry_rules)
# 5. Core Loop (Position Management) - Based on code2.py
code += self._get_core_loop(position_config, pyramiding_rules, risk_management)
# 6. Output Formatting
code += self._get_output_section(name, entry_rules)
return code
def _get_header(self, name):
return f'''
# Generated Strategy: {name}
import pandas as pd
import numpy as np
# Helper function for checking signals safely
def get_val(arr, i, default=0):
if i < 0 or i >= len(arr): return default
return arr[i]
'''
def _get_parameters(self, pos_config, pyr_rules, risk_mgmt):
# Default values
initial_size = pos_config.get('initial_size_pct', 10) / 100.0
leverage = pos_config.get('leverage', 1)
max_pyramiding = pos_config.get('max_pyramiding', 0)
pyr_enabled = pyr_rules.get('enabled', False)
add_size = pyr_rules.get('size_pct', 0) / 100.0 if pyr_enabled else 0
add_threshold = pyr_rules.get('value', 0) / 100.0
stop_loss = risk_mgmt.get('stop_loss', {})
sl_enabled = stop_loss.get('enabled', False)
sl_pct = stop_loss.get('value', 0) / 100.0 if sl_enabled else 0.0
trailing = risk_mgmt.get('trailing_stop', {})
ts_enabled = trailing.get('enabled', False)
ts_activation = trailing.get('activation_profit', 0) / 100.0
ts_callback = trailing.get('callback_pct', 0) / 100.0
return f'''
# ===========================
# 1. Parameters
# ===========================
initial_position_pct = {initial_size}
leverage = {leverage}
max_pyramiding = {max_pyramiding}
# Pyramiding
add_position_pct = {add_size}
add_threshold_pct = {add_threshold}
# Risk Management
stop_loss_pct = {sl_pct}
take_profit_activation = {ts_activation}
trailing_callback = {ts_callback}
'''
def _get_indicators_calculation(self, rules):
code = """
# ===========================
# 2. Indicators Calculation
# ===========================
"""
calculated = set()
for rule in rules:
ind = rule.get('indicator')
params = rule.get('params', {})
if ind == 'supertrend':
key = f"st_{params.get('period')}_{params.get('multiplier')}"
if key not in calculated:
code += f"""
# SuperTrend ({params.get('period')}, {params.get('multiplier')})
period = {params.get('period', 14)}
multiplier = {params.get('multiplier', 3.0)}
df['hl2'] = (df['high'] + df['low']) / 2
df['tr'] = np.maximum(df['high'] - df['low'], np.maximum(abs(df['high'] - df['close'].shift(1)), abs(df['low'] - df['close'].shift(1))))
df['atr'] = df['tr'].ewm(alpha=1/period, adjust=False).mean()
df['basic_upper'] = df['hl2'] + (multiplier * df['atr'])
df['basic_lower'] = df['hl2'] - (multiplier * df['atr'])
final_upper = [0.0] * len(df)
final_lower = [0.0] * len(df)
trend = [1] * len(df)
close_arr = df['close'].values
basic_upper = np.nan_to_num(df['basic_upper'].values)
basic_lower = np.nan_to_num(df['basic_lower'].values)
for i in range(1, len(df)):
if basic_upper[i] < final_upper[i-1] or close_arr[i-1] > final_upper[i-1]:
final_upper[i] = basic_upper[i]
else:
final_upper[i] = final_upper[i-1]
if basic_lower[i] > final_lower[i-1] or close_arr[i-1] < final_lower[i-1]:
final_lower[i] = basic_lower[i]
else:
final_lower[i] = final_lower[i-1]
prev_trend = trend[i-1]
if prev_trend == -1 and close_arr[i] > final_upper[i-1]:
trend[i] = 1
elif prev_trend == 1 and close_arr[i] < final_lower[i-1]:
trend[i] = -1
else:
trend[i] = prev_trend
df['st_trend'] = trend
df['st_upper'] = final_upper
df['st_lower'] = final_lower
"""
calculated.add(key)
elif ind == 'ema':
period = params.get('period', 20)
key = f"ema_{period}"
if key not in calculated:
code += f"\ndf['ema_{period}'] = df['close'].ewm(span={period}, adjust=False).mean()\n"
calculated.add(key)
elif ind == 'rsi':
period = params.get('period', 14)
key = f"rsi_{period}"
if key not in calculated:
code += f"""
# RSI ({period})
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window={period}).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window={period}).mean()
rs = gain / loss
df['rsi_{period}'] = 100 - (100 / (1 + rs))
"""
calculated.add(key)
elif ind == 'macd':
fast = params.get('fast_period', 12)
slow = params.get('slow_period', 26)
signal = params.get('signal_period', 9)
key = f"macd_{fast}_{slow}_{signal}"
if key not in calculated:
code += f"""
# MACD ({fast}, {slow}, {signal})
exp1 = df['close'].ewm(span={fast}, adjust=False).mean()
exp2 = df['close'].ewm(span={slow}, adjust=False).mean()
df['macd_{fast}_{slow}_{signal}_line'] = exp1 - exp2
df['macd_{fast}_{slow}_{signal}_signal'] = df['macd_{fast}_{slow}_{signal}_line'].ewm(span={signal}, adjust=False).mean()
df['macd_{fast}_{slow}_{signal}_hist'] = df['macd_{fast}_{slow}_{signal}_line'] - df['macd_{fast}_{slow}_{signal}_signal']
"""
calculated.add(key)
elif ind == 'bollinger':
period = params.get('period', 20)
std_dev = params.get('std_dev', 2.0)
key = f"bb_{period}_{std_dev}"
if key not in calculated:
code += f"""
# Bollinger Bands ({period}, {std_dev})
sma = df['close'].rolling(window={period}).mean()
std = df['close'].rolling(window={period}).std()
df['bb_{period}_{std_dev}_upper'] = sma + ({std_dev} * std)
df['bb_{period}_{std_dev}_lower'] = sma - ({std_dev} * std)
df['bb_{period}_{std_dev}_mid'] = sma
"""
calculated.add(key)
elif ind == 'kdj':
period = params.get('period', 9)
signal_period = params.get('signal_period', 3)
key = f"kdj_{period}_{signal_period}"
if key not in calculated:
code += f"""
# KDJ ({period}, {signal_period})
low_min = df['low'].rolling(window={period}).min()
high_max = df['high'].rolling(window={period}).max()
rsv = (df['close'] - low_min) / (high_max - low_min) * 100
df['kdj_{period}_{signal_period}_k'] = rsv.ewm(alpha=1/{signal_period}, adjust=False).mean()
df['kdj_{period}_{signal_period}_d'] = df['kdj_{period}_{signal_period}_k'].ewm(alpha=1/{signal_period}, adjust=False).mean()
df['kdj_{period}_{signal_period}_j'] = 3 * df['kdj_{period}_{signal_period}_k'] - 2 * df['kdj_{period}_{signal_period}_d']
"""
calculated.add(key)
elif ind == 'ma':
period = params.get('period', 20)
ma_type = params.get('ma_type', 'sma')
key = f"ma_{ma_type}_{period}"
if key not in calculated:
if ma_type == 'ema':
code += f"\ndf['ma_{ma_type}_{period}'] = df['close'].ewm(span={period}, adjust=False).mean()\n"
else:
code += f"\ndf['ma_{ma_type}_{period}'] = df['close'].rolling(window={period}).mean()\n"
calculated.add(key)
return code
def _get_entry_logic(self, rules):
code = """
# ===========================
# 3. Entry Signal Logic
# ===========================
# Default False
df['raw_buy'] = False
df['raw_sell'] = False
"""
conditions_buy = []
conditions_sell = []
for rule in rules:
ind = rule.get('indicator')
params = rule.get('params', {})
if ind == 'supertrend':
signal = rule.get('signal', 'trend_bullish')
if signal == 'trend_bullish':
conditions_buy.append("(df['st_trend'] == 1) & (df['st_trend'].shift(1) == -1)")
conditions_sell.append("(df['st_trend'] == -1) & (df['st_trend'].shift(1) == 1)")
elif signal == 'is_uptrend':
conditions_buy.append("(df['st_trend'] == 1)")
conditions_sell.append("(df['st_trend'] == -1)")
elif ind == 'ema':
period = params.get('period', 20)
operator = rule.get('operator', 'price_above')
col = f"df['ema_{period}']"
if operator == 'price_above':
conditions_buy.append(f"(df['close'] > {col})")
conditions_sell.append(f"(df['close'] < {col})")
elif operator == 'price_below':
conditions_buy.append(f"(df['close'] < {col})")
conditions_sell.append(f"(df['close'] > {col})")
elif operator == 'cross_up':
conditions_buy.append(f"(df['close'] > {col}) & (df['close'].shift(1) <= {col}.shift(1))")
conditions_sell.append(f"(df['close'] < {col}) & (df['close'].shift(1) >= {col}.shift(1))")
elif operator == 'cross_down':
conditions_buy.append(f"(df['close'] < {col}) & (df['close'].shift(1) >= {col}.shift(1))")
conditions_sell.append(f"(df['close'] > {col}) & (df['close'].shift(1) <= {col}.shift(1))")
elif ind == 'rsi':
period = params.get('period', 14)
operator = rule.get('operator', '<')
thresh = params.get('threshold', 30)
col = f"df['rsi_{period}']"
if operator == '<':
conditions_buy.append(f"({col} < {thresh})")
conditions_sell.append(f"({col} > {100-thresh})")
elif operator == '>':
conditions_buy.append(f"({col} > {thresh})")
conditions_sell.append(f"({col} < {100-thresh})")
elif operator == 'cross_up':
conditions_buy.append(f"({col} > {thresh}) & ({col}.shift(1) <= {thresh})")
conditions_sell.append(f"({col} < {100-thresh}) & ({col}.shift(1) >= {100-thresh})")
elif operator == 'cross_down':
conditions_buy.append(f"({col} < {thresh}) & ({col}.shift(1) >= {thresh})")
conditions_sell.append(f"({col} > {100-thresh}) & ({col}.shift(1) <= {100-thresh})")
elif ind == 'macd':
fast = params.get('fast_period', 12)
slow = params.get('slow_period', 26)
signal = params.get('signal_period', 9)
operator = rule.get('operator', 'diff_gt_dea')
line_col = f"df['macd_{fast}_{slow}_{signal}_line']"
sig_col = f"df['macd_{fast}_{slow}_{signal}_signal']"
if operator == 'diff_gt_dea':
conditions_buy.append(f"({line_col} > {sig_col})")
conditions_sell.append(f"({line_col} < {sig_col})")
elif operator == 'diff_lt_dea':
conditions_buy.append(f"({line_col} < {sig_col})")
conditions_sell.append(f"({line_col} > {sig_col})")
elif operator == 'cross_up':
conditions_buy.append(f"({line_col} > {sig_col}) & ({line_col}.shift(1) <= {sig_col}.shift(1))")
conditions_sell.append(f"({line_col} < {sig_col}) & ({line_col}.shift(1) >= {sig_col}.shift(1))")
elif operator == 'cross_down':
conditions_buy.append(f"({line_col} < {sig_col}) & ({line_col}.shift(1) >= {sig_col}.shift(1))")
conditions_sell.append(f"({line_col} > {sig_col}) & ({line_col}.shift(1) <= {sig_col}.shift(1))")
elif ind == 'bollinger':
period = params.get('period', 20)
std_dev = params.get('std_dev', 2.0)
operator = rule.get('operator', 'price_above_upper')
upper = f"df['bb_{period}_{std_dev}_upper']"
lower = f"df['bb_{period}_{std_dev}_lower']"
mid = f"df['bb_{period}_{std_dev}_mid']"
if operator == 'price_above_upper':
conditions_buy.append(f"(df['close'] > {upper})")
conditions_sell.append(f"(df['close'] < {lower})")
elif operator == 'price_below_lower':
conditions_buy.append(f"(df['close'] < {lower})")
conditions_sell.append(f"(df['close'] > {upper})")
elif operator == 'price_above_mid':
conditions_buy.append(f"(df['close'] > {mid})")
conditions_sell.append(f"(df['close'] < {mid})")
elif operator == 'price_below_mid':
conditions_buy.append(f"(df['close'] < {mid})")
conditions_sell.append(f"(df['close'] > {mid})")
elif operator == 'cross_up_lower':
conditions_buy.append(f"(df['close'] > {lower}) & (df['close'].shift(1) <= {lower}.shift(1))")
conditions_sell.append(f"(df['close'] < {upper}) & (df['close'].shift(1) >= {upper}.shift(1))")
elif operator == 'cross_down_upper':
conditions_buy.append(f"(df['close'] < {upper}) & (df['close'].shift(1) >= {upper}.shift(1))")
conditions_sell.append(f"(df['close'] > {lower}) & (df['close'].shift(1) <= {lower}.shift(1))")
elif ind == 'kdj':
period = params.get('period', 9)
signal_period = params.get('signal_period', 3)
operator = rule.get('operator', 'k_gt_d')
k_col = f"df['kdj_{period}_{signal_period}_k']"
d_col = f"df['kdj_{period}_{signal_period}_d']"
if operator == 'k_gt_d':
conditions_buy.append(f"({k_col} > {d_col})")
conditions_sell.append(f"({k_col} < {d_col})")
elif operator == 'k_lt_d':
conditions_buy.append(f"({k_col} < {d_col})")
conditions_sell.append(f"({k_col} > {d_col})")
elif operator == 'gold_cross':
conditions_buy.append(f"({k_col} > {d_col}) & ({k_col}.shift(1) <= {d_col}.shift(1))")
conditions_sell.append(f"({k_col} < {d_col}) & ({k_col}.shift(1) >= {d_col}.shift(1))")
elif operator == 'death_cross':
conditions_buy.append(f"({k_col} < {d_col}) & ({k_col}.shift(1) >= {d_col}.shift(1))")
conditions_sell.append(f"({k_col} > {d_col}) & ({k_col}.shift(1) <= {d_col}.shift(1))")
elif ind == 'ma':
period = params.get('period', 20)
ma_type = params.get('ma_type', 'sma')
operator = rule.get('operator', 'price_above')
col = f"df['ma_{ma_type}_{period}']"
if operator == 'price_above':
conditions_buy.append(f"(df['close'] > {col})")
conditions_sell.append(f"(df['close'] < {col})")
elif operator == 'price_below':
conditions_buy.append(f"(df['close'] < {col})")
conditions_sell.append(f"(df['close'] > {col})")
elif operator == 'cross_up':
conditions_buy.append(f"(df['close'] > {col}) & (df['close'].shift(1) <= {col}.shift(1))")
conditions_sell.append(f"(df['close'] < {col}) & (df['close'].shift(1) >= {col}.shift(1))")
elif operator == 'cross_down':
conditions_buy.append(f"(df['close'] < {col}) & (df['close'].shift(1) >= {col}.shift(1))")
conditions_sell.append(f"(df['close'] > {col}) & (df['close'].shift(1) <= {col}.shift(1))")
if conditions_buy:
code += f"\ndf['raw_buy'] = {' & '.join(conditions_buy)}\n"
if conditions_sell:
code += f"\ndf['raw_sell'] = {' & '.join(conditions_sell)}\n"
return code
def _get_core_loop(self, pos_config, pyr_rules, risk_mgmt):
# This mirrors the logic in code2.py loop
return """
# ===========================
# 4. Core Loop (Backtest)
# ===========================
open_long_signals = [False] * len(df)
add_long_signals = [False] * len(df)
close_long_signals = [False] * len(df)
open_long_text = [None] * len(df)
add_long_text = [None] * len(df)
open_long_price = [0.0] * len(df)
add_long_price = [0.0] * len(df)
close_long_price = [0.0] * len(df)
close_long_text = [None] * len(df)
open_short_signals = [False] * len(df)
add_short_signals = [False] * len(df)
close_short_signals = [False] * len(df)
open_short_price = [0.0] * len(df)
add_short_price = [0.0] * len(df)
close_short_price = [0.0] * len(df)
close_short_text = [None] * len(df)
position = 0 # 0, 1 (Long), -1 (Short)
position_count = 0
avg_entry_price = 0.0
last_add_price = 0.0
highest_price = 0.0 # For Long: Highest High; For Short: Lowest Low
close_arr = df['close'].values
high_arr = df['high'].values
low_arr = df['low'].values
raw_buy_arr = df['raw_buy'].values
raw_sell_arr = df['raw_sell'].values
for i in range(len(df)):
current_close = close_arr[i]
current_high = high_arr[i]
current_low = low_arr[i]
if position == 1:
# Long Position
if current_high > highest_price:
highest_price = current_high
profit_pct = (highest_price - avg_entry_price) / avg_entry_price
current_profit_pct = (current_close - avg_entry_price) / avg_entry_price
# 1. Trailing Stop
if take_profit_activation > 0 and profit_pct >= take_profit_activation:
drawdown = (highest_price - current_close) / avg_entry_price
if drawdown >= trailing_callback:
close_long_signals[i] = True
close_long_price[i] = current_close
close_long_text[i] = "Trailing Stop"
position = 0
position_count = 0
continue
# 2. Stop Loss
if stop_loss_pct > 0:
loss_pct = (avg_entry_price - current_low) / avg_entry_price
if loss_pct >= stop_loss_pct:
close_long_signals[i] = True
close_long_price[i] = avg_entry_price * (1 - stop_loss_pct)
close_long_text[i] = "Stop Loss"
position = 0
position_count = 0
continue
# 3. Signal Exit (if enabled)
# Note: Code2 uses raw_sell_arr for exit
if raw_sell_arr[i]:
close_long_signals[i] = True
close_long_price[i] = current_close
close_long_text[i] = "Signal Exit"
position = 0
position_count = 0
# Reverse to Short if trade_direction allows (simplified here)
# For now we just close.
continue
# 4. Pyramiding (Add Long)
if max_pyramiding > 0 and position_count < max_pyramiding + 1 and current_profit_pct > 0:
# Condition: Price rise by threshold
if add_threshold_pct > 0:
target_price = last_add_price * (1 + add_threshold_pct)
if current_high >= target_price:
add_long_signals[i] = True
add_long_price[i] = target_price
add_long_text[i] = "Add Long"
position_count += 1
last_add_price = target_price
elif position == -1:
# Short Position
# For Short, highest_price tracks the LOWEST price (best profit scenario)
if highest_price == 0: highest_price = avg_entry_price
if current_low < highest_price:
highest_price = current_low
# Profit: (Entry - Lowest) / Entry
profit_pct = (avg_entry_price - highest_price) / avg_entry_price
current_profit_pct = (avg_entry_price - current_close) / avg_entry_price
# 1. Trailing Stop
if take_profit_activation > 0 and profit_pct >= take_profit_activation:
# Drawdown: (Current - Lowest) / Entry
drawdown = (current_close - highest_price) / avg_entry_price
if drawdown >= trailing_callback:
close_short_signals[i] = True
close_short_price[i] = current_close
close_short_text[i] = "Trailing Stop"
position = 0
position_count = 0
continue
# 2. Stop Loss
if stop_loss_pct > 0:
# Loss: Price went up. (High - Entry) / Entry
loss_pct = (current_high - avg_entry_price) / avg_entry_price
if loss_pct >= stop_loss_pct:
close_short_signals[i] = True
close_short_price[i] = avg_entry_price * (1 + stop_loss_pct)
close_short_text[i] = "Stop Loss"
position = 0
position_count = 0
continue
# 3. Signal Exit
if raw_buy_arr[i]:
close_short_signals[i] = True
close_short_price[i] = current_close
close_short_text[i] = "Signal Exit"
position = 0
position_count = 0
continue
# 4. Pyramiding (Add Short)
if max_pyramiding > 0 and position_count < max_pyramiding + 1 and current_profit_pct > 0:
# Condition: Price drop by threshold
if add_threshold_pct > 0:
target_price = last_add_price * (1 - add_threshold_pct)
if current_low <= target_price:
add_short_signals[i] = True
add_short_price[i] = target_price
add_short_text[i] = "Add Short"
position_count += 1
last_add_price = target_price
else:
# No Position
if raw_buy_arr[i]:
open_long_signals[i] = True
open_long_price[i] = current_close
open_long_text[i] = "Open Long"
position = 1
position_count = 1
avg_entry_price = current_close
last_add_price = current_close
highest_price = current_close
elif raw_sell_arr[i]:
open_short_signals[i] = True
open_short_price[i] = current_close
open_short_text[i] = "Open Short"
position = -1
position_count = 1
avg_entry_price = current_close
last_add_price = current_close
highest_price = current_close # Init with Entry
# Append columns
df['open_long'] = open_long_signals
df['add_long'] = add_long_signals
df['close_long'] = close_long_signals
df['open_long_price'] = [p if s else None for p, s in zip(open_long_price, open_long_signals)]
df['add_long_price'] = [p if s else None for p, s in zip(add_long_price, add_long_signals)]
df['close_long_price'] = [p if s else None for p, s in zip(close_long_price, close_long_signals)]
df['open_long_text'] = open_long_text
df['add_long_text'] = add_long_text
df['close_long_text'] = close_long_text
"""
def _get_output_section(self, name, rules):
# Generate plot configs based on indicators
plots = []
for rule in rules:
ind = rule.get('indicator')
params = rule.get('params', {})
if ind == 'supertrend':
plots.append({
"name": "SuperTrend Up", "type": "line", "data": "df['st_lower'].tolist()", "color": "#00FF00", "overlay": True
})
plots.append({
"name": "SuperTrend Down", "type": "line", "data": "df['st_upper'].tolist()", "color": "#FF0000", "overlay": True
})
elif ind == 'ema':
p = params.get('period', 20)
plots.append({
"name": f"EMA {p}", "type": "line", "data": f"df['ema_{p}'].tolist()", "color": "#FFA500", "overlay": True
})
elif ind == 'ma':
p = params.get('period', 20)
t = params.get('ma_type', 'sma')
plots.append({
"name": f"{t.upper()} {p}", "type": "line", "data": f"df['ma_{t}_{p}'].tolist()", "color": "#FFA500", "overlay": True
})
elif ind == 'bollinger':
p = params.get('period', 20)
d = params.get('std_dev', 2.0)
plots.append({
"name": "BB Upper", "type": "line", "data": f"df['bb_{p}_{d}_upper'].tolist()", "color": "#0088FE", "overlay": True
})
plots.append({
"name": "BB Lower", "type": "line", "data": f"df['bb_{p}_{d}_lower'].tolist()", "color": "#0088FE", "overlay": True
})
# MACD, RSI, KDJ are typically separate panes, not overlay. The `overlay` param controls this.
elif ind == 'macd':
f = params.get('fast_period', 12)
s = params.get('slow_period', 26)
si = params.get('signal_period', 9)
plots.append({
"name": "MACD", "type": "line", "data": f"df['macd_{f}_{s}_{si}_line'].tolist()", "color": "#0088FE", "overlay": False
})
plots.append({
"name": "Signal", "type": "line", "data": f"df['macd_{f}_{s}_{si}_signal'].tolist()", "color": "#FF8042", "overlay": False
})
elif ind == 'rsi':
p = params.get('period', 14)
plots.append({
"name": f"RSI {p}", "type": "line", "data": f"df['rsi_{p}'].tolist()", "color": "#8884d8", "overlay": False
})
elif ind == 'kdj':
p = params.get('period', 9)
si = params.get('signal_period', 3)
plots.append({
"name": "K", "type": "line", "data": f"df['kdj_{p}_{si}_k'].tolist()", "color": "#8884d8", "overlay": False
})
plots.append({
"name": "D", "type": "line", "data": f"df['kdj_{p}_{si}_d'].tolist()", "color": "#82ca9d", "overlay": False
})
plots.append({
"name": "J", "type": "line", "data": f"df['kdj_{p}_{si}_j'].tolist()", "color": "#ffc658", "overlay": False
})
# Convert plots to string representation valid in Python
plots_py = "[\n"
for p in plots:
plots_py += f" {{'name': '{p['name']}', 'type': '{p['type']}', 'data': {p['data']}, 'color': '{p['color']}', 'overlay': {p['overlay']}}},\n"
plots_py += "]"
return f"""
# ===========================
# 5. Output
# ===========================
output = {{
"name": "{name}",
"plots": {plots_py},
"signals": [
{{
"name": "Open Long",
"type": "buy",
"data": df['open_long_price'].tolist(),
"color": "#00FF00",
"text": "Open Long"
}},
{{
"name": "Add Long",
"type": "buy",
"data": df['add_long_price'].tolist(),
"color": "#00DD00",
"text": "Add Long"
}},
{{
"name": "Close Long",
"type": "sell",
"data": df['close_long_price'].tolist(),
"color": "#FF6600",
"text": "Close Long"
}},
{{
"name": "Open Short",
"type": "sell",
"data": df['open_short_price'].tolist(),
"color": "#FF0000",
"text": "Open Short"
}},
{{
"name": "Add Short",
"type": "sell",
"data": df['add_short_price'].tolist(),
"color": "#DD0000",
"text": "Add Short"
}},
{{
"name": "Close Short",
"type": "buy",
"data": df['close_short_price'].tolist(),
"color": "#00CCFF",
"text": "Close Short"
}}
]
}}
"""