'Ação', 'etf' => 'ETF', 'bdr' => 'BDR', 'fii' => 'FII', ]; /** * Simula, em ordem cronológica, uma lista de operações (compra/venda) de um * mesmo ativo e devolve o estado final. Regra do PM (preço médio): * - compra: recalcula a média ponderada e aumenta a quantidade. * - venda: só reduz a quantidade; o PM das ações que sobram não muda. * "ok" vem false se em algum ponto a quantidade ficaria negativa (ou seja, * uma venda maior do que o que havia disponível até aquela data). * Cada item de $linhas precisa ter: tipo, quantidade, preco, data_operacao, _ordem. */ function ao_simular_transacoes(array $linhas) { usort($linhas, function ($a, $b) { $an = $a['data_operacao'] === null; $bn = $b['data_operacao'] === null; if ($an !== $bn) { return $an <=> $bn; // datadas primeiro, sem data por último } if (!$an && $a['data_operacao'] !== $b['data_operacao']) { return strcmp($a['data_operacao'], $b['data_operacao']); } return $a['_ordem'] <=> $b['_ordem']; }); $qtd = 0; $pm = 0.0; $ok = true; foreach ($linhas as $l) { $q = (int) $l['quantidade']; $p = (float) $l['preco']; if ($l['tipo'] === 'compra') { $novoQtd = $qtd + $q; $pm = $novoQtd > 0 ? (($qtd * $pm) + ($q * $p)) / $novoQtd : 0.0; $qtd = $novoQtd; } else { $qtd -= $q; if ($qtd < 0) { $ok = false; } } } return ['ok' => $ok, 'qtd' => max(0, $qtd), 'pm' => round($pm, 4)]; } /** Busca as operações de um ativo (de um usuário), no formato usado por ao_simular_transacoes(). */ function ao_buscar_transacoes_ativo(PDO $pdo, $usuario_id, $ativo, $ignorar_id = null) { $sql = 'SELECT id, tipo, quantidade, preco, data_operacao FROM acoes WHERE usuario_id = ? AND ativo = ?'; $params = [$usuario_id, $ativo]; if ($ignorar_id !== null) { $sql .= ' AND id != ?'; $params[] = $ignorar_id; } $stmt = $pdo->prepare($sql); $stmt->execute($params); $linhas = $stmt->fetchAll(); foreach ($linhas as &$l) { $l['_ordem'] = (int) $l['id']; } return $linhas; } /** * Recalcula e salva a posição consolidada (quantidade total + PM) de um * ativo para um usuário, simulando todo o histórico de compras/vendas dele. * Se não sobrar quantidade nenhuma, a posição é removida. */ function ao_recalcular_posicao_acao(PDO $pdo, $usuario_id, $ativo) { $linhas = ao_buscar_transacoes_ativo($pdo, $usuario_id, $ativo); $sim = ao_simular_transacoes($linhas); if ($sim['qtd'] <= 0) { $pdo->prepare('DELETE FROM acoes_posicoes WHERE usuario_id = ? AND ativo = ?')->execute([$usuario_id, $ativo]); return; } // A categoria do ativo é a mesma em todas as operações dele (garantido na // validação do save) — pega de qualquer uma para gravar na posição. $stmtCategoria = $pdo->prepare('SELECT categoria FROM acoes WHERE usuario_id = ? AND ativo = ? LIMIT 1'); $stmtCategoria->execute([$usuario_id, $ativo]); $categoria = $stmtCategoria->fetchColumn() ?: 'acao'; $stmt = $pdo->prepare( 'INSERT INTO acoes_posicoes (usuario_id, ativo, categoria, quantidade, preco_medio) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE categoria = VALUES(categoria), quantidade = VALUES(quantidade), preco_medio = VALUES(preco_medio)' ); $stmt->execute([$usuario_id, $ativo, $categoria, $sim['qtd'], $sim['pm']]); } /** Converte "38,42" ou "38.42" em float. Retorna null se não for um número válido. */ function ao_parse_decimal_br($valor) { $valor = str_replace(',', '.', trim((string) $valor)); return is_numeric($valor) ? (float) $valor : null; } /** Badge colorido com o rótulo (Ação/ETF/BDR/FII) de uma categoria de ativo. */ function ao_badge_categoria($categoria) { $classes = ['acao' => 'ao-badge-neutro', 'etf' => 'ao-badge-info', 'bdr' => 'ao-badge-alerta', 'fii' => 'ao-badge-fii']; $classe = $classes[$categoria] ?? 'ao-badge-neutro'; $rotulo = AO_CATEGORIAS_ATIVO[$categoria] ?? $categoria; return '' . htmlspecialchars($rotulo) . ''; } $erro = null; $sucesso = null; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'excluir') { $id = (int) ($_POST['id'] ?? 0); $stmt = $pdo->prepare('SELECT ativo FROM acoes WHERE id = ? AND usuario_id = ?'); $stmt->execute([$id, $ao_usuario_id]); $linha = $stmt->fetch(); if ($linha) { // Antes de excluir, confere se remover esta operação deixaria alguma // venda posterior sem ações suficientes (ex: excluir uma compra cujas // ações já foram vendidas depois). $sim = ao_simular_transacoes(ao_buscar_transacoes_ativo($pdo, $ao_usuario_id, $linha['ativo'], $id)); if (!$sim['ok']) { $erro = 'Não é possível excluir: isso deixaria uma venda sem ações suficientes na carteira.'; } else { $pdo->prepare('DELETE FROM acoes WHERE id = ? AND usuario_id = ?')->execute([$id, $ao_usuario_id]); ao_recalcular_posicao_acao($pdo, $ao_usuario_id, $linha['ativo']); header('Location: acoes.php?excluido=1'); exit; } } else { header('Location: acoes.php?excluido=1'); exit; } } if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar') { $id = (int) ($_POST['id'] ?? 0); $conta_id = (int) ($_POST['conta_id'] ?? 0); $ativo = strtoupper(trim($_POST['ativo'] ?? '')); $categoria = (string) ($_POST['categoria'] ?? ''); $tipo = ($_POST['tipo'] ?? '') === 'venda' ? 'venda' : 'compra'; $quantidade = (int) ($_POST['quantidade'] ?? 0); $data_operacao = trim($_POST['data_operacao'] ?? ''); $preco_bruto = trim($_POST['preco'] ?? ''); $preco = ao_parse_decimal_br($preco_bruto); $ativo_antigo = null; if (!array_key_exists($categoria, AO_CATEGORIAS_ATIVO)) { $categoria = 'acao'; } // A conta precisa existir e pertencer ao usuário logado — impede que // alguém forje o id de uma conta de outra pessoa no formulário. $stmtConta = $pdo->prepare('SELECT id FROM contas WHERE id = ? AND usuario_id = ?'); $stmtConta->execute([$conta_id, $ao_usuario_id]); $contaValida = (bool) $stmtConta->fetch(); if (!$contaValida) { $erro = 'Selecione uma conta válida.'; } elseif ($ativo === '' || $quantidade <= 0 || $preco === null || $preco <= 0) { $erro = 'Preencha código do ativo, quantidade (maior que zero) e preço (maior que zero) corretamente.'; } elseif ($data_operacao !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data_operacao)) { $erro = 'Data da operação inválida.'; } else { $ignorar_id = $id > 0 ? $id : null; if ($id > 0) { $stmtAntigo = $pdo->prepare('SELECT ativo FROM acoes WHERE id = ? AND usuario_id = ?'); $stmtAntigo->execute([$id, $ao_usuario_id]); $antigo = $stmtAntigo->fetch(); if (!$antigo) { $erro = 'Operação não encontrada.'; } else { $ativo_antigo = $antigo['ativo']; } } if (!$erro) { // A categoria é do ativo, não da operação — todas as operações de // um mesmo código precisam concordar (evita PETR4 virar "ETF" numa // operação e "Ação" noutra por engano). $sqlCategoriaExistente = 'SELECT DISTINCT categoria FROM acoes WHERE usuario_id = ? AND ativo = ?'; $paramsCategoriaExistente = [$ao_usuario_id, $ativo]; if ($ignorar_id !== null) { $sqlCategoriaExistente .= ' AND id != ?'; $paramsCategoriaExistente[] = $ignorar_id; } $stmtCategoriaExistente = $pdo->prepare($sqlCategoriaExistente); $stmtCategoriaExistente->execute($paramsCategoriaExistente); $categoriaExistente = $stmtCategoriaExistente->fetchColumn(); if ($categoriaExistente !== false && $categoriaExistente !== $categoria) { $erro = 'Já existe(m) operação(ões) de ' . htmlspecialchars($ativo) . ' cadastrada(s) como "' . htmlspecialchars(AO_CATEGORIAS_ATIVO[$categoriaExistente]) . '". Selecione a mesma categoria ' . 'para manter a carteira consistente (ou edite as operações antigas se o código estiver errado).'; } } if (!$erro) { // Valida o ativo de destino incluindo esta operação (nova ou editada). $linhasDestino = ao_buscar_transacoes_ativo($pdo, $ao_usuario_id, $ativo, $ignorar_id); $linhasDestino[] = [ 'tipo' => $tipo, 'quantidade' => $quantidade, 'preco' => $preco, 'data_operacao' => $data_operacao !== '' ? $data_operacao : null, '_ordem' => $id > 0 ? $id : PHP_INT_MAX, ]; $simDestino = ao_simular_transacoes($linhasDestino); if (!$simDestino['ok']) { $erro = 'Essa venda deixaria a quantidade de ' . htmlspecialchars($ativo) . ' negativa em alguma data — ' . 'você não pode vender mais ações do que possuía até aquele momento.'; } elseif ($ativo_antigo !== null && $ativo_antigo !== $ativo) { // O código do ativo mudou nesta edição: confere se o ativo antigo // continua consistente sem esta operação (que está saindo dele). $simAntigo = ao_simular_transacoes(ao_buscar_transacoes_ativo($pdo, $ao_usuario_id, $ativo_antigo, $id)); if (!$simAntigo['ok']) { $erro = 'Não é possível mover esta operação para outro ativo: sobraria uma venda de ' . htmlspecialchars($ativo_antigo) . ' sem ações suficientes.'; } } } if (!$erro) { try { if ($id > 0) { $stmt = $pdo->prepare('UPDATE acoes SET conta_id=?, ativo=?, categoria=?, tipo=?, quantidade=?, data_operacao=?, preco=? WHERE id=? AND usuario_id=?'); $stmt->execute([$conta_id, $ativo, $categoria, $tipo, $quantidade, $data_operacao !== '' ? $data_operacao : null, $preco, $id, $ao_usuario_id]); } else { $stmt = $pdo->prepare('INSERT INTO acoes (usuario_id, conta_id, ativo, categoria, tipo, quantidade, data_operacao, preco) VALUES (?,?,?,?,?,?,?,?)'); $stmt->execute([$ao_usuario_id, $conta_id, $ativo, $categoria, $tipo, $quantidade, $data_operacao !== '' ? $data_operacao : null, $preco]); } ao_recalcular_posicao_acao($pdo, $ao_usuario_id, $ativo); if ($ativo_antigo !== null && $ativo_antigo !== $ativo) { ao_recalcular_posicao_acao($pdo, $ao_usuario_id, $ativo_antigo); } header('Location: acoes.php?salvo=1'); exit; } catch (PDOException $e) { $erro = 'Erro ao salvar: ' . $e->getMessage(); } } } } $editando = null; if (isset($_GET['editar'])) { $stmt = $pdo->prepare('SELECT * FROM acoes WHERE id = ? AND usuario_id = ?'); $stmt->execute([(int) $_GET['editar'], $ao_usuario_id]); $editando = $stmt->fetch() ?: null; } // Se deu erro no envio, mantemos o que foi digitado na tela em vez de limpar tudo. if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['acao'] ?? '') === 'salvar' && $erro && $erro !== 'Operação não encontrada.') { $editando = [ 'id' => $id, 'conta_id' => $conta_id, 'ativo' => $ativo, 'categoria' => $categoria, 'tipo' => $tipo, 'quantidade' => $quantidade, 'data_operacao' => $data_operacao, 'preco' => $preco_bruto, ]; } if (isset($_GET['salvo'])) $sucesso = 'Operação salva com sucesso.'; if (isset($_GET['excluido'])) $sucesso = 'Operação excluída.'; $stmtContas = $pdo->prepare( 'SELECT ct.id, ct.nome AS conta_nome, cr.nome AS corretora_nome FROM contas ct INNER JOIN corretoras cr ON cr.id = ct.corretora_id WHERE ct.usuario_id = ? ORDER BY cr.nome, ct.nome' ); $stmtContas->execute([$ao_usuario_id]); $contas = $stmtContas->fetchAll(); $stmtPosicoes = $pdo->prepare('SELECT * FROM acoes_posicoes WHERE usuario_id = ? ORDER BY ativo'); $stmtPosicoes->execute([$ao_usuario_id]); $posicoes = $stmtPosicoes->fetchAll(); $stmtOperacoes = $pdo->prepare( 'SELECT a.*, ct.nome AS conta_nome, cr.nome AS corretora_nome FROM acoes a LEFT JOIN contas ct ON ct.id = a.conta_id LEFT JOIN corretoras cr ON cr.id = ct.corretora_id WHERE a.usuario_id = ? ORDER BY (a.data_operacao IS NULL), a.data_operacao DESC, a.id DESC' ); $stmtOperacoes->execute([$ao_usuario_id]); $operacoes = $stmtOperacoes->fetchAll(); require __DIR__ . '/includes/header.php'; ?>

Minha carteira de ações

Antes de cadastrar uma operação, você precisa ter uma corretora e uma conta cadastradas. Cadastre uma corretora e depois cadastre uma conta nela.

Carteira consolidada
AtivoCategoriaQuantidadePreço médio (PM)Valor investido
Nenhuma ação em carteira no momento.
R$ R$
Histórico de operações
DataAtivoCategoriaContaTipoQuantidadePreço
Nenhuma operação cadastrada ainda.
R$ Editar