fisk/core/forms.py

380 lines
15 KiB
Python
Raw Normal View History

2022-10-13 14:26:43 +00:00
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.core.exceptions import FieldDoesNotExist
2022-10-12 06:22:22 +00:00
from django.forms import ModelForm
from mixins.restrictions import RestrictedFormMixin
2022-10-13 14:26:43 +00:00
from .models import ( # AssetRestriction,
2022-12-18 17:21:52 +00:00
Account,
2023-02-10 07:20:19 +00:00
AssetGroup,
AssetRule,
2022-12-18 17:21:52 +00:00
Hook,
NotificationSettings,
2023-02-15 18:33:38 +00:00
OrderSettings,
2022-12-13 07:20:49 +00:00
RiskModel,
2022-12-18 17:21:52 +00:00
Signal,
Strategy,
Trade,
TradingTime,
User,
)
2022-10-13 14:26:43 +00:00
2022-11-28 20:42:07 +00:00
# flake8: noqa: E501
2022-10-13 14:26:43 +00:00
class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True)
class Meta:
model = User
fields = (
"username",
"email",
"first_name",
"last_name",
"password1",
"password2",
)
def save(self, commit=True):
user = super(NewUserForm, self).save(commit=False)
user.email = self.cleaned_data["email"]
if commit:
user.save()
return user
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = User
fields = "__all__"
2022-10-12 06:22:22 +00:00
class HookForm(RestrictedFormMixin, ModelForm):
2022-10-12 06:22:22 +00:00
class Meta:
model = Hook
fields = (
"name",
"hook",
)
2022-11-28 20:42:07 +00:00
help_texts = {
"name": "Name of the hook. Informational only.",
"hook": "The URL slug to use for the hook. Make it unique.",
}
2022-10-17 17:56:16 +00:00
2022-11-29 07:20:21 +00:00
class SignalForm(RestrictedFormMixin, ModelForm):
class Meta:
model = Signal
fields = (
"name",
"signal",
"hook",
2022-12-07 07:20:11 +00:00
"type",
2022-11-29 07:20:21 +00:00
"direction",
)
help_texts = {
"name": "Name of the signal. Informational only.",
"signal": "The name of the signal in Drakdoo. Copy it from there.",
"hook": "The hook this signal belongs to.",
2022-12-07 07:20:11 +00:00
"type": "Whether the signal is used for entering or exiting trades, or determining the trend.",
2022-11-29 07:20:21 +00:00
"direction": "The direction of the signal. This is used to determine if the signal is a buy or sell.",
}
class AccountForm(RestrictedFormMixin, ModelForm):
2022-10-17 17:56:16 +00:00
class Meta:
model = Account
2023-01-01 15:46:40 +00:00
fields = (
"name",
"exchange",
"api_key",
"api_secret",
2023-01-11 19:46:47 +00:00
"initial_balance",
"sandbox",
"enabled",
2023-01-01 15:46:40 +00:00
)
2022-11-28 20:42:07 +00:00
help_texts = {
"name": "Name of the account. Informational only.",
"exchange": "The exchange to use for this account.",
"api_key": "The API key or username for the account.",
"api_secret": "The API secret or password/token for the account.",
"sandbox": "Whether to use the sandbox/demo or not.",
2023-01-01 15:46:40 +00:00
"enabled": "Whether the account is enabled.",
2023-01-11 19:46:47 +00:00
"initial_balance": "The initial balance of the account.",
2022-11-28 20:42:07 +00:00
}
2022-10-17 17:56:16 +00:00
class StrategyForm(RestrictedFormMixin, ModelForm):
fieldargs = {
2022-12-07 07:20:11 +00:00
"entry_signals": {"type": "entry"},
"exit_signals": {"type": "exit"},
"trend_signals": {"type": "trend"},
}
2023-02-13 21:02:59 +00:00
# Filter for enabled accounts
def __init__(self, *args, **kwargs):
super(StrategyForm, self).__init__(*args, **kwargs)
self.fields["account"].queryset = Account.objects.filter(enabled=True)
class Meta:
model = Strategy
fields = (
"name",
"description",
"account",
"asset_group",
2023-02-15 18:15:36 +00:00
"risk_model",
2022-11-25 18:05:02 +00:00
"trading_times",
2023-02-15 18:33:38 +00:00
"order_settings",
2022-11-29 07:20:21 +00:00
"entry_signals",
"exit_signals",
2022-12-06 19:46:06 +00:00
"trend_signals",
"enabled",
)
2022-11-28 20:42:07 +00:00
help_texts = {
"name": "Name of the strategy. Informational only.",
"description": "Description of the strategy. Informational only.",
"account": "The account to use for this strategy.",
"asset_group": "Asset groups determine which pairs can be traded.",
2023-02-15 18:15:36 +00:00
"risk_model": "The risk model to use for this strategy. Highly recommended.",
2022-11-28 20:42:07 +00:00
"trading_times": "When the strategy will place new trades.",
2023-02-15 18:35:46 +00:00
"order_settings": "Order settings to use for this strategy.",
2022-12-06 19:46:06 +00:00
"entry_signals": "Callbacks received to these signals will trigger a trade.",
"exit_signals": "Callbacks received to these signals will close all trades for the symbol on the account.",
"trend_signals": "Callbacks received to these signals will limit the trading direction of the given symbol to the callback direction until further notice.",
2022-11-28 20:42:07 +00:00
"enabled": "Whether the strategy is enabled.",
}
2022-11-29 07:20:21 +00:00
entry_signals = forms.ModelMultipleChoiceField(
queryset=Signal.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["entry_signals"],
required=False,
)
exit_signals = forms.ModelMultipleChoiceField(
queryset=Signal.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["exit_signals"],
required=False,
)
2022-12-06 19:46:06 +00:00
trend_signals = forms.ModelMultipleChoiceField(
queryset=Signal.objects.all(),
widget=forms.CheckboxSelectMultiple,
help_text=Meta.help_texts["trend_signals"],
required=False,
)
2022-11-25 18:05:02 +00:00
trading_times = forms.ModelMultipleChoiceField(
queryset=TradingTime.objects.all(), widget=forms.CheckboxSelectMultiple
)
2022-12-01 19:32:08 +00:00
def clean(self):
super(StrategyForm, self).clean()
entry_signals = self.cleaned_data.get("entry_signals")
exit_signals = self.cleaned_data.get("exit_signals")
for entry in entry_signals.all():
if entry in exit_signals.all():
self._errors["entry_signals"] = self.error_class(
[
"You cannot bet against yourself. Do not use the same signal for entry and exit."
]
)
for exit in exit_signals.all():
if exit in entry_signals.all():
self._errors["exit_signals"] = self.error_class(
[
"You cannot bet against yourself. Do not use the same signal for entry and exit."
]
)
# Get all the directions for entry and exit signals
entries_set = set([x.direction for x in entry_signals.all()])
exits_set = set([x.direction for x in exit_signals.all()])
# Make sure both fields are filled before we check this
if entries_set and exits_set:
# Combine them into one set
check_set = set()
check_set.update(entries_set, exits_set)
# If the length is 1, they are all the same direction
if len(check_set) == 1:
self._errors["entry_signals"] = self.error_class(
[
"You cannot have entry and exit signals that are the same direction. At least one must be opposing."
]
)
self._errors["exit_signals"] = self.error_class(
[
"You cannot have entry and exit signals that are the same direction. At least one must be opposing."
]
)
class TradeForm(RestrictedFormMixin, ModelForm):
2022-10-17 17:56:16 +00:00
class Meta:
model = Trade
fields = (
"account",
"symbol",
"type",
"time_in_force",
2022-10-17 17:56:16 +00:00
"amount",
"price",
"stop_loss",
2022-11-15 07:20:17 +00:00
"trailing_stop_loss",
2022-10-17 17:56:16 +00:00
"take_profit",
2022-10-17 06:20:30 +00:00
"direction",
2022-10-17 17:56:16 +00:00
)
2022-11-28 20:42:07 +00:00
help_texts = {
"account": "The account to use for this trade.",
"symbol": "The symbol to trade.",
"type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.",
"time_in_force": "The time in force controls how the order is executed.",
"amount": "The amount to trade in the quote currency (the second part of the symbol if applicable, otherwise your account base currency).",
"price": "The price to trade at. Sets this price as the trigger price for limit orders. Sets a price bound (if supported) for market orders.",
"stop_loss": "The stop loss price. This will be set at the specified price.",
"trailing_stop_loss": "The trailing stop loss price. This will be set at the specified price and will follow the price as it moves in your favor.",
"take_profit": "The take profit price. This will be set at the specified price.",
"direction": "The direction of the trade. This is used to determine if the trade is a buy or sell.",
}
class TradingTimeForm(RestrictedFormMixin, ModelForm):
class Meta:
model = TradingTime
fields = (
"name",
"description",
"start_day",
"start_time",
"end_day",
"end_time",
)
2022-11-28 20:42:07 +00:00
help_texts = {
"name": "Name of the trading time. Informational only.",
"description": "Description of the trading time. Informational only.",
"start_day": "The day of the week to start trading.",
"start_time": "The time of day to start trading.",
"end_day": "The day of the week to stop trading.",
"end_time": "The time of day to stop trading.",
}
2022-12-18 17:21:52 +00:00
class NotificationSettingsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = NotificationSettings
fields = (
"ntfy_topic",
"ntfy_url",
)
help_texts = {
"ntfy_topic": "The topic to send notifications to.",
"ntfy_url": "Custom NTFY server. Leave blank to use the default server.",
}
2022-12-13 07:20:49 +00:00
class RiskModelForm(RestrictedFormMixin, ModelForm):
class Meta:
model = RiskModel
fields = (
"name",
"description",
"max_loss_percent",
"max_risk_percent",
"max_open_trades",
"max_open_trades_per_symbol",
2023-02-15 18:15:36 +00:00
"price_slippage_percent",
"callback_price_deviation_percent",
2022-12-13 07:20:49 +00:00
)
help_texts = {
"name": "Name of the risk model. Informational only.",
"description": "Description of the risk model. Informational only.",
"max_loss_percent": "The maximum percent of the account balance that can be lost before we cease trading.",
"max_risk_percent": "The maximum percent of the account balance that can be risked on all open trades.",
"max_open_trades": "The maximum number of open trades.",
"max_open_trades_per_symbol": "The maximum number of open trades per symbol.",
2023-02-15 18:15:36 +00:00
"price_slippage_percent": "The price slippage is the maximum percent the price can move against you before the trade is cancelled. Limit orders will be set at this percentage against your favour. Market orders will have a price bound set if this is supported.",
"callback_price_deviation_percent": "The callback price deviation is the maximum percent the price can change between receiving the callback and acting on it. This protects against rogue or delayed callbacks. Keep it low.",
2022-12-13 07:20:49 +00:00
}
2023-02-10 07:20:19 +00:00
class AssetGroupForm(RestrictedFormMixin, ModelForm):
class Meta:
model = AssetGroup
fields = (
"name",
"description",
"when_no_data",
"when_no_match",
"when_no_aggregation",
"when_not_in_bounds",
"when_bullish",
"when_bearish",
2023-02-10 07:20:19 +00:00
)
help_texts = {
"name": "Name of the asset group. Informational only.",
"description": "Description of the asset group. Informational only.",
"when_no_data": "The action to take when no webhooks have been received for an asset.",
"when_no_match": "The action to take when there were no matches last callback for an asset.",
"when_no_aggregation": "The action to take when there is no defined aggregations for the asset.",
"when_not_in_bounds": "The action to take when the aggregation is not breaching either bound.",
"when_bullish": "The action to take when the asset is bullish.",
"when_bearish": "The action to take when the asset is bearish.",
2023-02-10 07:20:19 +00:00
}
2023-02-10 14:33:17 +00:00
class AssetRuleForm(RestrictedFormMixin, ModelForm):
def __init__(self, *args, **kwargs):
super(AssetRuleForm, self).__init__(*args, **kwargs)
self.fields["value"].disabled = True
2023-02-14 07:20:47 +00:00
self.fields["original_status"].disabled = True
self.fields["aggregation"].disabled = True
class Meta:
model = AssetRule
fields = (
"asset",
"aggregation",
"value",
2023-02-14 07:20:47 +00:00
"original_status",
"status",
"trigger_below",
"trigger_above",
)
2023-02-14 07:20:47 +00:00
help_texts = {
"asset": "The asset to apply the rule to.",
"aggregation": "Aggregation of the callback",
"value": "Value of the aggregation",
"original_status": "The original status of the asset.",
"status": "The status of the asset, following rules configured on the Asset Group.",
"trigger_below": "Trigger Bearish when value is below this.",
"trigger_above": "Trigger Bullish when value is above this.",
}
2023-02-15 18:33:38 +00:00
class OrderSettingsForm(RestrictedFormMixin, ModelForm):
class Meta:
model = OrderSettings
fields = (
"name",
"description",
"order_type",
"time_in_force",
"take_profit_percent",
"stop_loss_percent",
"trailing_stop_loss_percent",
"trade_size_percent",
)
help_texts = {
"name": "Name of the order settings. Informational only.",
"description": "Description of the order settings. Informational only.",
"order_type": "Market: Buy/Sell at the current market price. Limit: Buy/Sell at a specified price. Limits protect you more against market slippage.",
"time_in_force": "The time in force controls how the order is executed.",
"take_profit_percent": "The take profit will be set at this percentage above/below the entry price.",
"stop_loss_percent": "The stop loss will be set at this percentage above/below the entry price.",
"trailing_stop_loss_percent": "The trailing stop loss will be set at this percentage above/below the entry price. A trailing stop loss will follow the price as it moves in your favor.",
"trade_size_percent": "Percentage of the account balance to use for each trade.",
}