Skip to content

Commit 105969d

Browse files
committed
feat(context): add order convenience functions (#440)
Add five high-level ordering helpers on Context that simplify portfolio management and rebalancing by removing manual amount calculations: - order_value(symbol, value, side, price): place a LIMIT order for a fixed currency amount (amount = value / price). - order_percent(symbol, percent, side, price): place a LIMIT order for a percentage of the portfolio's net size. - order_target(symbol, target_amount, price): adjust the position to exactly target_amount units; submits BUY/SELL for the difference, or no order when already on target. - order_target_value(symbol, target_value, price): adjust the position so its market value at price equals target_value. - order_target_percent(symbol, target_percent, price): adjust the position so its market value equals target_percent of the portfolio's net size (rebalancing). All helpers produce LIMIT orders by default and validate inputs (positive value/percent/price, non-negative targets). Implemented as thin wrappers around create_limit_order; target_* helpers compute the current position size and place a BUY or SELL for the difference. Tests: 14 new tests in test_order_convenience_functions.py covering buy paths, target-down rebalancing, no-op when already on target, and validation errors. Closes #440
1 parent de175ac commit 105969d

2 files changed

Lines changed: 512 additions & 0 deletions

File tree

investing_algorithm_framework/app/context.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,245 @@ def create_limit_buy_order(
673673
market=market,
674674
)
675675

676+
def order_value(
677+
self,
678+
target_symbol,
679+
value,
680+
order_side,
681+
price,
682+
market=None,
683+
precision=None,
684+
metadata=None,
685+
) -> Order:
686+
"""
687+
Place a LIMIT order for a fixed currency amount.
688+
689+
The order amount is computed as ``value / price``.
690+
691+
Args:
692+
target_symbol: The symbol of the asset to trade.
693+
value: Currency amount to spend (BUY) or to sell (SELL).
694+
order_side: ``OrderSide.BUY`` or ``OrderSide.SELL``.
695+
price: Limit price.
696+
market: Optional market identifier.
697+
precision: Optional decimal precision to round the
698+
computed amount down.
699+
metadata: Optional order metadata.
700+
701+
Returns:
702+
Order: The created order.
703+
"""
704+
if value is None or value <= 0:
705+
raise OperationalException(
706+
"`value` must be a positive number."
707+
)
708+
if price is None or price <= 0:
709+
raise OperationalException(
710+
"`price` must be a positive number."
711+
)
712+
amount = value / price
713+
if precision is not None:
714+
amount = RoundingService.round_down(amount, precision)
715+
return self.create_limit_order(
716+
target_symbol=target_symbol,
717+
price=price,
718+
order_side=order_side,
719+
amount=amount,
720+
market=market,
721+
metadata=metadata,
722+
)
723+
724+
def order_percent(
725+
self,
726+
target_symbol,
727+
percent,
728+
order_side,
729+
price,
730+
market=None,
731+
precision=None,
732+
metadata=None,
733+
) -> Order:
734+
"""
735+
Place a LIMIT order for ``percent`` of the portfolio's net size.
736+
737+
Args:
738+
target_symbol: The symbol of the asset to trade.
739+
percent: Percentage (0-100) of portfolio net size to allocate.
740+
order_side: ``OrderSide.BUY`` or ``OrderSide.SELL``.
741+
price: Limit price.
742+
market: Optional market identifier.
743+
precision: Optional decimal precision to round the
744+
computed amount down.
745+
metadata: Optional order metadata.
746+
747+
Returns:
748+
Order: The created order.
749+
"""
750+
if percent is None or percent <= 0:
751+
raise OperationalException(
752+
"`percent` must be a positive number."
753+
)
754+
portfolio = self.portfolio_service.find({"market": market})
755+
net_size = portfolio.get_net_size()
756+
value = net_size * (percent / 100.0)
757+
return self.order_value(
758+
target_symbol=target_symbol,
759+
value=value,
760+
order_side=order_side,
761+
price=price,
762+
market=market,
763+
precision=precision,
764+
metadata=metadata,
765+
)
766+
767+
def order_target(
768+
self,
769+
target_symbol,
770+
target_amount,
771+
price,
772+
market=None,
773+
precision=None,
774+
metadata=None,
775+
) -> Order:
776+
"""
777+
Adjust the position in ``target_symbol`` to hold exactly
778+
``target_amount`` units.
779+
780+
The difference between the current position size and
781+
``target_amount`` is submitted as a BUY (positive diff) or
782+
SELL (negative diff) LIMIT order. If the position already
783+
matches the target, no order is placed.
784+
785+
Args:
786+
target_symbol: The symbol of the asset to adjust.
787+
target_amount: Desired position size in units.
788+
price: Limit price.
789+
market: Optional market identifier.
790+
precision: Optional decimal precision to round the diff down.
791+
metadata: Optional order metadata.
792+
793+
Returns:
794+
Order: The created order, or ``None`` if no adjustment
795+
was required.
796+
"""
797+
if target_amount is None or target_amount < 0:
798+
raise OperationalException(
799+
"`target_amount` must be a non-negative number."
800+
)
801+
if price is None or price <= 0:
802+
raise OperationalException(
803+
"`price` must be a positive number."
804+
)
805+
portfolio = self.portfolio_service.find({"market": market})
806+
try:
807+
position = self.position_service.find(
808+
{"symbol": target_symbol, "portfolio": portfolio.id}
809+
)
810+
current_amount = position.get_amount() \
811+
if position is not None else 0
812+
except OperationalException:
813+
current_amount = 0
814+
diff = target_amount - current_amount
815+
if precision is not None:
816+
sign = 1 if diff >= 0 else -1
817+
diff = sign * RoundingService.round_down(abs(diff), precision)
818+
if diff == 0:
819+
return None
820+
order_side = OrderSide.BUY if diff > 0 else OrderSide.SELL
821+
return self.create_limit_order(
822+
target_symbol=target_symbol,
823+
price=price,
824+
order_side=order_side,
825+
amount=abs(diff),
826+
market=market,
827+
metadata=metadata,
828+
)
829+
830+
def order_target_value(
831+
self,
832+
target_symbol,
833+
target_value,
834+
price,
835+
market=None,
836+
precision=None,
837+
metadata=None,
838+
) -> Order:
839+
"""
840+
Adjust the position in ``target_symbol`` so its market value at
841+
``price`` equals ``target_value``.
842+
843+
Args:
844+
target_symbol: The symbol of the asset to adjust.
845+
target_value: Desired position market value in trading currency.
846+
price: Limit price.
847+
market: Optional market identifier.
848+
precision: Optional decimal precision to round the diff down.
849+
metadata: Optional order metadata.
850+
851+
Returns:
852+
Order: The created order, or ``None`` if no adjustment
853+
was required.
854+
"""
855+
if target_value is None or target_value < 0:
856+
raise OperationalException(
857+
"`target_value` must be a non-negative number."
858+
)
859+
if price is None or price <= 0:
860+
raise OperationalException(
861+
"`price` must be a positive number."
862+
)
863+
target_amount = target_value / price
864+
return self.order_target(
865+
target_symbol=target_symbol,
866+
target_amount=target_amount,
867+
price=price,
868+
market=market,
869+
precision=precision,
870+
metadata=metadata,
871+
)
872+
873+
def order_target_percent(
874+
self,
875+
target_symbol,
876+
target_percent,
877+
price,
878+
market=None,
879+
precision=None,
880+
metadata=None,
881+
) -> Order:
882+
"""
883+
Adjust the position in ``target_symbol`` so its market value
884+
equals ``target_percent`` of the portfolio's net size.
885+
886+
Args:
887+
target_symbol: The symbol of the asset to adjust.
888+
target_percent: Desired position size as a percentage (0-100)
889+
of portfolio net size.
890+
price: Limit price.
891+
market: Optional market identifier.
892+
precision: Optional decimal precision to round the diff down.
893+
metadata: Optional order metadata.
894+
895+
Returns:
896+
Order: The created order, or ``None`` if no adjustment
897+
was required.
898+
"""
899+
if target_percent is None or target_percent < 0:
900+
raise OperationalException(
901+
"`target_percent` must be a non-negative number."
902+
)
903+
portfolio = self.portfolio_service.find({"market": market})
904+
net_size = portfolio.get_net_size()
905+
target_value = net_size * (target_percent / 100.0)
906+
return self.order_target_value(
907+
target_symbol=target_symbol,
908+
target_value=target_value,
909+
price=price,
910+
market=market,
911+
precision=precision,
912+
metadata=metadata,
913+
)
914+
676915
def get_portfolio(self, market=None) -> Portfolio:
677916
"""
678917
Function to get the portfolio of the algorithm. This function

0 commit comments

Comments
 (0)