Se você usou wget para baixar finance.zip antes das 2024-04-10T08:30:00-04:00, certifique-se de copiar, colar e executar este comando dentro do seu diretório finance para baixar uma versão mais recente de helpers.py:
[ -f helpers.py ] && curl -O https://cdn.cs50.net/2024/x/psets/9/finance/helpers.py
C$50 Finanças
Implemente um site no qual usuários podem "comprar" e "vender" ações, como abaixo.

Histórico
Se você não tem certeza sobre o que significa comprar e vender ações (por ex., as porcentagens de uma empresa), vá aqui para um tutorial.
Você está prestes a implementar o C$50 Finanças, um aplicativo web no qual você pode gerenciar portfólios de ações. Essa ferramenta não só permitirá que você confira os preços reais das ações e os valores dos portfólios, como também permitirá que você compre (ok, "compre") e venda (ok, "venda") ações consultando os preços das ações.
De fato, existem ferramentas (uma é conhecida como IEX) que permitem que você baixe cotações de ações pela API (interface de programação de aplicativos) usando URLs como https://api.iex.cloud/v1/data/core/quote/nflx?token=API_KEY. Observe como o símbolo da Netflix (NFLX) está embutido neste URL; é assim que o IEX sabe de quem deve retornar os dados. Esse link não retornará nenhum dado porque o IEX exige que você use uma chave de API, mas se retornasse, você veria uma resposta no formato JSON (JavaScript Object Notation) como este:
{
"avgTotalVolume":6787785,
"calculationPrice":"tops",
"change":1.46,
"changePercent":0.00336,
"close":null,
"closeSource":"official",
"closeTime":null,
"companyName":"Netflix Inc.",
"currency":"USD",
"delayedPrice":null,
"delayedPriceTime":null,
"extendedChange":null,
"extendedChangePercent":null,
"extendedPrice":null,
"extendedPriceTime":null,
"high":null,
"highSource":"IEX real time price",
"highTime":1699626600947,
"iexAskPrice":460.87,
"iexAskSize":123,
"iexBidPrice":435,
"iexBidSize":100,
"iexClose":436.61,
"iexCloseTime":1699626704609,
"iexLastUpdated":1699626704609,
"iexMarketPercent":0.00864679844447232,
"iexOpen":437.37,
"iexOpenTime":1699626600859,
"iexRealtimePrice":436.61,
"iexRealtimeSize":5,
"iexVolume":965,
"lastTradeTime":1699626704609,
"latestPrice":436.61,
"latestSource":"IEX real time price",
"latestTime":"9:31:44 AM",
"latestUpdate":1699626704609,
"latestVolume":null,
"low":null,
"lowSource":"IEX real time price",
"lowTime":1699626634509,
"marketCap":192892118443,
"oddLotDelayedPrice":null,
"oddLotDelayedPriceTime":null,
"open":null,
"openTime":null,
"openSource":"official",
"peRatio":43.57,
"previousClose":435.15,
"previousVolume":2735507,
"primaryExchange":"NASDAQ",
"symbol":"NFLX",
"volume":null,
"week52High":485,
"week52Low":271.56,
"ytdChange":0.4790450244167119,
"isUSMarketOpen":true
}
Observe como, entre as chaves, há uma lista separada por vírgulas de pares chave-valor, com dois pontos separando cada chave do seu valor. Faremos algo muito parecido, com o Yahoo Finance.
Vamos nos concentrar agora em obter o código de distribuição deste problema!
Começando
Faça login em cs50.dev, clique na sua janela de terminal e execute cd por si só. Você deve descobrir que o prompt da janela de terminal se parece com o abaixo:
$
Em seguida, execute
wget https://cdn.cs50.net/2024/x/psets/9/finance.zip
para baixar um ZIP chamado finance.zip no seu espaço de código.
Então execute
unzip finance.zip
para criar uma pasta chamada finance. Você não precisará mais do arquivo ZIP, então você pode executar
rm finance.zip
e responder com "y" seguido de Enter no prompt para remover o arquivo ZIP que você baixou.
Agora digite
cd finance
seguido de Enter para ir para (por ex., abrir) esse diretório. Seu prompt agora deve se parecer com o abaixo.
finance/ $
Execute ls por si só e você verá alguns arquivos e pastas:
app.py finance.db helpers.py requirements.txt static/ templates/
Se você tiver algum problema, siga estes mesmos passos novamente e veja se consegue determinar onde errou!
Execução
Inicie o servidor web integrado ao Flask (dentro de finance/):
$ flask run
Visite a URL exibida por flask para ver o código de distribuição em ação. Você não conseguirá fazer login ou se registrar por enquanto!
Dentro de finance/, execute sqlite3 finance.db para abrir finance.db com o sqlite3. Se você executar .schema no prompt do SQLite, observe como finance.db vem com uma tabela chamada users (usuários). Dê uma olhada em sua estrutura (por ex., esquema). Observe como, por padrão, os novos usuários receberão US$ 10.000 em dinheiro. Mas se você executar SELECT * FROM users;, não haverá (ainda!) usuários (por ex., linhas) lá para navegar.
Outra forma de visualizar finance.db é com um programa chamado phpLiteAdmin. Clique em finance.db no navegador de arquivos do seu espaço de código e, em seguida, clique no link mostrado abaixo do texto "Please visit the following link to authorize GitHub Preview". Você deverá ver informações sobre o próprio banco de dados, bem como uma tabela, users, assim como viu no prompt sqlite3 com .schema.
Entendimento
app.py
Abra app.py. No início do arquivo há várias importações, entre elas o módulo SQL da CS50 e algumas funções auxiliares. Mais sobre elas em breve.
Depois de configurar o Flask, observe como esse arquivo desabilita o cache das respostas (desde que você esteja no modo de depuração, o que é ativado por padrão no seu código no Code50), para que você não faça uma alteração em algum arquivo e o seu navegador não perceba. Observe em seguida como ele configura o Jinja com um "filtro" personalizado, usd, uma função (definida em helpers.py) que facilitará a formatação dos valores como dólares americanos (USD). Ele configura ainda o Flask para armazenar sessões no sistema de arquivos local (ou seja, disco) em vez de armazená-las dentro de cookies (assinalados digitalmente), que é o padrão do Flask. O arquivo então configura o módulo SQL da CS50 para usar o finance.db.
Depois disso, há várias rotas, das quais somente duas estão totalmente implementadas: login e logout. Leia a implementação de login primeiro. Observe como ele usa db.execute (da biblioteca da CS50) para consultar finance.db. E observe como ele usa check_password_hash para comparar hashes das senhas dos usuários. Observe também como login "lembra" que um usuário está conectado armazenando seu user_id, um INTEIRO, em session. Dessa forma, qualquer uma das rotas desse arquivo pode verificar qual usuário, se houver, está conectado. Finalmente, observe como, depois que o usuário tiver conectado com sucesso, login redirecionará para "/", levando o usuário para sua página inicial. Enquanto isso, observe comologoutsimplesmente limpasession`, efetivamente desconectando o usuário.
Observe como a maioria das rotas são "decoradas" com @login_required (uma função também definida em helpers.py). Esse decorador garante que, se um usuário tentar visitar qualquer uma dessas rotas, ele será primeiro redirecionado para login para se conectar.
Observe também como a maioria das rotas suporta GET e POST. No entanto, a maioria delas (por enquanto!) simplesmente retorna um "pedido de desculpas", já que ainda não foram implementadas.
helpers.py
Em seguida, dê uma olhada em helpers.py. Ah, aí está a implementação de apology. Observe como ela acaba renderizando um template, apology.html. Por acaso ela também define internamente outra função, escape, que ela simplesmente usa para substituir caracteres especiais em pedidos de desculpas. Ao definir escape dentro de apology, nós escopaamos a primeira apenas para a segunda; nenhuma outra função conseguirá (ou precisará) chamá-la.
O próximo no arquivo é login_required. Não se preocupe se ela for um pouco críptica, mas se você já se perguntou como uma função pode retornar outra função, aqui está um exemplo!
Depois dela vem lookup, uma função que, dado um símbolo (ex.: NFLX), retorna uma cotação de ações para uma empresa na forma de um dict com duas chaves: price, cujo valor é um float; e symbol, cujo valor é uma str, uma versão padronizada (em caixa alta) do símbolo de uma ação, independentemente de como aquele símbolo foi escrito em caixa alta ou baixa quando passado para lookup.
Observação. Se você começou esse problema em 2023, observe que lookup não retorna mais uma chave de name, portanto, remova-a de qualquer consulta que a espere. Nenhum nome precisa ser exibido em nenhuma página.
O último no arquivo é usd, uma função curta que simplesmente formata um float como USD (ex.: 1234.56 é formatado como $1,234.56).
requirements.txt
Agora dê uma olhada rápida em requirements.txt. Esse arquivo simplesmente prescreve os pacotes dos quais esse aplicativo dependerá.
static/
Dê uma olhada também em static/, dentro do qual está styles.css. É lá que fica parte do CSS inicial. Fique à vontade para alterá-lo como quiser.
templates/
Agora olhe em templates/. Em login.html há, essencialmente, um formulário HTML, estilizado com Bootstrap. Enquanto isso, em apology.html, há um template para um pedido de desculpas. Lembre-se de que apology em helpers.py recebeu dois argumentos: message, que foi passado para render_template como o valor de bottom, e, opcionalmente, code, que foi passado para render_template como o valor de top. Observe em apology.html como esses valores são usados no final! E aqui está o motivo 0:-)
Por último, vem layout.html. Ele é um pouco maior do que o normal, mas isso ocorre principalmente porque ele vem com uma "navbar" (barra de navegação) sofisticada e otimizada para dispositivos móveis, também baseada no Bootstrap. Observe como ele define um bloco, main, dentro do qual os templates (incluindo apology.html e login.html) devem ir. Ele também inclui suporte para o message flashing do Flask, para que você possa transmitir mensagens de uma rota para outra para que o usuário as veja.
Especificação
register
Conclua a implementação de register de forma que permita que um usuário se cadastre para uma conta por meio de um formulário.
- Exija que o usuário insira um nome de usuário, implementado como um campo de texto cujo
nameéusername. Renderize um pedido de desculpas se a entrada do usuário estiver em branco ou se o nome de usuário já existir.- Observe que
cs50.SQL.executelançará uma exceçãoValueErrorse você tentarINSERTum nome de usuário duplicado porque criamos umUNIQUE INDEXemusers.username. Portanto, certifique-se de usartryeexceptpara determinar se o nome de usuário já existe.
- Observe que
- Exija que o usuário insira uma senha, implementada como um campo de texto cujo
nameépassword, e em seguida a mesma senha novamente, implementada como um campo de texto cujonameéconfirmation. Renderize um pedido de desculpas se uma das entradas estiver em branco ou se as senhas não corresponderem. - Envie a entrada do usuário via
POSTpara/register. INSERTo novo usuário emusers, armazenando um hash da senha do usuário, não a senha em si. Criptografe a senha do usuário comgenerate_password_hashProvavelmente você vai querer criar um novo template (ex.:register.html) que seja bem semelhante alogin.html.
Depois de implementar register corretamente, você deve conseguir se cadastrar para obter uma conta e conectar-se (já que login e logout já funcionam)! E você deve conseguir ver suas linhas pelo phpLiteAdmin ou sqlite3.
quote
Conclua a implementação de quote de forma que permita que um usuário procure o preço atual de uma ação.
- Exija que o usuário insira o símbolo de uma ação, implementado como um campo de texto cujo
nameésymbol. - Envie a entrada do usuário via
POSTpara/quote. - Provavelmente você vai querer criar dois novos templates (ex.:
quote.htmlequoted.html). Quando um usuário visita/quotevia GET, renderize um desses templates, dentro do qual deve haver um formulário HTML que envia para/quotevia POST. Em resposta a um POST,quotepode renderizar esse segundo template, incorporando a ele um ou mais valores delookup.
buy
Complete a implementação de buy de forma que permita que o usuário compre ações.
- Exija que o usuário informe um símbolo de ação, implementado como um campo de texto cujo
namesejasymbol. Renderize um pedido de desculpas se a entrada estiver em branco ou o símbolo não existir (de acordo com o valor de retorno delookup). - Exija que o usuário insira um número de ações, implementado como um campo de texto cujo
namesejashares. Renderize um pedido de desculpas se a entrada não for um número inteiro positivo. - Envie a entrada do usuário via
POSTpara/buy. - Após a conclusão, redirecione o usuário para a página inicial.
- Provavelmente, você desejará chamar
lookuppara consultar o preço atual de uma ação. - Provavelmente, você desejará
SELECIONARquanto dinheiro o usuário tem atualmente emusers. - Adicione uma ou mais tabelas novas ao
finance.dbpara manter o controle da compra. Armazene informações suficientes para saber quem comprou o quê, a que preço e quando.- Use tipos SQLite apropriados.
- Defina índices
UNIQUEem quaisquer campos que devem ser exclusivos. - Defina índices (não
UNIQUE) em quaisquer campos pelos quais você pesquisará (como por meio deSELECTcomWHERE).
- Renderize um pedido de desculpas, sem concluir a compra, se o usuário não puder comprar o número de ações ao preço atual.
- Você não precisa se preocupar com condições de corrida (ou usar transações).
Depois de implementar buy corretamente, você poderá ver as compras dos usuários em suas novas tabelas no phpLiteAdmin ou sqlite3.
index
Complete a implementação de index de forma que exiba uma tabela HTML resumindo, para o usuário atualmente conectado, quais ações o usuário possui, o número de ações possuídas, o preço atual de cada ação e o valor total de cada participação (ou seja, ações vezes preço). Exiba também o saldo atual de caixa do usuário e o total geral (ou seja, valor total das ações mais o dinheiro).
- Provavelmente, você desejará executar vários
SELECTs. Dependendo de como você implementar sua(s) tabela(s), você poderá encontrar GROUP BY HAVING SUM e/ou WHERE de seu interesse. - Provavelmente, você desejará chamar
lookuppara cada ação.
sell
Complete a implementação de sell para que permita que o usuário venda ações de uma ação (que ele possui).
- Requeira que o usuário insira um símbolo de ação, implementado como um menu
selectcujonamesejasymbol. Renderize um pedido de desculpas se o usuário não selecionar uma ação ou se (de alguma forma, depois de enviar) o usuário não possuir nenhuma ação daquela ação. - Exija que o usuário insira um número de ações, implementado como um campo de texto cujo
namesejashares. Renderize um pedido de desculpas se a entrada não for um número inteiro positivo ou se o usuário não possuir tantas ações da ação. - Envie a entrada do usuário via
POSTpara/sell. - Após a conclusão, redirecione o usuário para a página inicial.
- Você não precisa se preocupar com condições de corrida (ou usar transações).
history
Complete a implementação de history de forma que exiba uma tabela HTML resumindo todas as transações de um usuário, listando linha por linha cada compra e venda.
- Para cada linha, deixe claro se uma ação foi comprada ou vendida e inclua o símbolo da ação, o preço (de compra ou venda), o número de ações compradas ou vendidas e a data e hora em que a transação ocorreu.
- Talvez seja necessário alterar a tabela criada para
buyou complementá-la com uma tabela adicional. Tente minimizar redundâncias.
Toque pessoal
Implemente pelo menos um toque pessoal de sua escolha:
- Permita que os usuários alterem suas senhas.
- Permita que os usuários adicionem dinheiro extra à sua conta.
- Permita que os usuários comprem mais ações ou vendam ações que já possuem no próprio
index, sem precisar digitar os símbolos das ações manualmente. - Implemente algum outro recurso de escopo comparável.
Passo a passo
Observe que Brian menciona que lookup retornará o nome da ação. Conforme acima, agora ele retorna apenas o preço e o símbolo.
Testes
Certifique-se de testar seu aplicativo da web manualmente, como
- registrando um novo usuário e verificando se a página do portfólio é carregada com as informações corretas,
- solicitando uma cotação usando um símbolo de ação válido,
- comprando uma ação várias vezes, verificando se o portfólio exibe totais corretos,
- vendendo todas ou algumas ações, verificando o portfólio novamente e
- verificando se a página do histórico mostra todas as transações do usuário conectado.
Também teste alguns usos inesperados, como
- inserindo strings alfabéticas em formulários quando apenas números são esperados,
- inserindo zero ou números negativos em formulários quando apenas números positivos são esperados,
- inserindo valores de pontos flutuantes em formulários quando apenas inteiros são esperados,
- tentando gastar mais dinheiro do que o usuário tem,
- tentando vender mais ações do que o usuário tem,
- inserindo um símbolo de ação inválido e
- incluindo caracteres potencialmente perigosos, como
'e;em consultas SQL.
Você também pode verificar a validade do HTML clicando no botão I ♥ VALIDATOR no rodapé de cada uma das suas páginas, que enviará seu HTML para validator.w3.org.
Após a satisfação, para testar seu código com check50, execute o abaixo.
check50 cs50/problems/2024/x/finance
Esteja ciente de que check50 testará todo o seu programa como um todo. Se você o executar antes de concluir todas as funções necessárias, ele poderá relatar erros em funções que são realmente corretas, mas dependem de outras funções.
Estilo
style50 app.py
Solução do corpo docente
Você é bem-vindo para estilizar seu próprio aplicativo de forma diferente, mas é assim que a solução do corpo docente se parece!
Sinta-se à vontade para se registrar para obter uma conta e brincar com o app. Não use uma senha que você usa em outros sites.
É razoável olhar para o HTML e CSS do corpo docente.
Dicas
- Para formatar um valor como um valor em dólar dos EUA (com centavos listados em duas casas decimais), você pode usar o filtro
usdem seus modelos Jinja (imprimindo valores como{{ value | usd }}em vez de{{ value }}. - Dentro de
cs50.SQLhá um métodoexecutecujo primeiro argumento deve ser umastrde SQL. Se essastrcontiver parâmetros de ponto de interrogação aos quais valores devem ser vinculados, esses valores podem ser fornecidos como parâmetros nomeados adicionais paraexecute. Veja a implementação deloginpara um exemplo. O valor de retorno deexecuteé o seguinte:- Se
strfor umSELECT, entãoexecuteretornará umalistde zero ou mais objetosdict, dentro dos quais estão chaves e valores representando campos de uma tabela e células, respectivamente. - Se
strfor umINSERT, e a tabela na qual os dados foram inseridos contiver umaPRIMARY KEYde autoincremento, entãoexecuteretornará o valor da chave primária da linha recém-inserida. - Se
strfor umDELETEou umUPDATE, entãoexecuteretornará o número de linhas excluídas ou atualizadas porstr.
- Se
- Lembre-se que
cs50.SQLregistrará em sua janela de terminal todas as consultas que você executar viaexecute(para que você possa confirmar se elas estão conforme o esperado). - Certifique-se de usar parâmetros vinculados a ponto de interrogação (ou seja, um paramstyle de
named) ao chamar o métodoexecutedo CS50, à laWHERE ?. Não use f-strings,formatou+(ou seja, concatenação), para que você não corra o risco de um ataque de injeção de SQL. - Se (e somente se) já estiver confortável com SQL, você pode usar SQLAlchemy Core ou Flask-SQLAlchemy (ou seja, SQLAlchemy ORM) em vez de
cs50.SQL. - Você pode adicionar arquivos estáticos adicionais em
static/. - Provavelmente você desejará consultar a documentação do Jinja ao implementar seus modelos.
- É razoável pedir a outras pessoas para testar (e tentar acionar erros em) seu site.
- Você pode alterar a estética dos sites, como via
- Você pode achar a documentação do Flask e a documentação do Jinja úteis!
FAQs
ImportError: No module named ‘application’
Por padrão, flask procura um arquivo chamado app.py em seu diretório de trabalho atual (porque configuramos o valor de FLASK_APP, uma variável de ambiente, para ser app.py). Se ver este erro, provavelmente você executou flask no diretório errado!
OSError: [Errno 98] Address already in use
Se, ao executar flask, você vir este erro, provavelmente (ainda) tem flask sendo executado em outra aba. Certifique-se de encerrar aquele outro processo, como com ctrl-c, antes de iniciar flask novamente. Se você não tiver nenhuma outra aba, execute fuser -k 8080/tcp para encerrar quaisquer processos que (ainda) estejam escutando na porta TCP 8080.
Como enviar
Em seu terminal, execute o abaixo para enviar seu trabalho.
submit50 cs50/problems/2024/x/finance
Por que minha submissão passa pelo check50, mas mostra "Sem resultados" em meu Gradebook após executar submit50?
Em alguns casos, submit50 pode não avaliar a atribuição devido a (1) formatação inconsistente em seu arquivo app.py e/ou (2) arquivos adicionais desnecessários sendo enviados com o conjunto de problemas. Para corrigir esses problemas, execute black app.py na pasta finance. Resolva quaisquer problemas que sejam revelados. Em seguida, examine o conteúdo de sua pasta finance. Exclua arquivos estranhos, como sessões do flask ou outros arquivos que não fazem parte de sua implementação do conjunto de problemas. Além disso, execute check50 novamente para garantir que sua submissão ainda funcione. Por fim, execute o comando submit50 acima novamente. Seu resultado aparecerá em seu Gradebook em alguns minutos.
Observe que se houver uma pontuação numérica ao lado de sua submissão de finanças na área submissions do seu Gradebook, o procedimento discutido acima não se aplica a você. Provavelmente, você não atendeu totalmente aos requisitos do conjunto de problemas e deve confiar no check50 para obter pistas sobre que trabalho resta.