Skip to content

Commit a0b4637

Browse files
committed
feat(strategy): add is_market_order flag and improve order handling logic
- Added is_market_order attribute to Order class to identify market orders - Improved position reversal logic to prevent exit orders from reversing direction - Updated order processing to apply slippage for market orders - Refactored order filling to process entry and exit orders separately - Enhanced risk management checks for direction and pyramiding limits - Updated trade export to use entry/exit comments if available
1 parent 1db501f commit a0b4637

File tree

2 files changed

+56
-29
lines changed

2 files changed

+56
-29
lines changed

src/pynecore/core/script_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
337337
trade_num,
338338
trade.entry_bar_index,
339339
"Entry long" if trade.size > 0 else "Entry short",
340-
trade.entry_id,
340+
trade.entry_comment if trade.entry_comment else trade.entry_id,
341341
string.format_time(trade.entry_time), # type: ignore
342342
trade.entry_price,
343343
abs(trade.size),
@@ -354,7 +354,7 @@ def run_iter(self, on_progress: Callable[[datetime], None] | None = None) \
354354
trade_num,
355355
trade.exit_bar_index,
356356
"Exit long" if trade.size > 0 else "Exit short",
357-
trade.exit_id,
357+
trade.exit_comment if trade.exit_comment else trade.exit_id,
358358
string.format_time(trade.exit_time), # type: ignore
359359
trade.exit_price,
360360
abs(trade.size),

src/pynecore/lib/strategy/__init__.py

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class Order:
8585
"trail_price", "trail_offset",
8686
"trail_triggered",
8787
"profit_ticks", "loss_ticks", "trail_points_ticks", # Store tick values for later calculation
88+
"is_market_order", # Flag to check if this is a market order
8889
)
8990

