Python generators is fully implemented but not tested yet.
This commit is contained in:
parent
9d830fe8fa
commit
4fcc6f661d
|
|
@ -2,11 +2,12 @@ numpy<2.0.0
|
||||||
flask==3.0.3
|
flask==3.0.3
|
||||||
config~=0.5.1
|
config~=0.5.1
|
||||||
PyYAML==6.0.2
|
PyYAML==6.0.2
|
||||||
requests==2.30.0
|
|
||||||
pandas==2.2.3
|
pandas==2.2.3
|
||||||
passlib~=1.7.4
|
passlib~=1.7.4
|
||||||
ccxt==4.4.8
|
ccxt==4.4.8
|
||||||
flask-socketio
|
flask-socketio~=5.4.1
|
||||||
pytz==2024.2
|
pytz==2024.2
|
||||||
backtrader==1.9.78.123
|
backtrader==1.9.78.123
|
||||||
eventlet~=0.37.0
|
eventlet~=0.37.0
|
||||||
|
Flask-Cors~=3.0.10
|
||||||
|
email_validator~=2.2.0
|
||||||
File diff suppressed because it is too large
Load Diff
1371
src/Strategies.py
1371
src/Strategies.py
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,411 @@
|
||||||
|
import logging
|
||||||
|
from DataCache_v3 import DataCache
|
||||||
|
from indicators import Indicators
|
||||||
|
from trade import Trades
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class StrategyInstance:
|
||||||
|
def __init__(self, strategy_instance_id: str, strategy_id: str, strategy_name: str,
|
||||||
|
user_id: int, generated_code: str, data_cache: DataCache, indicators: Indicators, trades: Trades):
|
||||||
|
"""
|
||||||
|
Initializes a StrategyInstance.
|
||||||
|
|
||||||
|
:param strategy_instance_id: Unique identifier for this strategy execution instance.
|
||||||
|
:param strategy_id: Identifier of the strategy definition.
|
||||||
|
:param strategy_name: Name of the strategy.
|
||||||
|
:param user_id: ID of the user who owns the strategy.
|
||||||
|
:param generated_code: The generated 'next()' method code.
|
||||||
|
:param data_cache: Reference to the DataCache instance.
|
||||||
|
:param indicators: Reference to the Indicators manager.
|
||||||
|
:param trades: Reference to the Trades manager.
|
||||||
|
"""
|
||||||
|
self.strategy_instance_id = strategy_instance_id
|
||||||
|
self.strategy_id = strategy_id
|
||||||
|
self.strategy_name = strategy_name
|
||||||
|
self.user_id = user_id
|
||||||
|
self.generated_code = generated_code
|
||||||
|
self.data_cache = data_cache
|
||||||
|
self.indicators = indicators
|
||||||
|
self.trades = trades
|
||||||
|
|
||||||
|
# Initialize context variables
|
||||||
|
self.flags: dict[str, Any] = {}
|
||||||
|
self.starting_balance = self.trades.get_current_balance(self.user_id)
|
||||||
|
self.profit_loss: float = 0.0
|
||||||
|
self.active: bool = True
|
||||||
|
self.paused: bool = False
|
||||||
|
self.exit: bool = False
|
||||||
|
self.exit_method: str = 'all'
|
||||||
|
self.start_time = dt.datetime.now()
|
||||||
|
|
||||||
|
def load_context(self):
|
||||||
|
"""
|
||||||
|
Loads the strategy execution context from the database.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
context_data = self.data_cache.get_rows_from_datacache(
|
||||||
|
cache_name='strategy_contexts',
|
||||||
|
filter_vals=[('strategy_instance_id', self.strategy_instance_id)]
|
||||||
|
)
|
||||||
|
if context_data.empty:
|
||||||
|
logger.warning(f"No context found for StrategyInstance ID: {self.strategy_instance_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
context = context_data.iloc[0].to_dict()
|
||||||
|
self.flags = json.loads(context.get('flags', '{}'))
|
||||||
|
self.starting_balance = context.get('starting_balance', 0.0)
|
||||||
|
self.profit_loss = context.get('profit_loss', 0.0)
|
||||||
|
self.active = context.get('active', True)
|
||||||
|
self.paused = context.get('paused', False)
|
||||||
|
self.exit = context.get('exit', False)
|
||||||
|
self.exit_method = context.get('exit_method', 'all')
|
||||||
|
|
||||||
|
context_start_time = context.get('start_time', None)
|
||||||
|
if context_start_time:
|
||||||
|
self.start_time = dt.datetime.fromisoformat(context_start_time)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading context for StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def save_context(self):
|
||||||
|
"""
|
||||||
|
Saves the current strategy execution context to the database.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.data_cache.modify_datacache_item(
|
||||||
|
cache_name='strategy_contexts',
|
||||||
|
filter_vals=[('strategy_instance_id', self.strategy_instance_id)],
|
||||||
|
field_names=('flags', 'profit_loss', 'active', 'paused', 'exit', 'exit_method', 'start_time'),
|
||||||
|
new_values=(
|
||||||
|
json.dumps(self.flags),
|
||||||
|
self.profit_loss,
|
||||||
|
self.active,
|
||||||
|
self.paused,
|
||||||
|
self.exit,
|
||||||
|
self.exit_method,
|
||||||
|
self.start_time.isoformat()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error saving context for StrategyInstance '{self.strategy_instance_id}': {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def execute(self) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Executes the strategy's 'next()' method.
|
||||||
|
:return: Result of the execution.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Define the local execution environment
|
||||||
|
exec_context = {
|
||||||
|
'flags': self.flags,
|
||||||
|
'strategy_id': self.strategy_id,
|
||||||
|
'user_id': self.user_id,
|
||||||
|
'get_last_candle': self.get_last_candle,
|
||||||
|
'get_current_price': self.get_current_price, # Added method
|
||||||
|
'buy': self.buy_order,
|
||||||
|
'sell': self.sell_order,
|
||||||
|
'exit_strategy': self.exit_strategy,
|
||||||
|
'notify_user': self.notify_user,
|
||||||
|
'process_indicator': self.process_indicator,
|
||||||
|
'get_strategy_profit_loss': self.get_strategy_profit_loss,
|
||||||
|
'is_in_profit': self.is_in_profit,
|
||||||
|
'is_in_loss': self.is_in_loss,
|
||||||
|
'get_active_trades': self.get_active_trades,
|
||||||
|
'get_starting_balance': self.get_starting_balance,
|
||||||
|
'set_paused': self.set_paused,
|
||||||
|
'set_exit': self.set_exit
|
||||||
|
}
|
||||||
|
|
||||||
|
# Execute the generated 'next()' method
|
||||||
|
exec(self.generated_code, {}, exec_context)
|
||||||
|
|
||||||
|
# Call the 'next()' method
|
||||||
|
if 'next' in exec_context and callable(exec_context['next']):
|
||||||
|
exec_context['next']()
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"'next' method not defined in generated_code for StrategyInstance '{self.strategy_instance_id}'.")
|
||||||
|
|
||||||
|
# Retrieve and update profit/loss
|
||||||
|
self.profit_loss = exec_context.get('profit_loss', self.profit_loss)
|
||||||
|
self.save_context()
|
||||||
|
|
||||||
|
return {"success": True, "profit_loss": self.profit_loss}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing 'next()' for StrategyInstance '{self.strategy_instance_id}': {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
return {"success": False, "message": str(e)}
|
||||||
|
|
||||||
|
def set_paused(self, value: bool):
|
||||||
|
"""
|
||||||
|
Sets the paused state of the strategy.
|
||||||
|
:param value: True to pause, False to resume.
|
||||||
|
"""
|
||||||
|
self.paused = value
|
||||||
|
self.save_context()
|
||||||
|
logger.debug(f"Strategy '{self.strategy_id}' paused: {self.paused}")
|
||||||
|
|
||||||
|
def set_exit(self, exit_flag: bool, exit_method: str = 'all'):
|
||||||
|
"""
|
||||||
|
Sets the exit state and method of the strategy.
|
||||||
|
:param exit_flag: True to initiate exit.
|
||||||
|
:param exit_method: Method to use for exiting ('all', 'in_profit', 'in_loss').
|
||||||
|
"""
|
||||||
|
self.exit = exit_flag
|
||||||
|
self.exit_method = exit_method
|
||||||
|
self.save_context()
|
||||||
|
logger.debug(f"Strategy '{self.strategy_id}' exit set: {self.exit} with method '{self.exit_method}'")
|
||||||
|
|
||||||
|
def get_total_filled_order_volume(self) -> float:
|
||||||
|
"""
|
||||||
|
Retrieves the total filled order volume for the strategy.
|
||||||
|
"""
|
||||||
|
return self.trades.get_total_filled_order_volume(self.strategy_id)
|
||||||
|
|
||||||
|
def get_total_unfilled_order_volume(self) -> float:
|
||||||
|
"""
|
||||||
|
Retrieves the total unfilled order volume for the strategy.
|
||||||
|
"""
|
||||||
|
return self.trades.get_total_unfilled_order_volume(self.strategy_id)
|
||||||
|
|
||||||
|
def get_last_candle(self, candle_part: str, timeframe: str, exchange: str, symbol: str):
|
||||||
|
"""
|
||||||
|
Retrieves the last candle data based on provided parameters.
|
||||||
|
|
||||||
|
:param candle_part: Part of the candle data (e.g., 'close', 'open').
|
||||||
|
:param timeframe: Timeframe of the candle.
|
||||||
|
:param exchange: Exchange name.
|
||||||
|
:param symbol: Trading symbol.
|
||||||
|
:return: Last candle value.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sdt = dt.datetime.now() - dt.timedelta(minutes=int(timeframe[:-1]))
|
||||||
|
data = self.data_cache.get_records_since(start_datetime=sdt, ex_details=[exchange, symbol, timeframe])
|
||||||
|
if not data.empty:
|
||||||
|
return data.iloc[-1][candle_part]
|
||||||
|
else:
|
||||||
|
logger.warning(f"No candle data found for {exchange} {symbol} {timeframe}.")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving last candle: {e}", exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_current_price(self, timeframe: str = '1h', exchange: str = 'binance',
|
||||||
|
symbol: str = 'BTC/USD') -> float | None:
|
||||||
|
"""
|
||||||
|
Retrieves the current market price for the specified symbol.
|
||||||
|
|
||||||
|
:param timeframe: The timeframe of the data.
|
||||||
|
:param exchange: The exchange name.
|
||||||
|
:param symbol: The trading symbol.
|
||||||
|
:return: The current price or None if unavailable.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Assuming get_last_candle returns the last candle data as a dictionary
|
||||||
|
last_candle = self.get_last_candle('close', timeframe, exchange, symbol)
|
||||||
|
if last_candle is not None:
|
||||||
|
logger.debug(f"Retrieved current price for {symbol} on {exchange} ({timeframe}): {last_candle}")
|
||||||
|
return last_candle
|
||||||
|
else:
|
||||||
|
logger.warning(f"No last candle data available for {symbol} on {exchange} ({timeframe}).")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving current price for {symbol} on {exchange} ({timeframe}): {e}", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Define helper methods
|
||||||
|
def buy_order(self, size: float, symbol: str, order_type: str = 'market', price: float | None = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Executes a buy order.
|
||||||
|
|
||||||
|
:param size: Quantity to buy.
|
||||||
|
:param symbol: Trading symbol.
|
||||||
|
:param order_type: Type of order ('market' or 'limit').
|
||||||
|
:param price: Price for limit orders.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
order_data = {
|
||||||
|
'size': size,
|
||||||
|
'symbol': symbol,
|
||||||
|
'order_type': order_type.lower(),
|
||||||
|
'price': price,
|
||||||
|
**kwargs
|
||||||
|
}
|
||||||
|
status, msg = self.trades.buy(order_data, self.user_id)
|
||||||
|
if status != 'success':
|
||||||
|
logger.error(f"Buy order failed: {msg}")
|
||||||
|
self.notify_user(f"Buy order failed: {msg}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Buy order executed successfully: {msg}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing buy order in StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def sell_order(self, size: float, symbol: str, order_type: str = 'market', price: float | None = None, **kwargs):
|
||||||
|
"""
|
||||||
|
Executes a sell order.
|
||||||
|
|
||||||
|
:param size: Quantity to sell.
|
||||||
|
:param symbol: Trading symbol.
|
||||||
|
:param order_type: Type of order ('market' or 'limit').
|
||||||
|
:param price: Price for limit orders.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
order_data = {
|
||||||
|
'size': size,
|
||||||
|
'symbol': symbol,
|
||||||
|
'order_type': order_type.lower(),
|
||||||
|
'price': price,
|
||||||
|
**kwargs
|
||||||
|
}
|
||||||
|
status, msg = self.trades.sell(order_data, self.user_id)
|
||||||
|
if status != 'success':
|
||||||
|
logger.error(f"Sell order failed: {msg}")
|
||||||
|
self.notify_user(f"Sell order failed: {msg}")
|
||||||
|
else:
|
||||||
|
logger.info(f"Sell order executed successfully: {msg}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error executing sell order in StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def exit_strategy(self):
|
||||||
|
"""
|
||||||
|
Exits the strategy based on the exit_method.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.exit_method == 'all':
|
||||||
|
self.trades.exit_strategy_all(self.strategy_id)
|
||||||
|
logger.info(f"Exiting all positions for strategy '{self.strategy_id}'.")
|
||||||
|
elif self.exit_method == 'in_profit':
|
||||||
|
self.trades.exit_strategy_in_profit(self.strategy_id)
|
||||||
|
logger.info(f"Exiting profitable positions for strategy '{self.strategy_id}'.")
|
||||||
|
elif self.exit_method == 'in_loss':
|
||||||
|
self.trades.exit_strategy_in_loss(self.strategy_id)
|
||||||
|
logger.info(f"Exiting losing positions for strategy '{self.strategy_id}'.")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"Unknown exit method '{self.exit_method}' for StrategyInstance '{self.strategy_instance_id}'.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error exiting strategy '{self.strategy_id}' in StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def notify_user(self, message: str):
|
||||||
|
"""
|
||||||
|
Sends a notification to the user.
|
||||||
|
|
||||||
|
:param message: Notification message.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.trades.notify_user(self.user_id, message)
|
||||||
|
logger.debug(f"Notification sent to user '{self.user_id}': {message}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error notifying user '{self.user_id}' in StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def process_indicator(self, indicator_name: str, output_field: str) -> Any:
|
||||||
|
"""
|
||||||
|
Retrieves the latest value of an indicator.
|
||||||
|
|
||||||
|
:param indicator_name: Name of the indicator.
|
||||||
|
:param output_field: Specific field of the indicator.
|
||||||
|
:return: Indicator value.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
user_indicators = self.indicators.get_indicator_list(user_id=self.user_id)
|
||||||
|
indicator = user_indicators.get(indicator_name)
|
||||||
|
if not indicator:
|
||||||
|
logger.error(f"Indicator '{indicator_name}' not found for user '{self.user_id}'.")
|
||||||
|
return None
|
||||||
|
indicator_value = self.indicators.process_indicator(indicator)
|
||||||
|
value = indicator_value.get(output_field, None)
|
||||||
|
logger.debug(f"Processed indicator '{indicator_name}' with output field '{output_field}': {value}")
|
||||||
|
return value
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error processing indicator '{indicator_name}' in StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_strategy_profit_loss(self, strategy_id: str) -> float:
|
||||||
|
"""
|
||||||
|
Retrieves the current profit or loss of the strategy.
|
||||||
|
|
||||||
|
:param strategy_id: Unique identifier of the strategy.
|
||||||
|
:return: Profit or loss amount.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
profit_loss = self.trades.get_profit(strategy_id)
|
||||||
|
logger.debug(f"Retrieved profit/loss for strategy '{strategy_id}': {profit_loss}")
|
||||||
|
return profit_loss
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error retrieving profit/loss for StrategyInstance '{self.strategy_instance_id}': {e}",
|
||||||
|
exc_info=True)
|
||||||
|
traceback.print_exc()
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def is_in_profit(self) -> bool:
|
||||||
|
"""
|
||||||
|
Determines if the strategy is currently in profit.
|
||||||
|
"""
|
||||||
|
profit = self.profit_loss
|
||||||
|
logger.debug(f"Checking if in profit: {profit} > 0")
|
||||||
|
return self.profit_loss > 0
|
||||||
|
|
||||||
|
def is_in_loss(self) -> bool:
|
||||||
|
"""
|
||||||
|
Determines if the strategy is currently in loss.
|
||||||
|
"""
|
||||||
|
loss = self.profit_loss
|
||||||
|
logger.debug(f"Checking if in loss: {loss} < 0")
|
||||||
|
return self.profit_loss < 0
|
||||||
|
|
||||||
|
def get_active_trades(self) -> int:
|
||||||
|
"""
|
||||||
|
Returns the number of active trades.
|
||||||
|
"""
|
||||||
|
active_trades_count = len(self.trades.active_trades)
|
||||||
|
logger.debug(f"Number of active trades: {active_trades_count}")
|
||||||
|
return active_trades_count
|
||||||
|
|
||||||
|
def get_starting_balance(self) -> float:
|
||||||
|
"""
|
||||||
|
Returns the starting balance.
|
||||||
|
"""
|
||||||
|
logger.debug(f"Starting balance: {self.starting_balance}")
|
||||||
|
return self.starting_balance
|
||||||
|
|
||||||
|
def get_filled_orders(self) -> int:
|
||||||
|
"""
|
||||||
|
Retrieves the number of filled orders for the strategy.
|
||||||
|
"""
|
||||||
|
return self.trades.get_filled_orders_count(self.strategy_id)
|
||||||
|
|
||||||
|
def get_unfilled_orders(self) -> int:
|
||||||
|
"""
|
||||||
|
Retrieves the number of unfilled orders for the strategy.
|
||||||
|
"""
|
||||||
|
return self.trades.get_unfilled_orders_count(self.strategy_id)
|
||||||
|
|
||||||
|
def get_available_balance(self) -> float:
|
||||||
|
"""
|
||||||
|
Retrieves the available balance for the strategy.
|
||||||
|
"""
|
||||||
|
return self.trades.get_available_balance(self.strategy_id)
|
||||||
|
|
@ -31,7 +31,7 @@ export function defineTradeOrderBlocks() {
|
||||||
"args0": [
|
"args0": [
|
||||||
{
|
{
|
||||||
"type": "field_dropdown",
|
"type": "field_dropdown",
|
||||||
"name": "tradeType",
|
"name": "trade_type",
|
||||||
"options": [
|
"options": [
|
||||||
["Buy", "buy"],
|
["Buy", "buy"],
|
||||||
["Sell", "sell"]
|
["Sell", "sell"]
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ export function defineTradeOrderGenerators() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve and validate the trade type
|
// Retrieve and validate the trade type
|
||||||
const tradeType = block.getFieldValue('tradeType') || 'buy';
|
const tradeType = block.getFieldValue('trade_type') || 'buy';
|
||||||
const validatedTradeType = validateTradeType(tradeType);
|
const validatedTradeType = validateTradeType(tradeType);
|
||||||
|
|
||||||
// Retrieve and process the trade size
|
// Retrieve and process the trade size
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ export function defineIndicatorBlocks() {
|
||||||
for (let indicatorName in indicatorOutputs) {
|
for (let indicatorName in indicatorOutputs) {
|
||||||
const outputs = indicatorOutputs[indicatorName];
|
const outputs = indicatorOutputs[indicatorName];
|
||||||
|
|
||||||
// Create a unique block type for each indicator
|
// Create a unique block type by replacing spaces with underscores
|
||||||
const blockType = 'indicator_' + indicatorName;
|
const sanitizedIndicatorName = indicatorName.replace(/\s+/g, '_'); // Replace spaces with underscores
|
||||||
|
const blockType = 'indicator_' + sanitizedIndicatorName;
|
||||||
|
|
||||||
// Define the block for this indicator
|
// Define the block for this indicator
|
||||||
Blockly.defineBlocksWithJsonArray([{
|
Blockly.defineBlocksWithJsonArray([{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue