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
|
||||
{: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}
|
||||
|
||||
{:error, {:already_started, pid}} ->
|
||||
|
||||
@@ -52,7 +52,11 @@ defmodule Core.Bots.BotSupervisor do
|
||||
{:ok, :deferred} ->
|
||||
# Sell deferred until market opens - keep bot alive
|
||||
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
|
||||
|
||||
{:error, reason} ->
|
||||
|
||||
@@ -58,9 +58,12 @@ defmodule Core.Bots.DCABot do
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
@tick_interval 60_000 # 1 minute
|
||||
@drop_threshold 0.03 # 3% price drop triggers auto-buy
|
||||
@profit_threshold 0.20 # 20% total return triggers auto-sell
|
||||
# 1 minute
|
||||
@tick_interval 60_000
|
||||
# 3% price drop triggers auto-buy
|
||||
@drop_threshold 0.03
|
||||
# 20% total return triggers auto-sell
|
||||
@profit_threshold 0.20
|
||||
|
||||
# Public API
|
||||
|
||||
@@ -169,7 +172,9 @@ defmodule Core.Bots.DCABot do
|
||||
return_pct = restore_state["return_pct"] || 0.0
|
||||
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,
|
||||
@@ -271,8 +276,22 @@ defmodule Core.Bots.DCABot do
|
||||
end
|
||||
|
||||
@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
|
||||
next_buy_price = if last_purchase_price, do: last_purchase_price * (1 - @drop_threshold), else: nil
|
||||
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
|
||||
next_buy_price =
|
||||
if last_purchase_price, do: last_purchase_price * (1 - @drop_threshold), else: nil
|
||||
|
||||
position_info = %{
|
||||
symbol: symbol,
|
||||
@@ -324,7 +343,14 @@ defmodule Core.Bots.DCABot do
|
||||
Process.send_after(self(), :tick, @tick_interval)
|
||||
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
|
||||
{:ok, [quote | _]} ->
|
||||
price = quote.last
|
||||
@@ -343,12 +369,20 @@ defmodule Core.Bots.DCABot do
|
||||
# Log price and bot position status
|
||||
if bot_shares > 0 do
|
||||
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
|
||||
Logger.info("#{symbol} current price: $#{price} | Bot: no position")
|
||||
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%)
|
||||
state = maybe_auto_sell(state, price)
|
||||
@@ -368,12 +402,16 @@ defmodule Core.Bots.DCABot do
|
||||
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
|
||||
|
||||
if price_drop >= @drop_threshold do
|
||||
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
|
||||
{:ok, _order, purchase_price, shares} ->
|
||||
@@ -392,7 +430,10 @@ defmodule Core.Bots.DCABot do
|
||||
state
|
||||
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
|
||||
current_value = bot_shares * current_price
|
||||
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
|
||||
profit_percent = Float.round(total_return * 100, 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
|
||||
{: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)
|
||||
|
||||
# Re-establish position immediately
|
||||
@@ -442,16 +489,23 @@ defmodule Core.Bots.DCABot do
|
||||
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_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 |
|
||||
bot_shares: new_shares,
|
||||
bot_cost_basis: new_cost_basis,
|
||||
last_purchase_price: price
|
||||
new_state = %{
|
||||
state
|
||||
| bot_shares: new_shares,
|
||||
bot_cost_basis: new_cost_basis,
|
||||
last_purchase_price: price
|
||||
}
|
||||
|
||||
# Save state after position update
|
||||
@@ -461,12 +515,13 @@ defmodule Core.Bots.DCABot do
|
||||
end
|
||||
|
||||
defp reset_bot_position(state) do
|
||||
new_state = %{state |
|
||||
bot_shares: 0,
|
||||
bot_cost_basis: 0.0,
|
||||
last_purchase_price: nil,
|
||||
current_return_pct: 0.0,
|
||||
current_pl_dollars: 0.0
|
||||
new_state = %{
|
||||
state
|
||||
| bot_shares: 0,
|
||||
bot_cost_basis: 0.0,
|
||||
last_purchase_price: nil,
|
||||
current_return_pct: 0.0,
|
||||
current_pl_dollars: 0.0
|
||||
}
|
||||
|
||||
# Save state after reset
|
||||
@@ -495,8 +550,14 @@ defmodule Core.Bots.DCABot do
|
||||
true -> "Manual buy"
|
||||
end
|
||||
|
||||
Logger.info("#{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)})")
|
||||
Logger.info(
|
||||
"#{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}
|
||||
else
|
||||
{:error, reason} = error ->
|
||||
@@ -528,7 +589,8 @@ defmodule Core.Bots.DCABot do
|
||||
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
|
||||
end
|
||||
|
||||
@@ -604,13 +666,19 @@ defmodule Core.Bots.DCABot do
|
||||
{:error, {:order_rejected, order.status}}
|
||||
|
||||
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
|
||||
Process.sleep(500)
|
||||
verify_order_filled(account_id, order_id, symbol, retries - 1)
|
||||
|
||||
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}}
|
||||
end
|
||||
|
||||
|
||||
@@ -42,10 +42,12 @@ defmodule TraderexWeb.DashboardLive do
|
||||
}
|
||||
|
||||
bots = Core.Bots.BotSupervisor.list_bots()
|
||||
market_clock = case Core.Client.get_market_clock() do
|
||||
{:ok, clock} -> clock
|
||||
{:error, _} -> nil
|
||||
end
|
||||
|
||||
market_clock =
|
||||
case Core.Client.get_market_clock() do
|
||||
{:ok, clock} -> clock
|
||||
{:error, _} -> nil
|
||||
end
|
||||
|
||||
socket =
|
||||
case results do
|
||||
@@ -317,12 +319,12 @@ defmodule TraderexWeb.DashboardLive do
|
||||
</:col>
|
||||
<:col :let={bot} label="Return">
|
||||
<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))]}>
|
||||
{format_currency(Map.get(bot, :pl_dollars, 0))}
|
||||
</span>
|
||||
<span class={return_color(bot.return_pct)}>
|
||||
({format_percent_simple(bot.return_pct)})
|
||||
</span>
|
||||
</div>
|
||||
</:col>
|
||||
<:col :let={bot} label="Actions">
|
||||
@@ -386,8 +388,7 @@ defmodule TraderexWeb.DashboardLive do
|
||||
phx-value-sell="false"
|
||||
class="btn btn-warning flex-1"
|
||||
>
|
||||
<.icon name="hero-pause" class="size-5" />
|
||||
Keep Shares & Stop Bot
|
||||
<.icon name="hero-pause" class="size-5" /> Keep Shares & Stop Bot
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -396,8 +397,7 @@ defmodule TraderexWeb.DashboardLive do
|
||||
phx-value-sell="true"
|
||||
class="btn btn-error flex-1"
|
||||
>
|
||||
<.icon name="hero-arrow-trending-down" class="size-5" />
|
||||
Sell All & Stop Bot
|
||||
<.icon name="hero-arrow-trending-down" class="size-5" /> Sell All & Stop Bot
|
||||
</button>
|
||||
|
||||
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
|
||||
@@ -419,8 +419,7 @@ defmodule TraderexWeb.DashboardLive do
|
||||
phx-value-sell="false"
|
||||
class="btn btn-error"
|
||||
>
|
||||
<.icon name="hero-stop" class="size-5" />
|
||||
Stop Bot
|
||||
<.icon name="hero-stop" class="size-5" /> Stop Bot
|
||||
</button>
|
||||
|
||||
<button phx-click="close_stop_bot_modal" class="btn btn-ghost">
|
||||
@@ -585,7 +584,10 @@ defmodule TraderexWeb.DashboardLive do
|
||||
market_status_class(@clock.state)
|
||||
]}>
|
||||
<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 class={[
|
||||
"relative inline-flex rounded-full h-2 w-2",
|
||||
|
||||
Reference in New Issue
Block a user