Fixed formatting for return column.
This commit is contained in:
@@ -96,7 +96,10 @@ defmodule Core.Bots.BotPersistence do
|
|||||||
|
|
||||||
case Core.Bots.BotSupervisor.start_bot(symbol, restore_state: bot_state) do
|
case Core.Bots.BotSupervisor.start_bot(symbol, restore_state: bot_state) do
|
||||||
{:ok, pid} ->
|
{:ok, pid} ->
|
||||||
Logger.info("Restored bot for #{symbol} with #{bot_state["shares"]} shares at avg $#{bot_state["avg_price"]}")
|
Logger.info(
|
||||||
|
"Restored bot for #{symbol} with #{bot_state["shares"]} shares at avg $#{bot_state["avg_price"]}"
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
|
|
||||||
{:error, {:already_started, pid}} ->
|
{:error, {:already_started, pid}} ->
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ defmodule Core.Bots.BotSupervisor do
|
|||||||
{:ok, :deferred} ->
|
{:ok, :deferred} ->
|
||||||
# Sell deferred until market opens - keep bot alive
|
# Sell deferred until market opens - keep bot alive
|
||||||
require Logger
|
require Logger
|
||||||
Logger.info("Sell deferred for #{symbol} - bot will stop after selling when market opens")
|
|
||||||
|
Logger.info(
|
||||||
|
"Sell deferred for #{symbol} - bot will stop after selling when market opens"
|
||||||
|
)
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
|
|||||||
@@ -58,9 +58,12 @@ defmodule Core.Bots.DCABot do
|
|||||||
use GenServer
|
use GenServer
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@tick_interval 60_000 # 1 minute
|
# 1 minute
|
||||||
@drop_threshold 0.03 # 3% price drop triggers auto-buy
|
@tick_interval 60_000
|
||||||
@profit_threshold 0.20 # 20% total return triggers auto-sell
|
# 3% price drop triggers auto-buy
|
||||||
|
@drop_threshold 0.03
|
||||||
|
# 20% total return triggers auto-sell
|
||||||
|
@profit_threshold 0.20
|
||||||
|
|
||||||
# Public API
|
# Public API
|
||||||
|
|
||||||
@@ -169,7 +172,9 @@ defmodule Core.Bots.DCABot do
|
|||||||
return_pct = restore_state["return_pct"] || 0.0
|
return_pct = restore_state["return_pct"] || 0.0
|
||||||
last_purchase_price = restore_state["last_purchase_price"]
|
last_purchase_price = restore_state["last_purchase_price"]
|
||||||
|
|
||||||
Logger.info("DCABot RESTORED for #{symbol}: #{shares} shares @ avg $#{Float.round(avg_price, 2)} | Return: #{Float.round(return_pct, 2)}%")
|
Logger.info(
|
||||||
|
"DCABot RESTORED for #{symbol}: #{shares} shares @ avg $#{Float.round(avg_price, 2)} | Return: #{Float.round(return_pct, 2)}%"
|
||||||
|
)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
@@ -271,8 +276,22 @@ defmodule Core.Bots.DCABot do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call(:position, _from, %{symbol: symbol, bot_shares: bot_shares, bot_cost_basis: bot_cost_basis, current_return_pct: current_return_pct, current_pl_dollars: current_pl_dollars, last_price: last_price, last_purchase_price: last_purchase_price, sell_and_stop_pending: sell_and_stop_pending} = state) do
|
def handle_call(
|
||||||
next_buy_price = if last_purchase_price, do: last_purchase_price * (1 - @drop_threshold), else: nil
|
:position,
|
||||||
|
_from,
|
||||||
|
%{
|
||||||
|
symbol: symbol,
|
||||||
|
bot_shares: bot_shares,
|
||||||
|
bot_cost_basis: bot_cost_basis,
|
||||||
|
current_return_pct: current_return_pct,
|
||||||
|
current_pl_dollars: current_pl_dollars,
|
||||||
|
last_price: last_price,
|
||||||
|
last_purchase_price: last_purchase_price,
|
||||||
|
sell_and_stop_pending: sell_and_stop_pending
|
||||||
|
} = state
|
||||||
|
) do
|
||||||
|
next_buy_price =
|
||||||
|
if last_purchase_price, do: last_purchase_price * (1 - @drop_threshold), else: nil
|
||||||
|
|
||||||
position_info = %{
|
position_info = %{
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
@@ -324,7 +343,14 @@ defmodule Core.Bots.DCABot do
|
|||||||
Process.send_after(self(), :tick, @tick_interval)
|
Process.send_after(self(), :tick, @tick_interval)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_price(%{symbol: symbol, last_purchase_price: last_purchase_price, bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state) do
|
defp check_price(
|
||||||
|
%{
|
||||||
|
symbol: symbol,
|
||||||
|
last_purchase_price: last_purchase_price,
|
||||||
|
bot_shares: bot_shares,
|
||||||
|
bot_cost_basis: bot_cost_basis
|
||||||
|
} = state
|
||||||
|
) do
|
||||||
case Core.Client.get_quotes(symbol) do
|
case Core.Client.get_quotes(symbol) do
|
||||||
{:ok, [quote | _]} ->
|
{:ok, [quote | _]} ->
|
||||||
price = quote.last
|
price = quote.last
|
||||||
@@ -343,12 +369,20 @@ defmodule Core.Bots.DCABot do
|
|||||||
# Log price and bot position status
|
# Log price and bot position status
|
||||||
if bot_shares > 0 do
|
if bot_shares > 0 do
|
||||||
avg_cost = Float.round(bot_cost_basis / bot_shares, 2)
|
avg_cost = Float.round(bot_cost_basis / bot_shares, 2)
|
||||||
Logger.info("#{symbol} price: $#{price} | Bot: #{bot_shares} shares @ avg $#{avg_cost} | Return: #{return_pct}%")
|
|
||||||
|
Logger.info(
|
||||||
|
"#{symbol} price: $#{price} | Bot: #{bot_shares} shares @ avg $#{avg_cost} | Return: #{return_pct}%"
|
||||||
|
)
|
||||||
else
|
else
|
||||||
Logger.info("#{symbol} current price: $#{price} | Bot: no position")
|
Logger.info("#{symbol} current price: $#{price} | Bot: no position")
|
||||||
end
|
end
|
||||||
|
|
||||||
state = %{state | last_price: price, current_return_pct: return_pct, current_pl_dollars: pl_dollars}
|
state = %{
|
||||||
|
state
|
||||||
|
| last_price: price,
|
||||||
|
current_return_pct: return_pct,
|
||||||
|
current_pl_dollars: pl_dollars
|
||||||
|
}
|
||||||
|
|
||||||
# Check if we should auto-sell (position up 20%)
|
# Check if we should auto-sell (position up 20%)
|
||||||
state = maybe_auto_sell(state, price)
|
state = maybe_auto_sell(state, price)
|
||||||
@@ -368,12 +402,16 @@ defmodule Core.Bots.DCABot do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_auto_buy(state, current_price, last_purchase_price) when is_number(last_purchase_price) do
|
defp maybe_auto_buy(state, current_price, last_purchase_price)
|
||||||
|
when is_number(last_purchase_price) do
|
||||||
price_drop = (last_purchase_price - current_price) / last_purchase_price
|
price_drop = (last_purchase_price - current_price) / last_purchase_price
|
||||||
|
|
||||||
if price_drop >= @drop_threshold do
|
if price_drop >= @drop_threshold do
|
||||||
drop_percent = Float.round(price_drop * 100, 2)
|
drop_percent = Float.round(price_drop * 100, 2)
|
||||||
Logger.info("Price dropped #{drop_percent}% from last purchase ($#{last_purchase_price}). Auto-buying...")
|
|
||||||
|
Logger.info(
|
||||||
|
"Price dropped #{drop_percent}% from last purchase ($#{last_purchase_price}). Auto-buying..."
|
||||||
|
)
|
||||||
|
|
||||||
case execute_buy(state, :auto) do
|
case execute_buy(state, :auto) do
|
||||||
{:ok, _order, purchase_price, shares} ->
|
{:ok, _order, purchase_price, shares} ->
|
||||||
@@ -392,7 +430,10 @@ defmodule Core.Bots.DCABot do
|
|||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_auto_sell(%{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state, current_price) do
|
defp maybe_auto_sell(
|
||||||
|
%{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state,
|
||||||
|
current_price
|
||||||
|
) do
|
||||||
if bot_shares > 0 && bot_cost_basis > 0 do
|
if bot_shares > 0 && bot_cost_basis > 0 do
|
||||||
current_value = bot_shares * current_price
|
current_value = bot_shares * current_price
|
||||||
total_return = (current_value - bot_cost_basis) / bot_cost_basis
|
total_return = (current_value - bot_cost_basis) / bot_cost_basis
|
||||||
@@ -400,11 +441,17 @@ defmodule Core.Bots.DCABot do
|
|||||||
if total_return >= @profit_threshold do
|
if total_return >= @profit_threshold do
|
||||||
profit_percent = Float.round(total_return * 100, 2)
|
profit_percent = Float.round(total_return * 100, 2)
|
||||||
profit_amount = Float.round(current_value - bot_cost_basis, 2)
|
profit_amount = Float.round(current_value - bot_cost_basis, 2)
|
||||||
Logger.info("Bot position up #{profit_percent}% (profit: $#{profit_amount}). Auto-selling #{bot_shares} shares...")
|
|
||||||
|
Logger.info(
|
||||||
|
"Bot position up #{profit_percent}% (profit: $#{profit_amount}). Auto-selling #{bot_shares} shares..."
|
||||||
|
)
|
||||||
|
|
||||||
case execute_sell(state, bot_shares) do
|
case execute_sell(state, bot_shares) do
|
||||||
{:ok, _order} ->
|
{:ok, _order} ->
|
||||||
Logger.info("Successfully sold all bot shares. Re-establishing position with 5% of account...")
|
Logger.info(
|
||||||
|
"Successfully sold all bot shares. Re-establishing position with 5% of account..."
|
||||||
|
)
|
||||||
|
|
||||||
state = reset_bot_position(state)
|
state = reset_bot_position(state)
|
||||||
|
|
||||||
# Re-establish position immediately
|
# Re-establish position immediately
|
||||||
@@ -442,14 +489,21 @@ defmodule Core.Bots.DCABot do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp update_bot_position(%{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state, shares, price) do
|
defp update_bot_position(
|
||||||
|
%{bot_shares: bot_shares, bot_cost_basis: bot_cost_basis} = state,
|
||||||
|
shares,
|
||||||
|
price
|
||||||
|
) do
|
||||||
new_shares = bot_shares + shares
|
new_shares = bot_shares + shares
|
||||||
new_cost_basis = bot_cost_basis + (shares * price)
|
new_cost_basis = bot_cost_basis + shares * price
|
||||||
|
|
||||||
Logger.info("Bot position updated: #{new_shares} total shares, cost basis: $#{Float.round(new_cost_basis, 2)}")
|
Logger.info(
|
||||||
|
"Bot position updated: #{new_shares} total shares, cost basis: $#{Float.round(new_cost_basis, 2)}"
|
||||||
|
)
|
||||||
|
|
||||||
new_state = %{state |
|
new_state = %{
|
||||||
bot_shares: new_shares,
|
state
|
||||||
|
| bot_shares: new_shares,
|
||||||
bot_cost_basis: new_cost_basis,
|
bot_cost_basis: new_cost_basis,
|
||||||
last_purchase_price: price
|
last_purchase_price: price
|
||||||
}
|
}
|
||||||
@@ -461,8 +515,9 @@ defmodule Core.Bots.DCABot do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp reset_bot_position(state) do
|
defp reset_bot_position(state) do
|
||||||
new_state = %{state |
|
new_state = %{
|
||||||
bot_shares: 0,
|
state
|
||||||
|
| bot_shares: 0,
|
||||||
bot_cost_basis: 0.0,
|
bot_cost_basis: 0.0,
|
||||||
last_purchase_price: nil,
|
last_purchase_price: nil,
|
||||||
current_return_pct: 0.0,
|
current_return_pct: 0.0,
|
||||||
@@ -495,8 +550,14 @@ defmodule Core.Bots.DCABot do
|
|||||||
true -> "Manual buy"
|
true -> "Manual buy"
|
||||||
end
|
end
|
||||||
|
|
||||||
Logger.info("#{trigger_type}: Purchasing power: $#{Float.round(buying_power, 2)} | Using 5%: $#{Float.round(cash_amount, 2)}")
|
Logger.info(
|
||||||
Logger.info("#{trigger_type} FILLED: #{actual_shares} shares of #{symbol} at $#{actual_price} (total: $#{Float.round(total_cost, 2)})")
|
"#{trigger_type}: Purchasing power: $#{Float.round(buying_power, 2)} | Using 5%: $#{Float.round(cash_amount, 2)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
"#{trigger_type} FILLED: #{actual_shares} shares of #{symbol} at $#{actual_price} (total: $#{Float.round(total_cost, 2)})"
|
||||||
|
)
|
||||||
|
|
||||||
{:ok, filled_order, actual_price, actual_shares}
|
{:ok, filled_order, actual_price, actual_shares}
|
||||||
else
|
else
|
||||||
{:error, reason} = error ->
|
{:error, reason} = error ->
|
||||||
@@ -528,7 +589,8 @@ defmodule Core.Bots.DCABot do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_buying_power(%{margin: %{stock_buying_power: buying_power}}) when is_number(buying_power) do
|
defp get_buying_power(%{margin: %{stock_buying_power: buying_power}})
|
||||||
|
when is_number(buying_power) do
|
||||||
buying_power
|
buying_power
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -604,13 +666,19 @@ defmodule Core.Bots.DCABot do
|
|||||||
{:error, {:order_rejected, order.status}}
|
{:error, {:order_rejected, order.status}}
|
||||||
|
|
||||||
Core.Order.pending?(order) && retries > 0 ->
|
Core.Order.pending?(order) && retries > 0 ->
|
||||||
Logger.debug("Order #{order_id} for #{symbol} still pending (#{order.status}), retrying...")
|
Logger.debug(
|
||||||
|
"Order #{order_id} for #{symbol} still pending (#{order.status}), retrying..."
|
||||||
|
)
|
||||||
|
|
||||||
# Wait 500ms before checking again
|
# Wait 500ms before checking again
|
||||||
Process.sleep(500)
|
Process.sleep(500)
|
||||||
verify_order_filled(account_id, order_id, symbol, retries - 1)
|
verify_order_filled(account_id, order_id, symbol, retries - 1)
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
Logger.error("Order #{order_id} for #{symbol} timed out or unknown status: #{order.status}")
|
Logger.error(
|
||||||
|
"Order #{order_id} for #{symbol} timed out or unknown status: #{order.status}"
|
||||||
|
)
|
||||||
|
|
||||||
{:error, {:order_timeout, order.status}}
|
{:error, {:order_timeout, order.status}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ defmodule TraderexWeb.DashboardLive do
|
|||||||
}
|
}
|
||||||
|
|
||||||
bots = Core.Bots.BotSupervisor.list_bots()
|
bots = Core.Bots.BotSupervisor.list_bots()
|
||||||
market_clock = case Core.Client.get_market_clock() do
|
|
||||||
|
market_clock =
|
||||||
|
case Core.Client.get_market_clock() do
|
||||||
{:ok, clock} -> clock
|
{:ok, clock} -> clock
|
||||||
{:error, _} -> nil
|
{:error, _} -> nil
|
||||||
end
|
end
|
||||||
@@ -317,12 +319,12 @@ defmodule TraderexWeb.DashboardLive do
|
|||||||
</:col>
|
</:col>
|
||||||
<:col :let={bot} label="Return">
|
<:col :let={bot} label="Return">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class={return_color(bot.return_pct)}>
|
|
||||||
{format_percent_simple(bot.return_pct)}
|
|
||||||
</span>
|
|
||||||
<span class={["text-sm", pl_color(Map.get(bot, :pl_dollars, 0))]}>
|
<span class={["text-sm", pl_color(Map.get(bot, :pl_dollars, 0))]}>
|
||||||
{format_currency(Map.get(bot, :pl_dollars, 0))}
|
{format_currency(Map.get(bot, :pl_dollars, 0))}
|
||||||
</span>
|
</span>
|
||||||
|
<span class={return_color(bot.return_pct)}>
|
||||||
|
({format_percent_simple(bot.return_pct)})
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</:col>
|
</:col>
|
||||||
<:col :let={bot} label="Actions">
|
<:col :let={bot} label="Actions">
|
||||||
@@ -386,8 +388,7 @@ defmodule TraderexWeb.DashboardLive do
|
|||||||
phx-value-sell="false"
|
phx-value-sell="false"
|
||||||
class="btn btn-warning flex-1"
|
class="btn btn-warning flex-1"
|
||||||
>
|
>
|
||||||
<.icon name="hero-pause" class="size-5" />
|
<.icon name="hero-pause" class="size-5" /> Keep Shares & Stop Bot
|
||||||
Keep Shares & Stop Bot
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -396,8 +397,7 @@ defmodule TraderexWeb.DashboardLive do
|
|||||||
phx-value-sell="true"
|
phx-value-sell="true"
|
||||||
class="btn btn-error flex-1"
|
class="btn btn-error flex-1"
|
||||||
>
|
>
|
||||||
<.icon name="hero-arrow-trending-down" class="size-5" />
|
<.icon name="hero-arrow-trending-down" class="size-5" /> Sell All & Stop Bot
|
||||||
Sell All & Stop Bot
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
|
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
|
||||||
@@ -419,8 +419,7 @@ defmodule TraderexWeb.DashboardLive do
|
|||||||
phx-value-sell="false"
|
phx-value-sell="false"
|
||||||
class="btn btn-error"
|
class="btn btn-error"
|
||||||
>
|
>
|
||||||
<.icon name="hero-stop" class="size-5" />
|
<.icon name="hero-stop" class="size-5" /> Stop Bot
|
||||||
Stop Bot
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
|
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
|
||||||
@@ -585,7 +584,10 @@ defmodule TraderexWeb.DashboardLive do
|
|||||||
market_status_class(@clock.state)
|
market_status_class(@clock.state)
|
||||||
]}>
|
]}>
|
||||||
<span class="relative flex h-2 w-2">
|
<span class="relative flex h-2 w-2">
|
||||||
<span :if={@clock.state == "open"} class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75">
|
<span
|
||||||
|
:if={@clock.state == "open"}
|
||||||
|
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75"
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<span class={[
|
<span class={[
|
||||||
"relative inline-flex rounded-full h-2 w-2",
|
"relative inline-flex rounded-full h-2 w-2",
|
||||||
|
|||||||
Reference in New Issue
Block a user