Finish copy trading

This commit is contained in:
2025-11-20 14:46:54 +07:00
parent ff2df2d9ac
commit 190a9cf12d
13 changed files with 96 additions and 222 deletions

View File

@@ -509,6 +509,7 @@ public class BotController : BaseController
Roi = item.Roi,
Identifier = item.Identifier.ToString(),
AgentName = item.User.AgentName,
MasterAgentName = item.MasterBotUser?.AgentName ?? item.User.AgentName,
CreateDate = item.CreateDate,
StartupTime = item.StartupTime,
Name = item.Name,

View File

@@ -407,7 +407,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
_copyTradingStreamHandle = await streamProvider.GetStream<Position>(streamId)
.SubscribeAsync(OnCopyTradingPositionReceivedAsync);
_logger.LogInformation("LiveTradingBotGrain {GrainId} subscribed to copy trading stream for master bot {MasterBotId}",
_logger.LogInformation(
"LiveTradingBotGrain {GrainId} subscribed to copy trading stream for master bot {MasterBotId}",
this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value);
}
catch (Exception ex)
@@ -425,7 +426,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
{
await _copyTradingStreamHandle.UnsubscribeAsync();
_copyTradingStreamHandle = null;
_logger.LogInformation("LiveTradingBotGrain {GrainId} unsubscribed from copy trading stream", this.GetPrimaryKey());
_logger.LogInformation("LiveTradingBotGrain {GrainId} unsubscribed from copy trading stream",
this.GetPrimaryKey());
}
}
@@ -438,7 +440,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
{
if (_tradingBot == null)
{
_logger.LogWarning("Received copy trading position {PositionId} but trading bot is not running for bot {GrainId}",
_logger.LogWarning(
"Received copy trading position {PositionId} but trading bot is not running for bot {GrainId}",
masterPosition.Identifier, this.GetPrimaryKey());
return;
}
@@ -511,41 +514,52 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
// Check if copy trading authorization is still valid
if (_state.State.Config.IsForCopyTrading && _state.State.Config.MasterBotIdentifier.HasValue)
{
try
// Check if copy trading validation should be bypassed (for testing)
var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")?
.Equals("true", StringComparison.OrdinalIgnoreCase) ?? true;
if (enableValidation)
{
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(_state.State.User);
var masterStrategy = await ServiceScopeHelpers.WithScopedService<IBotService, Bot>(
_scopeFactory,
async botService => await botService.GetBotByIdentifier(_state.State.Config.MasterBotIdentifier.Value));
if (masterStrategy == null)
try
{
_logger.LogWarning("Master strategy {MasterBotId} not found", _state.State.Config.MasterBotIdentifier.Value);
return;
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(_state.State.User);
var masterStrategy = await ServiceScopeHelpers.WithScopedService<IBotService, Bot>(
_scopeFactory,
async botService =>
await botService.GetBotByIdentifier(_state.State.Config.MasterBotIdentifier.Value));
if (masterStrategy == null)
{
_logger.LogWarning("Master strategy {MasterBotId} not found",
_state.State.Config.MasterBotIdentifier.Value);
return;
}
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
string.Equals(key.AgentName, masterStrategy.User.AgentName,
StringComparison.OrdinalIgnoreCase) &&
key.Owned >= 1);
if (!hasMasterStrategyKey)
{
_logger.LogWarning(
"Copy trading bot {GrainId} no longer has authorization for master strategy {MasterBotId}. Stopping bot.",
this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value);
await StopAsync(
"Copy trading authorization revoked - user no longer owns keys for master strategy");
return;
}
}
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
string.Equals(key.AgentName, masterStrategy.User.AgentName, StringComparison.OrdinalIgnoreCase) &&
key.Owned >= 1);
if (!hasMasterStrategyKey)
catch (Exception ex)
{
_logger.LogWarning(
"Copy trading bot {GrainId} no longer has authorization for master strategy {MasterBotId}. Stopping bot.",
_logger.LogError(ex,
"Failed to verify copy trading authorization for bot {GrainId} with master strategy {MasterBotId}. Continuing execution.",
this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value);
await StopAsync("Copy trading authorization revoked - user no longer owns keys for master strategy");
return;
SentrySdk.CaptureException(ex);
}
}
catch (Exception ex)
{
_logger.LogError(ex,
"Failed to verify copy trading authorization for bot {GrainId} with master strategy {MasterBotId}. Continuing execution.",
this.GetPrimaryKey(), _state.State.Config.MasterBotIdentifier.Value);
SentrySdk.CaptureException(ex);
}
}
if (_tradingBot.Positions.Any(p => p.Value.IsOpen() || p.Value.Status.Equals(PositionStatus.New)))
@@ -1128,7 +1142,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
_scopeFactory,
async tradingService => await tradingService.GetPositionsByInitiatorIdentifierAsync(botId));
var openPositions = positions?.Where(p => p.IsOpen() || p.Status.Equals(PositionStatus.New)).ToList() ?? new List<Position>();
var openPositions = positions?.Where(p => p.IsOpen() || p.Status.Equals(PositionStatus.New)).ToList() ??
new List<Position>();
if (openPositions.Any())
{
@@ -1140,13 +1155,16 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
{
try
{
_logger.LogInformation("Closing position {PositionId} for bot {GrainId}", position.Identifier, botId);
_logger.LogInformation("Closing position {PositionId} for bot {GrainId}", position.Identifier,
botId);
await ClosePositionAsync(position.Identifier);
_logger.LogInformation("Successfully closed position {PositionId} for bot {GrainId}", position.Identifier, botId);
_logger.LogInformation("Successfully closed position {PositionId} for bot {GrainId}",
position.Identifier, botId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to close position {PositionId} for bot {GrainId}", position.Identifier, botId);
_logger.LogError(ex, "Failed to close position {PositionId} for bot {GrainId}",
position.Identifier, botId);
// Continue with other positions even if one fails
}
}

View File

@@ -93,13 +93,16 @@ public class TradingBotBase : ITradingBot
{
case BotStatus.Saved:
var indicatorNames = Config.Scenario.Indicators.Select(i => i.Type.ToString()).ToList();
var modeText = Config.IsForWatchingOnly ? "Watch Only" :
Config.IsForCopyTrading ? "Copy Trading" : "Live Trading";
var startupMessage = $"🚀 Bot Started Successfully\n\n" +
$"📊 Trading Setup:\n" +
$"🎯 Ticker: `{Config.Ticker}`\n" +
$"⏰ Timeframe: `{Config.Timeframe}`\n" +
$"🎮 Scenario: `{Config.Scenario?.Name ?? "Unknown"}`\n" +
$"💰 Balance: `${Config.BotTradingBalance:F2}`\n" +
$"👀 Mode: `{(Config.IsForWatchingOnly ? "Watch Only" : "Live Trading")}`\n\n" +
$"👀 Mode: `{modeText}`\n\n" +
$"📈 Active Indicators: `{string.Join(", ", indicatorNames)}`\n\n" +
$"✅ Ready to monitor signals and execute trades\n" +
$"📢 Notifications will be sent when positions are triggered";

View File

@@ -71,7 +71,9 @@ namespace Managing.Application.ManageBot
try
{
var config = await grain.GetConfiguration();
var account = await grain.GetAccount();
var account = await ServiceScopeHelpers.WithScopedService<IAccountService, Account>(
_scopeFactory,
async accountService => await accountService.GetAccount(config.AccountName, true, false));
await grain.StopAsync("Deleting bot");
await _botRepository.DeleteBot(identifier);
await grain.DeleteAsync();

View File

@@ -8,7 +8,6 @@ using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using MediatR;
using System;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
@@ -50,7 +49,7 @@ namespace Managing.Application.ManageBot
// Check if copy trading validation should be bypassed (for testing)
var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")?
.Equals("true", StringComparison.OrdinalIgnoreCase) == true;
.Equals("true", StringComparison.OrdinalIgnoreCase) ?? true;
if (enableValidation)
{

View File

@@ -194,6 +194,7 @@ public class PostgreSqlBotRepository : IBotRepository
var query = _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.AsQueryable();
// Apply filters

View File

@@ -754,7 +754,8 @@ public static class PostgreSqlMappers
Fees = entity.Fees,
LongPositionCount = entity.LongPositionCount,
ShortPositionCount = entity.ShortPositionCount,
MasterBotUserId = entity.MasterBotUserId
MasterBotUserId = entity.MasterBotUserId,
MasterBotUser = entity.MasterBotUser != null ? Map(entity.MasterBotUser) : null
};
return bot;

View File

@@ -4866,7 +4866,6 @@ export interface Backtest {
endDate: Date;
statistics: PerformanceMetrics;
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
user: User;
score: number;
requestId?: string;
@@ -4874,6 +4873,7 @@ export interface Backtest {
scoreMessage?: string;
initialBalance: number;
netPnl: number;
positionCount: number;
}
export interface TradingBotConfig {
@@ -4900,6 +4900,7 @@ export interface TradingBotConfig {
useForDynamicStopLoss?: boolean;
isForCopyTrading?: boolean;
masterBotIdentifier?: string | null;
masterBotUserId?: number | null;
}
export interface LightMoneyManagement {
@@ -5007,6 +5008,7 @@ export interface Position {
initiator: PositionInitiator;
user: User;
initiatorIdentifier: string;
recoveryAttempted?: boolean;
}
export enum TradeDirection {
@@ -5128,11 +5130,6 @@ export interface PerformanceMetrics {
totalPnL?: number;
}
export interface KeyValuePairOfDateTimeAndDecimal {
key?: Date;
value?: number;
}
export interface DeleteBacktestsRequest {
backtestIds: string[];
}
@@ -5153,6 +5150,7 @@ export interface LightBacktestResponse {
scoreMessage: string;
initialBalance: number;
netPnl: number;
positionCount: number;
}
export interface PaginatedBacktestsResponse {
@@ -5200,6 +5198,7 @@ export interface LightBacktest {
ticker?: string | null;
initialBalance?: number;
netPnl?: number;
positionCount?: number;
}
export interface RunBacktestRequest {
@@ -5490,6 +5489,7 @@ export interface TradingBotResponse {
startupTime: Date;
name: string;
ticker: Ticker;
masterAgentName?: string | null;
}
export interface PaginatedResponseOfTradingBotResponse {

View File

@@ -332,7 +332,6 @@ export interface Backtest {
endDate: Date;
statistics: PerformanceMetrics;
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
user: User;
score: number;
requestId?: string;
@@ -340,6 +339,7 @@ export interface Backtest {
scoreMessage?: string;
initialBalance: number;
netPnl: number;
positionCount: number;
}
export interface TradingBotConfig {
@@ -366,6 +366,7 @@ export interface TradingBotConfig {
useForDynamicStopLoss?: boolean;
isForCopyTrading?: boolean;
masterBotIdentifier?: string | null;
masterBotUserId?: number | null;
}
export interface LightMoneyManagement {
@@ -473,6 +474,7 @@ export interface Position {
initiator: PositionInitiator;
user: User;
initiatorIdentifier: string;
recoveryAttempted?: boolean;
}
export enum TradeDirection {
@@ -594,11 +596,6 @@ export interface PerformanceMetrics {
totalPnL?: number;
}
export interface KeyValuePairOfDateTimeAndDecimal {
key?: Date;
value?: number;
}
export interface DeleteBacktestsRequest {
backtestIds: string[];
}
@@ -619,6 +616,7 @@ export interface LightBacktestResponse {
scoreMessage: string;
initialBalance: number;
netPnl: number;
positionCount: number;
}
export interface PaginatedBacktestsResponse {
@@ -666,6 +664,7 @@ export interface LightBacktest {
ticker?: string | null;
initialBalance?: number;
netPnl?: number;
positionCount?: number;
}
export interface RunBacktestRequest {
@@ -956,6 +955,7 @@ export interface TradingBotResponse {
startupTime: Date;
name: string;
ticker: Ticker;
masterAgentName?: string | null;
}
export interface PaginatedResponseOfTradingBotResponse {

View File

@@ -328,6 +328,22 @@ const BotList: React.FC<IBotList> = ({ list }) => {
{
Header: 'Agent',
accessor: 'agentName',
Cell: ({ row }: any) => {
const bot = row.original
const hasMasterAgent = bot.masterAgentName && bot.masterAgentName !== bot.agentName
if (hasMasterAgent) {
return (
<div className="tooltip tooltip-bottom" data-tip={`Strategy created by ${bot.masterAgentName}`}>
<span className="underline decoration-dotted decoration-primary cursor-help">
{bot.agentName}
</span>
</div>
)
}
return <span>{bot.agentName}</span>
},
},
{
Header: 'Win Rate %',

View File

@@ -1,126 +0,0 @@
import React, {useEffect, useState} from 'react'
import {GridTile, Table} from '../../../components/mollecules'
import useApiUrlStore from '../../../app/store/apiStore'
import {type AgentBalanceHistory, type BestAgentsResponse, DataClient} from '../../../generated/ManagingApi'
// Extend the type to include agentBalances for runtime use
export interface AgentBalanceWithBalances extends AgentBalanceHistory {
agentBalances?: Array<{
totalValue?: number
totalAccountUsdValue?: number
botsAllocationUsdValue?: number
pnL?: number
time?: string
}>;
}
const FILTERS = [
{ label: '24H', value: '24H', days: 1 },
{ label: '3D', value: '3D', days: 3 },
{ label: '1W', value: '1W', days: 7 },
{ label: '1M', value: '1M', days: 30 },
{ label: '1Y', value: '1Y', days: 365 },
{ label: 'Total', value: 'Total', days: null },
]
function BestAgents({ index }: { index: number }) {
const { apiUrl } = useApiUrlStore()
const [data, setData] = useState<AgentBalanceWithBalances[]>([])
const [isLoading, setIsLoading] = useState(true)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [totalPages, setTotalPages] = useState(1)
const [selectedFilter, setSelectedFilter] = useState('Total')
useEffect(() => {
setIsLoading(true)
const client = new DataClient({}, apiUrl)
const now = new Date()
// Calculate start date based on selected filter
const filterObj = FILTERS.find(f => f.value === selectedFilter)
let startDate: Date
if (filterObj?.days) {
// Use the filter's days value to calculate start date
startDate = new Date(now.getTime() - filterObj.days * 24 * 60 * 60 * 1000)
} else {
// For 'Total', fetch from a far past date (e.g., 5 years ago)
startDate = new Date(now.getFullYear() - 5, now.getMonth(), now.getDate())
}
client.data_GetBestAgents(startDate, now, page, pageSize).then((res: BestAgentsResponse) => {
setData(res.agents as AgentBalanceWithBalances[] ?? [])
setTotalPages(res.totalPages ?? 1)
console.log(res)
}).finally(() => setIsLoading(false))
}, [apiUrl, page, pageSize, selectedFilter])
function filterBalancesByRange(agent: AgentBalanceWithBalances) {
if (!agent.agentBalances || selectedFilter === 'Total') return agent.agentBalances ?? []
const days = FILTERS.find(f => f.value === selectedFilter)?.days
if (!days) return agent.agentBalances ?? []
const now = new Date()
const cutoff = new Date(now.getTime() - days * 24 * 60 * 60 * 1000)
return agent.agentBalances.filter(b => b.time && new Date(b.time) >= cutoff)
}
// Get the latest balance for each agent
const latestBalances = data.map(agent => {
const filteredBalances = filterBalancesByRange(agent)
if (filteredBalances.length > 0) {
const lastBalance = filteredBalances[filteredBalances.length - 1]
return {
agentName: agent.agentName,
originalAgent: { ...agent, agentBalances: filteredBalances },
...lastBalance
}
}
return { agentName: agent.agentName, originalAgent: { ...agent, agentBalances: filteredBalances } }
})
const columns = [
{ Header: 'Agent', accessor: 'agentName' },
{ Header: 'Total Value (USD)', accessor: 'totalValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'Account Value (USD)', accessor: 'totalAccountUsdValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'Bots Allocation (USD)', accessor: 'botsAllocationUsdValue', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'PnL (USD)', accessor: 'pnL', Cell: ({ value }: any) => value?.toLocaleString(undefined, { maximumFractionDigits: 2 }) },
{ Header: 'Last Update', accessor: 'time', Cell: ({ value }: any) => value ? new Date(value).toLocaleString() : '' },
]
return (
<div className="container mx-auto pt-6">
<GridTile title="Best Agents">
<div className="flex gap-2 mb-3">
{FILTERS.map(f => (
<button
key={f.value}
className={`px-2 py-0.5 text-xs rounded ${selectedFilter === f.value ? 'bg-primary text-primary-content' : 'bg-base-200'}`}
onClick={() => setSelectedFilter(f.value)}
>
{f.label}
</button>
))}
</div>
{isLoading ? (
<progress className="progress progress-primary w-56"></progress>
) : (
<Table
columns={columns}
data={latestBalances}
showPagination={false}
/>
)}
<div className="flex justify-between items-center mt-4">
<button className="btn" onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1}>Previous</button>
<span>Page {page} of {totalPages}</span>
<button className="btn" onClick={() => setPage((p) => Math.min(totalPages, p + 1))} disabled={page === totalPages}>Next</button>
<select className="select select-bordered ml-2" value={pageSize} onChange={e => { setPageSize(Number(e.target.value)); setPage(1) }}>
{[10, 20, 30, 40, 50].map(size => <option key={size} value={size}>Show {size}</option>)}
</select>
</div>
</GridTile>
</div>
)
}
export default BestAgents

View File

@@ -5,7 +5,6 @@ import type {ITabsType} from '../../global/type.tsx'
import Analytics from './analytics/analytics'
import Monitoring from './monitoring'
import BestAgents from './analytics/bestAgents'
import AgentSearch from './agentSearch'
import AgentIndex from './agentIndex'
import AgentStrategy from './agentStrategy'
@@ -27,24 +26,19 @@ const tabs: ITabsType = [
index: 3,
label: 'Analytics',
},
{
Component: BestAgents,
index: 4,
label: 'Best Agents',
},
{
Component: AgentSearch,
index: 5,
index: 4,
label: 'Agent Search',
},
{
Component: AgentIndex,
index: 6,
index: 5,
label: 'Agent Index',
},
{
Component: AgentStrategy,
index: 7,
index: 6,
label: 'Agent Strategy',
},
]

View File

@@ -198,13 +198,6 @@ function PlatformSummary({index}: { index: number }) {
{topStrategies?.topStrategies?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between gap-1 md:gap-2">
<div className="flex items-center gap-1.5 md:gap-3 min-w-0 flex-1">
<div className="avatar placeholder flex-shrink-0 hidden md:flex">
<div className="bg-primary text-primary-content rounded-full w-8 h-8">
<span className="text-xs font-bold">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-xs md:text-sm text-base-content font-medium truncate">
{strategy.strategyName || '[Strategy Name]'}
@@ -235,13 +228,6 @@ function PlatformSummary({index}: { index: number }) {
{topStrategiesByRoi?.topStrategiesByRoi?.slice(0, 3).map((strategy, index) => (
<div key={index} className="flex items-center justify-between gap-1 md:gap-2">
<div className="flex items-center gap-1.5 md:gap-3 min-w-0 flex-1">
<div className="avatar placeholder flex-shrink-0 hidden md:flex">
<div className="bg-success text-success-content rounded-full w-8 h-8">
<span className="text-xs font-bold">
{strategy.strategyName?.charAt(0) || 'S'}
</span>
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-xs md:text-sm text-base-content font-medium truncate">
{strategy.strategyName || '[Strategy Name]'}
@@ -280,13 +266,6 @@ function PlatformSummary({index}: { index: number }) {
{topAgentsByPnL?.slice(0, 3).map((agent, index) => (
<div key={index} className="flex items-center justify-between gap-1 md:gap-2">
<div className="flex items-center gap-1.5 md:gap-3 min-w-0 flex-1">
<div className="avatar placeholder flex-shrink-0 hidden md:flex">
<div className="bg-secondary text-secondary-content rounded-full w-8 h-8">
<span className="text-xs font-bold">
{agent.agentName?.charAt(0) || 'A'}
</span>
</div>
</div>
<div className="min-w-0 flex-1">
<div className="text-xs md:text-sm text-base-content font-medium truncate">
{agent.agentName || '[Agent Name]'}
@@ -485,13 +464,6 @@ function PlatformSummary({index}: { index: number }) {
.map(([asset, volume]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="avatar placeholder">
<div className="bg-primary text-primary-content rounded-full w-8">
<span className="text-xs font-bold">
{asset.substring(0, 2)}
</span>
</div>
</div>
<span className="text-base-content font-medium">{asset}</span>
</div>
<div className="text-right">
@@ -520,13 +492,6 @@ function PlatformSummary({index}: { index: number }) {
.map(([asset, count]) => (
<div key={asset} className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="avatar placeholder">
<div className="bg-secondary text-secondary-content rounded-full w-8">
<span className="text-xs font-bold">
{asset.substring(0, 2)}
</span>
</div>
</div>
<span className="text-base-content font-medium">{asset}</span>
</div>
<div className="text-right">