Compare commits
6 Commits
fix/issue-
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da8327c0e0 | ||
| 8d33ea9a44 | |||
|
|
d81464b869 | ||
| 55b008d4e8 | |||
|
|
04e4c1a487 | ||
| feb65131fa |
@@ -1,5 +1,5 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from typing import Optional, List, Any
|
||||
from pydantic import BaseModel, EmailStr, field_validator
|
||||
from typing import Optional, List, Any, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@@ -69,6 +69,13 @@ class BacktestCreate(BaseModel):
|
||||
start_date: str
|
||||
end_date: str
|
||||
|
||||
@field_validator("chain")
|
||||
@classmethod
|
||||
def chain_must_be_bsc(cls, v: str) -> str:
|
||||
if v != "bsc":
|
||||
raise ValueError("Phase 1 only supports BSC (bnb chain)")
|
||||
return v
|
||||
|
||||
|
||||
class BacktestResponse(BaseModel):
|
||||
id: str
|
||||
@@ -90,6 +97,13 @@ class SimulationCreate(BaseModel):
|
||||
check_interval: int = 60
|
||||
auto_execute: bool = False
|
||||
|
||||
@field_validator("chain")
|
||||
@classmethod
|
||||
def chain_must_be_bsc(cls, v: str) -> str:
|
||||
if v != "bsc":
|
||||
raise ValueError("Phase 1 only supports BSC (bnb chain)")
|
||||
return v
|
||||
|
||||
|
||||
class SimulationResponse(BaseModel):
|
||||
id: str
|
||||
|
||||
@@ -33,29 +33,24 @@ class StrategyValidator:
|
||||
errors.append(f"Condition {i}: unsupported type '{cond_type}'")
|
||||
continue
|
||||
|
||||
params = condition.get("params", {})
|
||||
if cond_type in ["price_drop", "price_rise", "volume_spike"]:
|
||||
if "token" not in params:
|
||||
if "token" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'token'")
|
||||
if "threshold_percent" not in params:
|
||||
errors.append(f"Condition {i}: missing 'threshold_percent'")
|
||||
elif not isinstance(params["threshold_percent"], (int, float)):
|
||||
errors.append(
|
||||
f"Condition {i}: 'threshold_percent' must be a number"
|
||||
)
|
||||
elif params["threshold_percent"] <= 0:
|
||||
errors.append(
|
||||
f"Condition {i}: 'threshold_percent' must be positive"
|
||||
)
|
||||
if "threshold" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'threshold'")
|
||||
elif not isinstance(condition["threshold"], (int, float)):
|
||||
errors.append(f"Condition {i}: 'threshold' must be a number")
|
||||
elif condition["threshold"] <= 0:
|
||||
errors.append(f"Condition {i}: 'threshold' must be positive")
|
||||
|
||||
elif cond_type == "price_level":
|
||||
if "token" not in params:
|
||||
if "token" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'token'")
|
||||
if "price" not in params:
|
||||
if "price" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'price'")
|
||||
if "direction" not in params:
|
||||
if "direction" not in condition:
|
||||
errors.append(f"Condition {i}: missing 'direction'")
|
||||
elif params["direction"] not in ["above", "below"]:
|
||||
elif condition["direction"] not in ["above", "below"]:
|
||||
errors.append(
|
||||
f"Condition {i}: direction must be 'above' or 'below'"
|
||||
)
|
||||
@@ -85,23 +80,22 @@ class StrategyExplainer:
|
||||
explanations.append("This strategy will trigger when:")
|
||||
for cond in cond_list:
|
||||
cond_type = cond.get("type")
|
||||
params = cond.get("params", {})
|
||||
token = params.get("token", "the token")
|
||||
token = cond.get("token", "the token")
|
||||
|
||||
if cond_type == "price_drop":
|
||||
pct = params.get("threshold_percent", 0)
|
||||
pct = cond.get("threshold", 0)
|
||||
explanations.append(f" - {token} price drops by {pct}%")
|
||||
elif cond_type == "price_rise":
|
||||
pct = params.get("threshold_percent", 0)
|
||||
pct = cond.get("threshold", 0)
|
||||
explanations.append(f" - {token} price rises by {pct}%")
|
||||
elif cond_type == "volume_spike":
|
||||
pct = params.get("threshold_percent", 0)
|
||||
pct = cond.get("threshold", 0)
|
||||
explanations.append(
|
||||
f" - {token} trading volume increases by {pct}%"
|
||||
)
|
||||
elif cond_type == "price_level":
|
||||
price = params.get("price", 0)
|
||||
direction = params.get("direction", "unknown")
|
||||
price = cond.get("price", 0)
|
||||
direction = cond.get("direction", "unknown")
|
||||
explanations.append(
|
||||
f" - {token} price crosses {direction} ${price}"
|
||||
)
|
||||
|
||||
@@ -61,9 +61,9 @@ class MiniMaxConnector:
|
||||
system_prompt = """You are a trading strategy designer. Parse the user's natural language request into a JSON strategy_config object.
|
||||
|
||||
Supported conditions (MVP):
|
||||
- price_drop: Token price drops by X% (requires: token, threshold_percent)
|
||||
- price_rise: Token price rises by X% (requires: token, threshold_percent)
|
||||
- volume_spike: Trading volume increases X% (requires: token, threshold_percent)
|
||||
- price_drop: Token price drops by X% (requires: token, threshold)
|
||||
- price_rise: Token price rises by X% (requires: token, threshold)
|
||||
- volume_spike: Trading volume increases X% (requires: token, threshold)
|
||||
- price_level: Price crosses above/below X (requires: token, price, direction)
|
||||
|
||||
Output ONLY valid JSON with this schema:
|
||||
@@ -71,18 +71,17 @@ Output ONLY valid JSON with this schema:
|
||||
"conditions": [
|
||||
{
|
||||
"type": "price_drop|price_rise|volume_spike|price_level",
|
||||
"params": {
|
||||
"token": "TOKEN_SYMBOL",
|
||||
"threshold_percent": number, // for price_drop, price_rise, volume_spike
|
||||
"price": number, // for price_level
|
||||
"direction": "above|below" // for price_level
|
||||
}
|
||||
"token": "TOKEN_SYMBOL",
|
||||
"chain": "bsc",
|
||||
"threshold": number, // for price_drop, price_rise, volume_spike
|
||||
"price": number, // for price_level
|
||||
"direction": "above|below", // for price_level
|
||||
"timeframe": "1h"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "buy|sell|notify",
|
||||
"params": {}
|
||||
"type": "buy|sell|notify"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,10 +20,15 @@ class BacktestEngine:
|
||||
self.strategy_config = config.get("strategy_config", {})
|
||||
self.conditions = self.strategy_config.get("conditions", [])
|
||||
self.actions = self.strategy_config.get("actions", [])
|
||||
self.risk_management = self.strategy_config.get("risk_management", {})
|
||||
self.stop_loss_percent = self.risk_management.get("stop_loss_percent")
|
||||
self.take_profit_percent = self.risk_management.get("take_profit_percent")
|
||||
self.initial_balance = config.get("initial_balance", 10000.0)
|
||||
self.current_balance = self.initial_balance
|
||||
self.position = 0.0
|
||||
self.position_token = ""
|
||||
self.entry_price: Optional[float] = None
|
||||
self.entry_time: Optional[int] = None
|
||||
self.trades: List[Dict[str, Any]] = []
|
||||
self.running = False
|
||||
|
||||
@@ -103,11 +108,73 @@ class BacktestEngine:
|
||||
|
||||
timestamp = kline.get("timestamp", 0)
|
||||
|
||||
if self.position > 0 and self.entry_price is not None:
|
||||
exit_info = self._check_risk_management(price, timestamp)
|
||||
if exit_info:
|
||||
await self._execute_risk_exit(price, timestamp, exit_info)
|
||||
continue
|
||||
|
||||
for condition in self.conditions:
|
||||
if self._check_condition(condition, klines, i, price):
|
||||
await self._execute_actions(price, timestamp, condition)
|
||||
break
|
||||
|
||||
def _check_risk_management(
|
||||
self, current_price: float, timestamp: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if self.position <= 0 or self.entry_price is None:
|
||||
return None
|
||||
|
||||
if self.stop_loss_percent is not None:
|
||||
stop_loss_price = self.entry_price * (1 - self.stop_loss_percent / 100)
|
||||
if current_price <= stop_loss_price:
|
||||
return {"reason": "stop_loss", "price": stop_loss_price}
|
||||
|
||||
if self.take_profit_percent is not None:
|
||||
take_profit_price = self.entry_price * (1 + self.take_profit_percent / 100)
|
||||
if current_price >= take_profit_price:
|
||||
return {"reason": "take_profit", "price": take_profit_price}
|
||||
|
||||
return None
|
||||
|
||||
async def _execute_risk_exit(
|
||||
self, price: float, timestamp: int, exit_info: Dict[str, Any]
|
||||
):
|
||||
if self.position <= 0:
|
||||
return
|
||||
|
||||
reason = exit_info["reason"]
|
||||
sell_amount = self.position * price
|
||||
self.current_balance += sell_amount
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"amount": sell_amount,
|
||||
"quantity": self.position,
|
||||
"timestamp": timestamp,
|
||||
"exit_reason": reason,
|
||||
}
|
||||
)
|
||||
self.signals.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"confidence": 1.0,
|
||||
"reasoning": f"Risk management triggered {reason}",
|
||||
"executed": False,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.entry_time = None
|
||||
|
||||
def _check_condition(
|
||||
self,
|
||||
condition: Dict[str, Any],
|
||||
@@ -173,6 +240,8 @@ class BacktestEngine:
|
||||
self.position += amount / price
|
||||
self.current_balance -= amount
|
||||
self.position_token = token
|
||||
self.entry_price = price
|
||||
self.entry_time = timestamp
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "buy",
|
||||
@@ -209,9 +278,12 @@ class BacktestEngine:
|
||||
"amount": sell_amount,
|
||||
"quantity": self.position,
|
||||
"timestamp": timestamp,
|
||||
"exit_reason": "manual",
|
||||
}
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.entry_time = None
|
||||
self.signals.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
|
||||
@@ -20,6 +20,9 @@ class SimulateEngine:
|
||||
self.strategy_config = config.get("strategy_config", {})
|
||||
self.conditions = self.strategy_config.get("conditions", [])
|
||||
self.actions = self.strategy_config.get("actions", [])
|
||||
self.risk_management = self.strategy_config.get("risk_management", {})
|
||||
self.stop_loss_percent = self.risk_management.get("stop_loss_percent")
|
||||
self.take_profit_percent = self.risk_management.get("take_profit_percent")
|
||||
self.check_interval = config.get("check_interval", 60)
|
||||
self.duration_seconds = config.get("duration_seconds", 3600)
|
||||
self.auto_execute = config.get("auto_execute", False)
|
||||
@@ -29,6 +32,12 @@ class SimulateEngine:
|
||||
self.started_at: Optional[datetime] = None
|
||||
self.last_price: Optional[float] = None
|
||||
self.last_volume: Optional[float] = None
|
||||
self.position: float = 0.0
|
||||
self.position_token: str = ""
|
||||
self.entry_price: Optional[float] = None
|
||||
self.entry_time: Optional[int] = None
|
||||
self.current_balance: float = config.get("initial_balance", 10000.0)
|
||||
self.trades: List[Dict[str, Any]] = []
|
||||
|
||||
async def run(self) -> Dict[str, Any]:
|
||||
self.running = True
|
||||
@@ -94,11 +103,70 @@ class SimulateEngine:
|
||||
):
|
||||
timestamp = int(datetime.utcnow().timestamp() * 1000)
|
||||
|
||||
if self.position > 0 and self.entry_price is not None:
|
||||
exit_info = self._check_risk_management(current_price, timestamp)
|
||||
if exit_info:
|
||||
await self._execute_risk_exit(current_price, timestamp, exit_info)
|
||||
return
|
||||
|
||||
for condition in self.conditions:
|
||||
if self._check_condition(condition, current_price, current_volume):
|
||||
await self._execute_actions(current_price, timestamp, condition)
|
||||
break
|
||||
|
||||
def _check_risk_management(
|
||||
self, current_price: float, timestamp: int
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if self.position <= 0 or self.entry_price is None:
|
||||
return None
|
||||
|
||||
if self.stop_loss_percent is not None:
|
||||
stop_loss_price = self.entry_price * (1 - self.stop_loss_percent / 100)
|
||||
if current_price <= stop_loss_price:
|
||||
return {"reason": "stop_loss", "price": stop_loss_price}
|
||||
|
||||
if self.take_profit_percent is not None:
|
||||
take_profit_price = self.entry_price * (1 + self.take_profit_percent / 100)
|
||||
if current_price >= take_profit_price:
|
||||
return {"reason": "take_profit", "price": take_profit_price}
|
||||
|
||||
return None
|
||||
|
||||
async def _execute_risk_exit(
|
||||
self, price: float, timestamp: int, exit_info: Dict[str, Any]
|
||||
):
|
||||
if self.position <= 0:
|
||||
return
|
||||
|
||||
reason = exit_info["reason"]
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"quantity": self.position,
|
||||
"timestamp": timestamp,
|
||||
"exit_reason": reason,
|
||||
}
|
||||
)
|
||||
self.signals.append(
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": "sell",
|
||||
"token": self.position_token,
|
||||
"price": price,
|
||||
"confidence": 1.0,
|
||||
"reasoning": f"Risk management triggered {reason}",
|
||||
"executed": self.auto_execute,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
)
|
||||
self.position = 0
|
||||
self.entry_price = None
|
||||
self.entry_time = None
|
||||
|
||||
def _check_condition(
|
||||
self,
|
||||
condition: Dict[str, Any],
|
||||
@@ -146,20 +214,41 @@ class SimulateEngine:
|
||||
token = matched_condition.get("token", self.token)
|
||||
reasoning = f"Condition {matched_condition.get('type')} triggered"
|
||||
|
||||
signal = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": "signal",
|
||||
"token": token,
|
||||
"price": price,
|
||||
"confidence": 0.8,
|
||||
"reasoning": reasoning,
|
||||
"executed": self.auto_execute,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
for action in self.actions:
|
||||
action_type = action.get("type", "")
|
||||
if action_type == "buy":
|
||||
amount_percent = action.get("amount_percent", 10)
|
||||
amount = self.current_balance * (amount_percent / 100)
|
||||
self.position += amount / price
|
||||
self.position_token = token
|
||||
self.entry_price = price
|
||||
self.entry_time = timestamp
|
||||
self.current_balance -= amount
|
||||
self.trades.append(
|
||||
{
|
||||
"type": "buy",
|
||||
"token": token,
|
||||
"price": price,
|
||||
"amount": amount,
|
||||
"quantity": amount / price,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
)
|
||||
|
||||
self.signals.append(signal)
|
||||
signal = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"bot_id": self.bot_id,
|
||||
"run_id": self.run_id,
|
||||
"signal_type": action_type,
|
||||
"token": token,
|
||||
"price": price,
|
||||
"confidence": 0.8,
|
||||
"reasoning": reasoning,
|
||||
"executed": self.auto_execute,
|
||||
"created_at": datetime.utcnow(),
|
||||
}
|
||||
|
||||
self.signals.append(signal)
|
||||
|
||||
async def stop(self):
|
||||
self.running = False
|
||||
|
||||
Reference in New Issue
Block a user