mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 23:37:45 +00:00
167 lines
5.1 KiB
Python
167 lines
5.1 KiB
Python
"""
|
|
Position management for take profit/stop loss functionality.
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
from solders.pubkey import Pubkey
|
|
|
|
|
|
class ExitReason(Enum):
|
|
"""Reasons for position exit."""
|
|
|
|
TAKE_PROFIT = "take_profit"
|
|
STOP_LOSS = "stop_loss"
|
|
MAX_HOLD_TIME = "max_hold_time"
|
|
MANUAL = "manual"
|
|
|
|
|
|
@dataclass
|
|
class Position:
|
|
"""Represents an active trading position."""
|
|
|
|
# Token information
|
|
mint: Pubkey
|
|
symbol: str
|
|
|
|
# Position details
|
|
entry_price: float
|
|
quantity: float
|
|
entry_time: datetime
|
|
|
|
# Exit conditions
|
|
take_profit_price: float | None = None
|
|
stop_loss_price: float | None = None
|
|
max_hold_time: int | None = None # seconds
|
|
|
|
# Status
|
|
is_active: bool = True
|
|
exit_reason: ExitReason | None = None
|
|
exit_price: float | None = None
|
|
exit_time: datetime | None = None
|
|
|
|
@classmethod
|
|
def create_from_buy_result(
|
|
cls,
|
|
mint: Pubkey,
|
|
symbol: str,
|
|
entry_price: float,
|
|
quantity: float,
|
|
take_profit_percentage: float | None = None,
|
|
stop_loss_percentage: float | None = None,
|
|
max_hold_time: int | None = None,
|
|
) -> "Position":
|
|
"""Create a position from a successful buy transaction.
|
|
|
|
Args:
|
|
mint: Token mint address
|
|
symbol: Token symbol
|
|
entry_price: Price at which position was entered
|
|
quantity: Quantity of tokens purchased
|
|
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
|
|
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
|
|
max_hold_time: Maximum hold time in seconds
|
|
|
|
Returns:
|
|
Position instance
|
|
"""
|
|
take_profit_price = None
|
|
if take_profit_percentage is not None:
|
|
take_profit_price = entry_price * (1 + take_profit_percentage)
|
|
|
|
stop_loss_price = None
|
|
if stop_loss_percentage is not None:
|
|
stop_loss_price = entry_price * (1 - stop_loss_percentage)
|
|
|
|
return cls(
|
|
mint=mint,
|
|
symbol=symbol,
|
|
entry_price=entry_price,
|
|
quantity=quantity,
|
|
entry_time=datetime.utcnow(),
|
|
take_profit_price=take_profit_price,
|
|
stop_loss_price=stop_loss_price,
|
|
max_hold_time=max_hold_time,
|
|
)
|
|
|
|
def should_exit(self, current_price: float) -> tuple[bool, ExitReason | None]:
|
|
"""Check if position should be exited based on current conditions.
|
|
|
|
Args:
|
|
current_price: Current token price
|
|
|
|
Returns:
|
|
Tuple of (should_exit, exit_reason)
|
|
"""
|
|
if not self.is_active:
|
|
return False, None
|
|
|
|
# Check take profit
|
|
if self.take_profit_price and current_price >= self.take_profit_price:
|
|
return True, ExitReason.TAKE_PROFIT
|
|
|
|
# Check stop loss
|
|
if self.stop_loss_price and current_price <= self.stop_loss_price:
|
|
return True, ExitReason.STOP_LOSS
|
|
|
|
# Check max hold time
|
|
if self.max_hold_time:
|
|
elapsed_time = (datetime.utcnow() - self.entry_time).total_seconds()
|
|
if elapsed_time >= self.max_hold_time:
|
|
return True, ExitReason.MAX_HOLD_TIME
|
|
|
|
return False, None
|
|
|
|
def close_position(self, exit_price: float, exit_reason: ExitReason) -> None:
|
|
"""Close the position with exit details.
|
|
|
|
Args:
|
|
exit_price: Price at which position was exited
|
|
exit_reason: Reason for exit
|
|
"""
|
|
self.is_active = False
|
|
self.exit_price = exit_price
|
|
self.exit_reason = exit_reason
|
|
self.exit_time = datetime.utcnow()
|
|
|
|
def get_pnl(self, current_price: float | None = None) -> dict:
|
|
"""Calculate profit/loss for the position.
|
|
|
|
Args:
|
|
current_price: Current price (uses exit_price if position is closed)
|
|
|
|
Returns:
|
|
Dictionary with PnL information
|
|
"""
|
|
if self.is_active and current_price is None:
|
|
raise ValueError("current_price required for active position")
|
|
|
|
price_to_use = self.exit_price if not self.is_active else current_price
|
|
if price_to_use is None:
|
|
raise ValueError("No price available for PnL calculation")
|
|
|
|
price_change = price_to_use - self.entry_price
|
|
price_change_pct = (price_change / self.entry_price) * 100
|
|
unrealized_pnl = price_change * self.quantity
|
|
|
|
return {
|
|
"entry_price": self.entry_price,
|
|
"current_price": price_to_use,
|
|
"price_change": price_change,
|
|
"price_change_pct": price_change_pct,
|
|
"unrealized_pnl_sol": unrealized_pnl,
|
|
"quantity": self.quantity,
|
|
}
|
|
|
|
def __str__(self) -> str:
|
|
"""String representation of position."""
|
|
if self.is_active:
|
|
status = "ACTIVE"
|
|
elif self.exit_reason:
|
|
status = f"CLOSED ({self.exit_reason.value})"
|
|
else:
|
|
status = "CLOSED (UNKNOWN)"
|
|
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} SOL - {status})"
|