9091
def __init__(
@@ -129,6 +130,9 @@ def __init__(
129130
self.loss_ticks = loss_ticks
130131
self.trail_points_ticks = trail_points_ticks
131132

133+
# Check if this is a market order (no limit, stop, or trail price)
134+
self.is_market_order = self.limit is None and self.stop is None and self.trail_price is None
135+
132136
def __repr__(self):
133137
return f"Order(order_id={self.order_id}; exit_id={self.exit_id}; size={self.size}; type: {self.order_type}; " \
134138
f"limit={self.limit}; stop={self.stop}; " \
@@ -468,9 +472,8 @@ def _fill_order(self, order: Order, price: float, h: float, l: float):
468472

469473
self.open_trades = open_trades
470474
if delete:
471-
if order.exit_id is not None:
472-
# Remove from exit_orders dict
473-
self.exit_orders.pop(order.exit_id, None)
475+
# Remove from exit_orders dict
476+
self.exit_orders.pop(order.exit_id, None)
474477

475478
if commission_type == _commission.cash_per_order:
476479
# Realize commission
@@ -565,16 +568,35 @@ def fill_order(self, order: Order, price: float, h: float, l: float) -> bool:
565568
new_size = 0.0
566569
new_sign = 0.0 if new_size == 0.0 else 1.0 if new_size > 0.0 else -1.0
567570
if self.size != 0.0 and new_sign != self.sign and new_size != 0.0:
571+
# Exit orders should never reverse position direction
572+
# Only entry orders can open new positions or reverse direction
573+
if order.order_type == _order_type_close:
574+
# Limit the exit order size to just close the position
575+
order.size = -self.size
576+
self._fill_order(order, price, h, l)
577+
return False
578+
568579
# Create a copy for closing existing position
569580
order1 = copy(order)
570581
order1.order_type = _order_type_close
571582
order1.size = -self.size
572583
# Set order_id to None so it will close any open trades
573584
order1.order_id = None
574-
# Don't need to remove this order, because it is not in the orders list
575-
order1.exit_id = None
585+
# The exit_id will be the order_id of the original order
586+
order1.exit_id = order.order_id
576587
# Fill the closing order first
577588
self._fill_order(order1, price, h, l)
589+
590+
# Check if new direction is allowed by risk management
591+
# According to Pine Script docs: "long exit trades will be made instead of reverse trades"
592+
new_direction_sign = 1.0 if new_size > 0.0 else -1.0
593+
if self.risk_allowed_direction is not None:
594+
if (new_direction_sign > 0 and self.risk_allowed_direction != long) or \
595+
(new_direction_sign < 0 and self.risk_allowed_direction != short):
596+
# Direction not allowed - convert entry to exit only
597+
# Don't open new position in restricted direction
598+
return False
599+
578600
# Modify the original order to open a position in the new direction
579601
order.size = new_size
580602
# Store in the appropriate dict based on order type
@@ -666,15 +688,17 @@ def process_orders(self):
666688
self.l = round_to_mintick(lib.low)
667689
self.c = round_to_mintick(lib.close)
668690

691+
# Get script reference for slippage
692+
script = lib._script
693+
669694
# If the order is open → high → low → close or open → low → high → close
670695
ohlc = abs(self.h - self.o) < abs(self.l - self.o)
671696

672697
self.drawdown_summ = self.runup_summ = 0.0
673698
self.new_closed_trades.clear()
674699

675-
# Process all orders: entry orders first, then exit orders (guaranteed order with chain)
676-
from itertools import chain
677-
for order in list(chain(self.entry_orders.values(), self.exit_orders.values())):
700+
# Process all orders: entry orders first, then exit orders (guaranteed order)
701+
for order in list(self.entry_orders.values()) + list(self.exit_orders.values()):
678702
# For exit orders, calculate limit/stop from entry price if ticks are specified
679703
if order.order_type == _order_type_close and order.order_id:
680704
# Try to find the trade with matching entry_id
@@ -705,13 +729,22 @@ def process_orders(self):
705729
order.trail_price = _price_round(order.trail_price, direction)
706730

707731
# Market orders
708-
if order.limit is None and order.stop is None and order.trail_price is None:
732+
if order.is_market_order:
733+
# Apply slippage to market orders
734+
fill_price = self.o
735+
if script.slippage > 0:
736+
# Slippage is in ticks, always adverse to trade direction
737+
# For long orders (buying), slippage increases the price
738+
# For short orders (selling), slippage decreases the price
739+
slippage_amount = syminfo.mintick * script.slippage * order.sign
740+
fill_price = self.o + slippage_amount
741+
709742
# open → high → low → close
710743
if ohlc:
711-
self.fill_order(order, self.o, self.o, self.l)
744+
self.fill_order(order, fill_price, self.o, self.l)
712745
# open → low → high → close
713746
else:
714-
self.fill_order(order, self.o, self.l, self.o)
747+
self.fill_order(order, fill_price, self.l, self.o)
715748

716749
# Limit/Stop orders
717750
else:
@@ -725,7 +758,7 @@ def process_orders(self):
725758
self._check_low(order)
726759

727760
# 2nd round of process open orders
728-
for order in list(chain(self.entry_orders.values(), self.exit_orders.values())):
761+
for order in list(self.entry_orders.values()) + list(self.exit_orders.values()):
729762
# For exit orders, calculate limit/stop from entry price if not already done
730763
if order.order_type == _order_type_close and order.order_id:
731764
# Only recalculate if not already set in first round
@@ -1095,28 +1128,22 @@ def entry(id: str, direction: direction.Direction, qty: int | float | NA[float]
10951128
size = qty * direction_sign
10961129
sign = 0.0 if size == 0.0 else 1.0 if size > 0.0 else -1.0
10971130

1098-
# Change direction
1099-
is_direction_change = False
1131+
# Check pyramiding limit (only for same direction trades)
11001132
if position.size:
1101-
if position.sign != sign:
1102-
is_direction_change = True
1103-
else:
1104-
# Handle pyramiding
1133+
if position.sign == sign:
1134+
# Same direction - check pyramiding
11051135
if script.pyramiding <= len(script.position.open_trades):
11061136
return
11071137

1108-
# Risk management: Check allowed direction (only for new positions, not direction changes)
1138+
# Risk management: Check allowed direction for new positions
1139+
# Direction changes are handled in fill_order() which will convert entry to exit if needed
11091140
if position.risk_allowed_direction is not None:
11101141
if (sign > 0 and position.risk_allowed_direction != long) or \
11111142
(sign < 0 and position.risk_allowed_direction != short):
1112-
if not is_direction_change:
1113-
return
1114-
else:
1115-
is_direction_change = False
1116-
1117-
# We need to adjust the size if we are changing direction
1118-
if is_direction_change:
1119-
size -= position.size
1143+
# Check if this would be a new position (not a direction change)
1144+
if not position.size or position.sign == sign:
1145+
return # Block new positions in restricted direction
1146+
# For direction changes, let fill_order() handle the conversion to exit
11201147

11211148
# Risk management: Check max position size
11221149
if position.risk_max_position_size is not None:

0 commit comments

Comments
 (0)