Stage 07.4.3.10 — Risk and position control
This commit is contained in:
@@ -376,6 +376,8 @@ class AutoTradeRunner:
|
|||||||
exit_price = cls._format_price(payload.get("exit_price"))
|
exit_price = cls._format_price(payload.get("exit_price"))
|
||||||
size = cls._format_size(payload.get("size"))
|
size = cls._format_size(payload.get("size"))
|
||||||
pnl = cls._format_pnl(payload.get("pnl"))
|
pnl = cls._format_pnl(payload.get("pnl"))
|
||||||
|
risk_reason = payload.get("risk_reason")
|
||||||
|
risk_line = f"\nRisk: {risk_reason}" if risk_reason else ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f"<b>✅ Paper position closed</b>\n\n"
|
f"<b>✅ Paper position closed</b>\n\n"
|
||||||
@@ -384,6 +386,7 @@ class AutoTradeRunner:
|
|||||||
f"Exit: $ {exit_price}\n"
|
f"Exit: $ {exit_price}\n"
|
||||||
f"Size: {size}\n\n"
|
f"Size: {size}\n\n"
|
||||||
f"PnL: {pnl}"
|
f"PnL: {pnl}"
|
||||||
|
f"{risk_line}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if event_type == "paper_position_flipped":
|
if event_type == "paper_position_flipped":
|
||||||
|
|||||||
@@ -65,4 +65,18 @@ class AutoTradeState:
|
|||||||
max_drawdown_usd: float | None = None
|
max_drawdown_usd: float | None = None
|
||||||
|
|
||||||
# плечо
|
# плечо
|
||||||
leverage: float | None = 2.0
|
leverage: float | None = 2.0
|
||||||
|
|
||||||
|
# stop loss по движению цены в %
|
||||||
|
#stop_loss_percent: float | None = 2.0
|
||||||
|
|
||||||
|
# take profit по движению цены в %
|
||||||
|
#take_profit_percent: float | None = 3.0
|
||||||
|
|
||||||
|
# максимальный допустимый paper-убыток в USD
|
||||||
|
#max_loss_usd: float | None = None
|
||||||
|
|
||||||
|
# для демонстрации рисков: стоп-лосс и тейк-профит по риску в % от капитала
|
||||||
|
stop_loss_percent: float | None = None
|
||||||
|
take_profit_percent: float | None = None
|
||||||
|
max_loss_usd: float | None = 0.01
|
||||||
@@ -26,6 +26,10 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
self._update_unrealized_pnl(state)
|
self._update_unrealized_pnl(state)
|
||||||
|
|
||||||
|
risk_decision = self._risk_close_decision(state)
|
||||||
|
if risk_decision is not None:
|
||||||
|
return risk_decision
|
||||||
|
|
||||||
if state.decision_status != "READY" or not state.is_signal_ready:
|
if state.decision_status != "READY" or not state.is_signal_ready:
|
||||||
return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.")
|
return ExecutionDecision("NONE", False, "Сигнал ещё не готов к execution.")
|
||||||
|
|
||||||
@@ -183,20 +187,31 @@ class ExecutionEngine:
|
|||||||
f"Paper FLIP выполнен: {old_side} → {new_side}.",
|
f"Paper FLIP выполнен: {old_side} → {new_side}.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _close_position(self, state: AutoTradeState) -> ExecutionDecision:
|
def _close_position(
|
||||||
|
self,
|
||||||
|
state: AutoTradeState,
|
||||||
|
*,
|
||||||
|
forced_reason: str | None = None,
|
||||||
|
forced_exit_price: float | None = None,
|
||||||
|
forced_pnl: float | None = None,
|
||||||
|
) -> ExecutionDecision:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
if position.side == "NONE":
|
if position.side == "NONE":
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
return ExecutionDecision("NONE", False, "Нет открытой позиции для закрытия.")
|
return ExecutionDecision("NONE", False, "Нет открытой позиции для закрытия.")
|
||||||
|
|
||||||
try:
|
if forced_exit_price is not None:
|
||||||
ticker = ExchangeService().get_price(state.symbol)
|
exit_price = forced_exit_price
|
||||||
exit_price = ticker.price
|
else:
|
||||||
except Exception as exc:
|
try:
|
||||||
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
|
ticker = ExchangeService().get_price(state.symbol)
|
||||||
|
exit_price = ticker.price
|
||||||
|
except Exception as exc:
|
||||||
|
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
|
||||||
|
|
||||||
|
pnl = forced_pnl if forced_pnl is not None else self._calculate_pnl(exit_price)
|
||||||
|
|
||||||
pnl = self._calculate_pnl(exit_price)
|
|
||||||
now = self._now_time()
|
now = self._now_time()
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
@@ -213,13 +228,19 @@ class ExecutionEngine:
|
|||||||
"confidence": state.last_signal_confidence,
|
"confidence": state.last_signal_confidence,
|
||||||
"repeat_count": state.last_signal_repeat_count,
|
"repeat_count": state.last_signal_repeat_count,
|
||||||
"reason": state.last_signal_reason,
|
"reason": state.last_signal_reason,
|
||||||
|
"risk_reason": forced_reason,
|
||||||
|
"is_forced": forced_reason is not None,
|
||||||
"opened_at": position.opened_at,
|
"opened_at": position.opened_at,
|
||||||
"closed_at": now,
|
"closed_at": now,
|
||||||
}
|
}
|
||||||
|
|
||||||
JournalService().log_ui_info(
|
JournalService().log_ui_info(
|
||||||
event_type="paper_position_closed",
|
event_type="paper_position_closed",
|
||||||
message=f"Paper EXIT закрыта: {position.side} {state.symbol}",
|
message=(
|
||||||
|
f"Paper EXIT закрыта по риску {forced_reason}: {position.side} {state.symbol}"
|
||||||
|
if forced_reason is not None
|
||||||
|
else f"Paper EXIT закрыта: {position.side} {state.symbol}"
|
||||||
|
),
|
||||||
screen="auto",
|
screen="auto",
|
||||||
action="paper_execution",
|
action="paper_execution",
|
||||||
payload=payload,
|
payload=payload,
|
||||||
@@ -230,8 +251,101 @@ class ExecutionEngine:
|
|||||||
type(self)._position = PositionState()
|
type(self)._position = PositionState()
|
||||||
self._sync_state_from_position(state)
|
self._sync_state_from_position(state)
|
||||||
|
|
||||||
|
if forced_reason is not None:
|
||||||
|
return ExecutionDecision(
|
||||||
|
f"FORCE_CLOSE_{forced_reason}",
|
||||||
|
True,
|
||||||
|
f"Paper EXIT выполнена по риску: {forced_reason}.",
|
||||||
|
)
|
||||||
|
|
||||||
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
|
return ExecutionDecision("CLOSE", True, "Paper EXIT выполнена.")
|
||||||
|
|
||||||
|
def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None:
|
||||||
|
position = type(self)._position
|
||||||
|
|
||||||
|
if position.side == "NONE":
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
ticker = ExchangeService().get_price(position.symbol or state.symbol)
|
||||||
|
current_price = ticker.price
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
price_move_percent = self._calculate_price_move_percent(current_price)
|
||||||
|
unrealized_pnl = self._calculate_pnl(current_price)
|
||||||
|
|
||||||
|
if self._is_stop_loss_hit(state, price_move_percent):
|
||||||
|
return self._close_position(
|
||||||
|
state,
|
||||||
|
forced_reason="STOP_LOSS",
|
||||||
|
forced_exit_price=current_price,
|
||||||
|
forced_pnl=unrealized_pnl,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._is_take_profit_hit(state, price_move_percent):
|
||||||
|
return self._close_position(
|
||||||
|
state,
|
||||||
|
forced_reason="TAKE_PROFIT",
|
||||||
|
forced_exit_price=current_price,
|
||||||
|
forced_pnl=unrealized_pnl,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self._is_max_loss_hit(state, unrealized_pnl):
|
||||||
|
return self._close_position(
|
||||||
|
state,
|
||||||
|
forced_reason="MAX_LOSS",
|
||||||
|
forced_exit_price=current_price,
|
||||||
|
forced_pnl=unrealized_pnl,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _is_stop_loss_hit(
|
||||||
|
self,
|
||||||
|
state: AutoTradeState,
|
||||||
|
price_move_percent: float,
|
||||||
|
) -> bool:
|
||||||
|
if state.stop_loss_percent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return price_move_percent <= -abs(state.stop_loss_percent)
|
||||||
|
|
||||||
|
def _is_take_profit_hit(
|
||||||
|
self,
|
||||||
|
state: AutoTradeState,
|
||||||
|
price_move_percent: float,
|
||||||
|
) -> bool:
|
||||||
|
if state.take_profit_percent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return price_move_percent >= abs(state.take_profit_percent)
|
||||||
|
|
||||||
|
def _is_max_loss_hit(
|
||||||
|
self,
|
||||||
|
state: AutoTradeState,
|
||||||
|
unrealized_pnl: float,
|
||||||
|
) -> bool:
|
||||||
|
if state.max_loss_usd is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return unrealized_pnl <= -abs(state.max_loss_usd)
|
||||||
|
|
||||||
|
def _calculate_price_move_percent(self, current_price: float) -> float:
|
||||||
|
position = type(self)._position
|
||||||
|
|
||||||
|
entry = position.entry_price or 0.0
|
||||||
|
if entry <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if position.side == "LONG":
|
||||||
|
return round(((current_price - entry) / entry) * 100, 4)
|
||||||
|
|
||||||
|
if position.side == "SHORT":
|
||||||
|
return round(((entry - current_price) / entry) * 100, 4)
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
def _should_flip_position(self, state: AutoTradeState) -> bool:
|
def _should_flip_position(self, state: AutoTradeState) -> bool:
|
||||||
position = type(self)._position
|
position = type(self)._position
|
||||||
|
|
||||||
|
|||||||
300
docs/stages/stage-07_4_3_10-risk_position_control.md
Normal file
300
docs/stages/stage-07_4_3_10-risk_position_control.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# Stage 07.4.3.10 — Risk & Position Control
|
||||||
|
|
||||||
|
## 📌 Обзор
|
||||||
|
|
||||||
|
На данном этапе реализована система контроля риска для paper-trading:
|
||||||
|
|
||||||
|
- Stop Loss (по движению цены)
|
||||||
|
- Take Profit (по движению цены)
|
||||||
|
- Max Loss (по абсолютному убытку)
|
||||||
|
- Принудительное закрытие позиции (forced execution)
|
||||||
|
- Интеграция с EventBus, Journal и Telegram alerts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Поддерживаемые risk-механики
|
||||||
|
|
||||||
|
### 1. Stop Loss (%)
|
||||||
|
|
||||||
|
Процентное ограничение убытка относительно цены входа.
|
||||||
|
Закрывает позицию при неблагоприятном движении цены:
|
||||||
|
LONG: цена ↓
|
||||||
|
SHORT: цена ↑
|
||||||
|
|
||||||
|
### 2. Take Profit (%)
|
||||||
|
Процентное ограничение прибыли.
|
||||||
|
Фиксирует прибыль при благоприятном движении:
|
||||||
|
LONG: цена ↑
|
||||||
|
SHORT: цена ↓
|
||||||
|
|
||||||
|
### 3. Max Loss (USD)
|
||||||
|
Абсолютное ограничение убытка в USD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 Логика работы
|
||||||
|
|
||||||
|
Risk-контроль выполняется до execution сигнала:
|
||||||
|
1. Обновление PnL
|
||||||
|
2. Проверка risk-условий
|
||||||
|
3. При необходимости → forced CLOSE
|
||||||
|
4. Только потом → обычный execution
|
||||||
|
|
||||||
|
### 🔁 Типы execution событий
|
||||||
|
|
||||||
|
ENTRY
|
||||||
|
paper_position_opened
|
||||||
|
|
||||||
|
EXIT (обычный)
|
||||||
|
paper_position_closed
|
||||||
|
|
||||||
|
EXIT (forced)
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"execution_type": "EXIT",
|
||||||
|
"risk_reason": "STOP_LOSS | TAKE_PROFIT | MAX_LOSS",
|
||||||
|
"is_forced": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
FLIP
|
||||||
|
paper_position_flipped
|
||||||
|
|
||||||
|
### 📡 EventBus события
|
||||||
|
|
||||||
|
* paper_position_opened
|
||||||
|
* paper_position_closed
|
||||||
|
* paper_position_flipped
|
||||||
|
|
||||||
|
## 🧾 Journal
|
||||||
|
|
||||||
|
Пример записи:
|
||||||
|
[DEMO] Paper EXIT закрыта по риску TAKE_PROFIT: LONG BTC/USD
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"risk_reason": "TAKE_PROFIT",
|
||||||
|
"is_forced": true,
|
||||||
|
"pnl": 0.75
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📲 Telegram Alerts
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
```
|
||||||
|
✅ Paper position closed
|
||||||
|
|
||||||
|
LONG · BTC / USD · x2
|
||||||
|
Entry: $ 81 022.85
|
||||||
|
Exit: $ 81 041.70
|
||||||
|
Size: 0.04
|
||||||
|
|
||||||
|
PnL: 🟢 +0.75 USD
|
||||||
|
Risk: TAKE_PROFIT
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Расчёты
|
||||||
|
|
||||||
|
PnL:
|
||||||
|
LONG: (current - entry) * size
|
||||||
|
SHORT: (entry - current) * size
|
||||||
|
|
||||||
|
Price Move %:
|
||||||
|
LONG: (current - entry) / entry * 100
|
||||||
|
SHORT: (entry - current) / entry * 100
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Тестирование
|
||||||
|
|
||||||
|
### STOP LOSS
|
||||||
|
stop_loss_percent = 0.01
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
Risk: STOP_LOSS
|
||||||
|
PnL: 🔴 отрицательный
|
||||||
|
|
||||||
|
### TAKE PROFIT
|
||||||
|
take_profit_percent = 0.01
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
Risk: TAKE_PROFIT
|
||||||
|
PnL: 🟢 положительный
|
||||||
|
|
||||||
|
### MAX LOSS
|
||||||
|
max_loss_usd = 0.01
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
Risk: MAX_LOSS
|
||||||
|
|
||||||
|
### SHORT сценарий
|
||||||
|
/debug_signal SELL 0.95 3
|
||||||
|
|
||||||
|
Проверяется обратная логика TP/SL.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Важно
|
||||||
|
|
||||||
|
* Risk работает только в RUNNING
|
||||||
|
* Работает независимо от новых сигналов
|
||||||
|
* Может закрыть позицию без нового READY
|
||||||
|
* Не конфликтует с flip logic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 Архитектура
|
||||||
|
|
||||||
|
AutoTradeService
|
||||||
|
↓
|
||||||
|
ExecutionEngine.process()
|
||||||
|
↓
|
||||||
|
Risk Control (NEW)
|
||||||
|
↓
|
||||||
|
Execution (ENTRY / EXIT / FLIP)
|
||||||
|
↓
|
||||||
|
EventBus
|
||||||
|
↓
|
||||||
|
AutoTradeRunner
|
||||||
|
↓
|
||||||
|
Telegram + Journal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Результат этапа
|
||||||
|
|
||||||
|
* Добавлен полноценный риск-контроль
|
||||||
|
* Реализованы forced exits
|
||||||
|
* Интеграция с UI, Journal и Telegram
|
||||||
|
* Подготовка к real trading
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Математика расчётов
|
||||||
|
|
||||||
|
Обозначения:
|
||||||
|
|
||||||
|
- P_entry — цена входа
|
||||||
|
- P_current — текущая цена
|
||||||
|
- size — размер позиции
|
||||||
|
- side — направление (LONG / SHORT)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. PnL
|
||||||
|
|
||||||
|
#### LONG:
|
||||||
|
PnL = (P_current - P_entry) * size
|
||||||
|
|
||||||
|
#### SHORT:
|
||||||
|
PnL = (P_entry - P_current) * size
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Stop Loss
|
||||||
|
|
||||||
|
#### LONG:
|
||||||
|
если P_current <= P_entry * (1 - SL%)
|
||||||
|
|
||||||
|
#### SHORT:
|
||||||
|
если P_current >= P_entry * (1 + SL%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Take Profit
|
||||||
|
|
||||||
|
#### LONG:
|
||||||
|
если P_current >= P_entry * (1 + TP%)
|
||||||
|
|
||||||
|
#### SHORT:
|
||||||
|
если P_current <= P_entry * (1 - TP%)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Max Loss
|
||||||
|
|
||||||
|
условие:
|
||||||
|
PnL <= -max_loss_usd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритет проверок
|
||||||
|
|
||||||
|
1. Max Loss (самый жёсткий)
|
||||||
|
2. Stop Loss
|
||||||
|
3. Take Profit
|
||||||
|
|
||||||
|
Первое выполненное условие закрывает позицию.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Flow
|
||||||
|
|
||||||
|
ExecutionEngine.process():
|
||||||
|
|
||||||
|
1. update_unrealized_pnl()
|
||||||
|
2. check_risk_conditions()
|
||||||
|
3. если риск сработал → close_position(risk_reason)
|
||||||
|
4. иначе:
|
||||||
|
- flip
|
||||||
|
- open
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Payload событий
|
||||||
|
|
||||||
|
### paper_position_closed
|
||||||
|
|
||||||
|
Добавлено:
|
||||||
|
|
||||||
|
- risk_reason: STOP_LOSS | TAKE_PROFIT | MAX_LOSS
|
||||||
|
- is_forced: true
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Telegram отображение
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
PnL: 🟢 +0.75 USD
|
||||||
|
Risk: TAKE_PROFIT
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Journal
|
||||||
|
|
||||||
|
Message:
|
||||||
|
Paper EXIT закрыта по риску TAKE_PROFIT
|
||||||
|
|
||||||
|
Payload:
|
||||||
|
{
|
||||||
|
"pnl": ...,
|
||||||
|
"risk_reason": "TAKE_PROFIT",
|
||||||
|
"is_forced": true
|
||||||
|
}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Важные нюансы
|
||||||
|
|
||||||
|
- Risk работает ТОЛЬКО в RUNNING
|
||||||
|
- Работает независимо от сигналов
|
||||||
|
- Срабатывает раньше flip
|
||||||
|
- Не зависит от decision_status
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
|
||||||
|
Добавлен критический слой защиты:
|
||||||
|
|
||||||
|
- ограничение убытков
|
||||||
|
- фиксация прибыли
|
||||||
|
- автономное поведение системы
|
||||||
|
|
||||||
|
Это база для:
|
||||||
|
- real trading
|
||||||
|
- advanced risk management
|
||||||
|
- portfolio-level контроля
|
||||||
Reference in New Issue
Block a user