This commit is contained in:
Peter Wood
2025-11-10 06:38:04 -05:00
17 changed files with 2139 additions and 6 deletions

9
margotwood/Caddyfile Normal file
View File

@@ -0,0 +1,9 @@
{
auto_https off
}
:80 {
root * /usr/share/caddy
encode gzip
file_server
}

View File

@@ -0,0 +1,16 @@
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- 8083:80
- 8243:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./:/usr/share/caddy
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data: null
caddy_config: null
networks: {}

View File

@@ -0,0 +1,430 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toddler Math Flash Cards - Addition Fun!</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
color: #333;
padding: 10px 0;
}
.app-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: center;
max-width: 500px;
width: 95%;
margin: 10px auto;
}
.title {
color: #4a5568;
font-size: 2.2em;
margin-bottom: 15px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.flashcard {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 15px;
padding: 25px;
margin: 15px 0;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.flashcard:hover {
transform: scale(1.05);
}
.equation {
font-size: 3.5em;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
margin-bottom: 10px;
}
.answer-section {
margin: 20px 0;
}
.answer-input {
font-size: 2.5em;
padding: 12px 15px;
border: 4px solid #667eea;
border-radius: 15px;
text-align: center;
width: 130px;
margin: 0 auto 15px auto;
display: block;
font-family: 'Nunito', sans-serif;
}
.answer-input:focus {
outline: none;
border-color: #f093fb;
box-shadow: 0 0 20px rgba(240, 147, 251, 0.3);
}
.buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.btn {
font-size: 1.5em;
padding: 15px 30px;
border: none;
border-radius: 50px;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn-check {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-next {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.feedback {
font-size: 2.5em;
font-weight: bold;
margin: 20px 0;
padding: 20px;
border-radius: 15px;
transition: all 0.5s ease;
}
.correct {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
color: #2d5a27;
}
.incorrect {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
color: #8b4513;
}
.score {
background: rgba(255, 255, 255, 0.9);
padding: 8px 15px;
border-radius: 20px;
font-size: 1em;
font-weight: bold;
color: #4a5568;
margin-bottom: 15px;
display: inline-block;
}
.celebration {
font-size: 3em;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.number-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 15px 0;
}
.number-buttons .number-btn:last-child {
grid-column: span 2;
}
.number-btn {
font-size: 1.8em;
padding: 12px;
border: 3px solid #667eea;
border-radius: 12px;
background: white;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-weight: bold;
color: #667eea;
transition: all 0.2s ease;
min-height: 50px;
}
.number-btn:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
@media (max-width: 600px) {
.app-container {
padding: 15px;
margin: 5px auto;
}
.title {
font-size: 1.8em;
margin-bottom: 10px;
}
.equation {
font-size: 2.8em;
}
.answer-input {
font-size: 2.2em;
width: 110px;
}
.number-btn {
font-size: 1.6em;
padding: 10px;
min-height: 45px;
}
.btn {
font-size: 1.3em;
padding: 12px 25px;
}
}
@media (max-height: 700px) {
.flashcard {
padding: 20px;
margin: 10px 0;
}
.equation {
font-size: 3em;
}
.number-btn {
font-size: 1.6em;
padding: 8px;
min-height: 40px;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="score" id="score">Score: 0/0</div>
<h1 class="title">🎓 Addition Practice</h1>
<div class="flashcard">
<div class="equation" id="equation">5 + 3 = ?</div>
</div>
<div class="answer-section">
<input type="number" class="answer-input" id="answerInput" placeholder="?" min="0" max="20">
<div class="number-buttons" id="numberButtons">
<button class="number-btn" onclick="inputNumber(0)">0</button>
<button class="number-btn" onclick="inputNumber(1)">1</button>
<button class="number-btn" onclick="inputNumber(2)">2</button>
<button class="number-btn" onclick="inputNumber(3)">3</button>
<button class="number-btn" onclick="inputNumber(4)">4</button>
<button class="number-btn" onclick="inputNumber(5)">5</button>
<button class="number-btn" onclick="inputNumber(6)">6</button>
<button class="number-btn" onclick="inputNumber(7)">7</button>
<button class="number-btn" onclick="inputNumber(8)">8</button>
<button class="number-btn" onclick="inputNumber(9)">9</button>
<button class="number-btn" onclick="inputNumber(10)">10</button>
<button class="number-btn" onclick="clearAnswer()"
style="background: #ff6b6b; color: white;">Clear</button>
</div>
</div>
<div class="feedback" id="feedback" style="display: none;"></div>
<div class="buttons">
<button class="btn btn-check" onclick="checkAnswer()">Check Answer! 🎯</button>
<button class="btn btn-next" onclick="nextCard()" style="display: none;" id="nextBtn">Next Card! ➡️</button>
</div>
</div>
<script>
let currentNum1, currentNum2, correctAnswer;
let totalQuestions = 0;
let correctAnswers = 0;
function generateNewCard () {
// Generate two random numbers from 0 to 10
currentNum1 = Math.floor(Math.random() * 11);
currentNum2 = Math.floor(Math.random() * 11);
correctAnswer = currentNum1 + currentNum2;
// Update the equation display
document.getElementById('equation').textContent = `${currentNum1} + ${currentNum2} = ?`;
// Reset the input and feedback
document.getElementById('answerInput').value = '';
document.getElementById('feedback').style.display = 'none';
document.getElementById('nextBtn').style.display = 'none';
// Don't focus on input to prevent keyboard from showing on mobile
}
function inputNumber (num) {
const input = document.getElementById('answerInput');
const currentValue = input.value;
if (currentValue === '' || currentValue === '0') {
input.value = num;
} else if (currentValue.length < 2) { // Limit to 2 digits max
input.value = currentValue + num;
}
}
function clearAnswer () {
document.getElementById('answerInput').value = '';
}
function checkAnswer () {
const userAnswer = parseInt(document.getElementById('answerInput').value);
const feedbackEl = document.getElementById('feedback');
if (isNaN(userAnswer)) {
alert('Please enter a number! 😊');
return;
}
totalQuestions++;
if (userAnswer === correctAnswer) {
correctAnswers++;
feedbackEl.innerHTML = `<div class="celebration">🎉</div>Awesome! ${currentNum1} + ${currentNum2} = ${correctAnswer}`;
feedbackEl.className = 'feedback correct';
// Play success sound (if browser supports it)
playSound('success');
} else {
feedbackEl.innerHTML = `<div>Try again! 💪</div>${currentNum1} + ${currentNum2} = ${correctAnswer}`;
feedbackEl.className = 'feedback incorrect';
// Play try again sound (if browser supports it)
playSound('tryAgain');
}
feedbackEl.style.display = 'block';
document.getElementById('nextBtn').style.display = 'inline-block';
updateScore();
}
function nextCard () {
generateNewCard();
}
function updateScore () {
const percentage = totalQuestions > 0 ? Math.round((correctAnswers / totalQuestions) * 100) : 0;
document.getElementById('score').textContent = `Score: ${correctAnswers}/${totalQuestions} (${percentage}%)`;
}
function playSound (type) {
// Create audio context for simple beep sounds
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
if (type === 'success') {
// Happy ascending notes
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
} else {
// Gentle encouraging tone
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4
}
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
} catch (e) {
// Audio not supported, silent fail
}
}
// Keyboard support
document.getElementById('answerInput').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
if (document.getElementById('nextBtn').style.display === 'none') {
checkAnswer();
} else {
nextCard();
}
}
});
// Initialize the first card when page loads
window.onload = function () {
generateNewCard();
updateScore();
};
// Prevent negative numbers
document.getElementById('answerInput').addEventListener('input', function (e) {
if (e.target.value < 0) {
e.target.value = 0;
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,431 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toddler Math Flash Cards - Subtraction Fun!</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
color: #333;
padding: 10px 0;
}
.app-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: center;
max-width: 500px;
width: 95%;
margin: 10px auto;
}
.title {
color: #4a5568;
font-size: 2.2em;
margin-bottom: 15px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.flashcard {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
border-radius: 15px;
padding: 25px;
margin: 15px 0;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.flashcard:hover {
transform: scale(1.05);
}
.equation {
font-size: 3.5em;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
margin-bottom: 10px;
}
.answer-section {
margin: 20px 0;
}
.answer-input {
font-size: 2.5em;
padding: 12px 15px;
border: 4px solid #667eea;
border-radius: 15px;
text-align: center;
width: 130px;
margin: 0 auto 15px auto;
display: block;
font-family: 'Nunito', sans-serif;
}
.answer-input:focus {
outline: none;
border-color: #43e97b;
box-shadow: 0 0 20px rgba(67, 233, 123, 0.3);
}
.buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.btn {
font-size: 1.5em;
padding: 15px 30px;
border: none;
border-radius: 50px;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn-check {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-next {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.feedback {
font-size: 2.5em;
font-weight: bold;
margin: 20px 0;
padding: 20px;
border-radius: 15px;
transition: all 0.5s ease;
}
.correct {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
color: #2d5a27;
}
.incorrect {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
color: #8b4513;
}
.score {
background: rgba(255, 255, 255, 0.9);
padding: 8px 15px;
border-radius: 20px;
font-size: 1em;
font-weight: bold;
color: #4a5568;
margin-bottom: 15px;
display: inline-block;
}
.celebration {
font-size: 3em;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.number-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 15px 0;
}
.number-buttons .number-btn:last-child {
grid-column: span 2;
}
.number-btn {
font-size: 1.8em;
padding: 12px;
border: 3px solid #667eea;
border-radius: 12px;
background: white;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-weight: bold;
color: #667eea;
transition: all 0.2s ease;
min-height: 50px;
}
.number-btn:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
@media (max-width: 600px) {
.app-container {
padding: 15px;
margin: 5px auto;
}
.title {
font-size: 1.8em;
margin-bottom: 10px;
}
.equation {
font-size: 2.8em;
}
.answer-input {
font-size: 2.2em;
width: 110px;
}
.number-btn {
font-size: 1.6em;
padding: 10px;
min-height: 45px;
}
.btn {
font-size: 1.3em;
padding: 12px 25px;
}
}
@media (max-height: 700px) {
.flashcard {
padding: 20px;
margin: 10px 0;
}
.equation {
font-size: 3em;
}
.number-btn {
font-size: 1.6em;
padding: 8px;
min-height: 40px;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="score" id="score">Score: 0/0</div>
<h1 class="title">🎓 Subtraction Practice</h1>
<div class="flashcard">
<div class="equation" id="equation">5 + 3 = ?</div>
</div>
<div class="answer-section">
<input type="number" class="answer-input" id="answerInput" placeholder="?" min="0" max="10">
<div class="number-buttons" id="numberButtons">
<button class="number-btn" onclick="inputNumber(0)">0</button>
<button class="number-btn" onclick="inputNumber(1)">1</button>
<button class="number-btn" onclick="inputNumber(2)">2</button>
<button class="number-btn" onclick="inputNumber(3)">3</button>
<button class="number-btn" onclick="inputNumber(4)">4</button>
<button class="number-btn" onclick="inputNumber(5)">5</button>
<button class="number-btn" onclick="inputNumber(6)">6</button>
<button class="number-btn" onclick="inputNumber(7)">7</button>
<button class="number-btn" onclick="inputNumber(8)">8</button>
<button class="number-btn" onclick="inputNumber(9)">9</button>
<button class="number-btn" onclick="inputNumber(10)">10</button>
<button class="number-btn" onclick="clearAnswer()"
style="background: #ff6b6b; color: white;">Clear</button>
</div>
</div>
<div class="feedback" id="feedback" style="display: none;"></div>
<div class="buttons">
<button class="btn btn-check" onclick="checkAnswer()">Check Answer! 🎯</button>
<button class="btn btn-next" onclick="nextCard()" style="display: none;" id="nextBtn">Next Card! ➡️</button>
</div>
</div>
<script>
let currentNum1, currentNum2, correctAnswer;
let totalQuestions = 0;
let correctAnswers = 0;
function generateNewCard () {
// Generate two random numbers from 0 to 10, ensuring positive result
currentNum1 = Math.floor(Math.random() * 11); // 0-10
currentNum2 = Math.floor(Math.random() * (currentNum1 + 1)); // 0 to currentNum1 (ensures positive result)
correctAnswer = currentNum1 - currentNum2;
// Update the equation display
document.getElementById('equation').textContent = `${currentNum1} - ${currentNum2} = ?`;
// Reset the input and feedback
document.getElementById('answerInput').value = '';
document.getElementById('feedback').style.display = 'none';
document.getElementById('nextBtn').style.display = 'none';
// Focus on input for keyboard users
document.getElementById('answerInput').focus();
}
function inputNumber (num) {
const input = document.getElementById('answerInput');
const currentValue = input.value;
if (currentValue === '' || currentValue === '0') {
input.value = num;
} else if (currentValue.length < 2) { // Limit to 2 digits max
input.value = currentValue + num;
}
}
function clearAnswer () {
document.getElementById('answerInput').value = '';
}
function checkAnswer () {
const userAnswer = parseInt(document.getElementById('answerInput').value);
const feedbackEl = document.getElementById('feedback');
if (isNaN(userAnswer)) {
alert('Please enter a number! 😊');
return;
}
totalQuestions++;
if (userAnswer === correctAnswer) {
correctAnswers++;
feedbackEl.innerHTML = `<div class="celebration">🎉</div>Awesome! ${currentNum1} - ${currentNum2} = ${correctAnswer}`;
feedbackEl.className = 'feedback correct';
// Play success sound (if browser supports it)
playSound('success');
} else {
feedbackEl.innerHTML = `<div>Try again! 💪</div>${currentNum1} - ${currentNum2} = ${correctAnswer}`;
feedbackEl.className = 'feedback incorrect';
// Play try again sound (if browser supports it)
playSound('tryAgain');
}
feedbackEl.style.display = 'block';
document.getElementById('nextBtn').style.display = 'inline-block';
updateScore();
}
function nextCard () {
generateNewCard();
}
function updateScore () {
const percentage = totalQuestions > 0 ? Math.round((correctAnswers / totalQuestions) * 100) : 0;
document.getElementById('score').textContent = `Score: ${correctAnswers}/${totalQuestions} (${percentage}%)`;
}
function playSound (type) {
// Create audio context for simple beep sounds
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
if (type === 'success') {
// Happy ascending notes
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
} else {
// Gentle encouraging tone
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4
}
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
} catch (e) {
// Audio not supported, silent fail
}
}
// Keyboard support
document.getElementById('answerInput').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
if (document.getElementById('nextBtn').style.display === 'none') {
checkAnswer();
} else {
nextCard();
}
}
});
// Initialize the first card when page loads
window.onload = function () {
generateNewCard();
updateScore();
};
// Prevent negative numbers
document.getElementById('answerInput').addEventListener('input', function (e) {
if (e.target.value < 0) {
e.target.value = 0;
}
});
</script>
</body>
</html>

222
margotwood/index.html Normal file
View File

@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Learning Fun for Toddlers!</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
color: #333;
}
.container {
background: white;
border-radius: 30px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
padding: 40px 30px;
text-align: center;
max-width: 600px;
width: 100%;
margin: 20px;
}
.title {
color: #4a5568;
font-size: 3em;
margin-bottom: 20px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
line-height: 1.2;
}
.subtitle {
color: #667eea;
font-size: 1.5em;
margin-bottom: 40px;
font-weight: normal;
}
.app-link {
display: block;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
text-decoration: none;
font-size: 2.5em;
font-weight: bold;
padding: 40px 30px;
border-radius: 25px;
margin: 30px 0;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 10px;
}
.app-link:hover,
.app-link:active {
transform: scale(1.05);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
background: linear-gradient(135deg, #f5576c 0%, #f093fb 100%);
}
.app-link:active {
transform: scale(0.98);
}
.emoji {
font-size: 1.5em;
margin-bottom: 10px;
}
.link-text {
font-size: 0.8em;
line-height: 1.2;
}
.footer {
margin-top: 30px;
color: #667eea;
font-size: 1.2em;
}
/* Mobile-first responsive design */
@media (max-width: 480px) {
.container {
padding: 30px 20px;
margin: 10px;
}
.title {
font-size: 2.5em;
}
.subtitle {
font-size: 1.3em;
}
.app-link {
font-size: 2.2em;
padding: 35px 25px;
min-height: 100px;
}
}
@media (max-width: 360px) {
.title {
font-size: 2.2em;
}
.app-link {
font-size: 2em;
padding: 30px 20px;
}
}
/* Tablet optimizations */
@media (min-width: 481px) and (max-width: 768px) {
.container {
padding: 50px 40px;
}
.title {
font-size: 3.5em;
}
.app-link {
font-size: 3em;
padding: 50px 40px;
min-height: 140px;
}
}
/* Large tablet/desktop */
@media (min-width: 769px) {
.container {
padding: 60px 50px;
}
.title {
font-size: 4em;
}
.app-link {
font-size: 3.5em;
padding: 60px 50px;
min-height: 160px;
}
}
/* Touch-friendly adjustments */
@media (pointer: coarse) {
.app-link {
min-height: 120px;
padding: 40px;
}
}
/* Subtraction-specific styling */
.app-link.subtraction {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
}
.app-link.subtraction:hover,
.app-link.subtraction:active {
background: linear-gradient(135deg, #38f9d7 0%, #43e97b 100%);
}
/* Skip counting-specific styling */
.app-link.skip-counting {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
}
.app-link.skip-counting:hover,
.app-link.skip-counting:active {
background: linear-gradient(135deg, #fecfef 0%, #ff9a9e 100%);
}
</style>
</head>
<body>
<div class="container">
<h1 class="title">🎓 Learning Time! 🎓</h1>
<p class="subtitle">Tap to start learning math!</p>
<a href="flashcards/addition.html" class="app-link">
<div class="emoji"></div>
<div class="link-text">Addition</div>
</a>
<a href="flashcards/subtraction.html" class="app-link subtraction">
<div class="emoji"></div>
<div class="link-text">Subtraction</div>
</a>
<a href="skipcount/twos.html" class="app-link skip-counting">
<div class="emoji">⏭️ 2</div>
<div class="link-text">Skip Counting by 2s</div>
</a>
<div class="footer">
<p>Fun math games for little learners! 🌟</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toddler Math Flash Cards - Skip Counting by 2s!</title>
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Nunito', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start;
color: #333;
padding: 10px 0;
}
.app-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
padding: 20px;
text-align: center;
max-width: 500px;
width: 95%;
margin: 10px auto;
}
.title {
color: #4a5568;
font-size: 2.2em;
margin-bottom: 15px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.flashcard {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
border-radius: 15px;
padding: 25px;
margin: 15px 0;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.flashcard:hover {
transform: scale(1.05);
}
.equation {
font-size: 3.5em;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
margin-bottom: 10px;
}
.sequence-display {
font-size: 2.2em;
font-weight: bold;
color: white;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
margin-bottom: 15px;
line-height: 1.2;
}
.answer-section {
margin: 20px 0;
}
.answer-input {
font-size: 2.5em;
padding: 12px 15px;
border: 4px solid #667eea;
border-radius: 15px;
text-align: center;
width: 130px;
margin: 0 auto 15px auto;
display: block;
font-family: 'Nunito', sans-serif;
}
.answer-input:focus {
outline: none;
border-color: #ff9a9e;
box-shadow: 0 0 20px rgba(255, 154, 158, 0.3);
}
.buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.btn {
font-size: 1.5em;
padding: 15px 30px;
border: none;
border-radius: 50px;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-weight: bold;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn-check {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-next {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.feedback {
font-size: 2.5em;
font-weight: bold;
margin: 20px 0;
padding: 20px;
border-radius: 15px;
transition: all 0.5s ease;
}
.correct {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
color: #2d5a27;
}
.incorrect {
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
color: #8b4513;
}
.score {
background: rgba(255, 255, 255, 0.9);
padding: 8px 15px;
border-radius: 20px;
font-size: 1em;
font-weight: bold;
color: #4a5568;
margin-bottom: 15px;
display: inline-block;
}
.celebration {
font-size: 3em;
animation: bounce 1s infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.number-buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 15px 0;
}
.number-buttons .number-btn:last-child {
grid-column: span 2;
}
.number-btn {
font-size: 1.8em;
padding: 12px;
border: 3px solid #667eea;
border-radius: 12px;
background: white;
cursor: pointer;
font-family: 'Nunito', sans-serif;
font-weight: bold;
color: #667eea;
transition: all 0.2s ease;
min-height: 50px;
}
.number-btn:hover {
background: #667eea;
color: white;
transform: scale(1.1);
}
.hint {
font-size: 1.2em;
color: #666;
margin-bottom: 10px;
font-style: italic;
}
@media (max-width: 600px) {
.app-container {
padding: 15px;
margin: 5px auto;
}
.title {
font-size: 1.8em;
margin-bottom: 10px;
}
.equation {
font-size: 2.8em;
}
.sequence-display {
font-size: 1.8em;
}
.answer-input {
font-size: 2.2em;
width: 110px;
}
.number-btn {
font-size: 1.6em;
padding: 10px;
min-height: 45px;
}
.btn {
font-size: 1.3em;
padding: 12px 25px;
}
}
@media (max-height: 700px) {
.flashcard {
padding: 20px;
margin: 10px 0;
}
.equation {
font-size: 3em;
}
.sequence-display {
font-size: 2em;
}
.number-btn {
font-size: 1.6em;
padding: 8px;
min-height: 40px;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="score" id="score">Score: 0/0</div>
<h1 class="title">⏭️ Skip Counting by 2s</h1>
<div class="flashcard">
<div class="hint">What comes next?</div>
<div class="sequence-display" id="sequenceDisplay">2, 4, 6, ?</div>
</div>
<div class="answer-section">
<input type="number" class="answer-input" id="answerInput" placeholder="?" min="0" max="100">
<div class="number-buttons" id="numberButtons">
<button class="number-btn" onclick="inputNumber(0)">0</button>
<button class="number-btn" onclick="inputNumber(1)">1</button>
<button class="number-btn" onclick="inputNumber(2)">2</button>
<button class="number-btn" onclick="inputNumber(3)">3</button>
<button class="number-btn" onclick="inputNumber(4)">4</button>
<button class="number-btn" onclick="inputNumber(5)">5</button>
<button class="number-btn" onclick="inputNumber(6)">6</button>
<button class="number-btn" onclick="inputNumber(7)">7</button>
<button class="number-btn" onclick="inputNumber(8)">8</button>
<button class="number-btn" onclick="inputNumber(9)">9</button>
<button class="number-btn" onclick="inputNumber(10)">10</button>
<button class="number-btn" onclick="clearAnswer()"
style="background: #ff6b6b; color: white;">Clear</button>
</div>
</div>
<div class="feedback" id="feedback" style="display: none;"></div>
<div class="buttons">
<button class="btn btn-check" onclick="checkAnswer()">Check Answer! 🎯</button>
<button class="btn btn-next" onclick="nextCard()" style="display: none;" id="nextBtn">Next Card! ➡️</button>
</div>
</div>
<script>
let currentSequence = [];
let correctAnswer;
let totalQuestions = 0;
let correctAnswers = 0;
let sequenceLength = 3; // Start with 3 numbers shown, ask for 4th
function generateNewCard() {
// Generate a random starting point for skip counting by 2s
// Start with even numbers from 0 to 10, then continue the sequence
const startNumber = Math.floor(Math.random() * 6) * 2; // 0, 2, 4, 6, 8, 10
// Create sequence of specified length
currentSequence = [];
for (let i = 0; i < sequenceLength; i++) {
currentSequence.push(startNumber + (i * 2));
}
// The correct answer is the next number in the sequence
correctAnswer = startNumber + (sequenceLength * 2);
// Display the sequence with a question mark for the next number
const sequenceDisplay = currentSequence.join(', ') + ', ?';
document.getElementById('sequenceDisplay').textContent = sequenceDisplay;
// Reset the input and feedback
document.getElementById('answerInput').value = '';
document.getElementById('feedback').style.display = 'none';
document.getElementById('nextBtn').style.display = 'none';
// Don't focus on input to prevent keyboard from showing on mobile
}
function inputNumber(num) {
const input = document.getElementById('answerInput');
const currentValue = input.value;
if (currentValue === '' || currentValue === '0') {
input.value = num;
} else if (currentValue.length < 3) { // Limit to 3 digits max for larger numbers
input.value = currentValue + num;
}
}
function clearAnswer() {
document.getElementById('answerInput').value = '';
}
function checkAnswer() {
const userAnswer = parseInt(document.getElementById('answerInput').value);
const feedbackEl = document.getElementById('feedback');
if (isNaN(userAnswer)) {
alert('Please enter a number! 😊');
return;
}
totalQuestions++;
if (userAnswer === correctAnswer) {
correctAnswers++;
feedbackEl.innerHTML = `<div class="celebration">🎉</div>Perfect! The sequence is: ${currentSequence.join(', ')}, ${correctAnswer}`;
feedbackEl.className = 'feedback correct';
// Play success sound (if browser supports it)
playSound('success');
// Gradually increase difficulty by showing longer sequences
if (correctAnswers % 5 === 0 && sequenceLength < 5) {
sequenceLength++;
}
} else {
feedbackEl.innerHTML = `<div>Try again! 💪</div>The sequence is: ${currentSequence.join(', ')}, ${correctAnswer}`;
feedbackEl.className = 'feedback incorrect';
// Play try again sound (if browser supports it)
playSound('tryAgain');
}
feedbackEl.style.display = 'block';
document.getElementById('nextBtn').style.display = 'inline-block';
updateScore();
}
function nextCard() {
generateNewCard();
}
function updateScore() {
const percentage = totalQuestions > 0 ? Math.round((correctAnswers / totalQuestions) * 100) : 0;
document.getElementById('score').textContent = `Score: ${correctAnswers}/${totalQuestions} (${percentage}%)`;
}
function playSound(type) {
// Create audio context for simple beep sounds
try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
if (type === 'success') {
// Happy ascending notes
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
} else {
// Gentle encouraging tone
oscillator.frequency.setValueAtTime(440, audioContext.currentTime); // A4
}
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.1, audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
} catch (e) {
// Audio not supported, silent fail
}
}
// Keyboard support
document.getElementById('answerInput').addEventListener('keypress', function (e) {
if (e.key === 'Enter') {
if (document.getElementById('nextBtn').style.display === 'none') {
checkAnswer();
} else {
nextCard();
}
}
});
// Initialize the first card when page loads
window.onload = function () {
generateNewCard();
updateScore();
};
// Prevent negative numbers
document.getElementById('answerInput').addEventListener('input', function (e) {
if (e.target.value < 0) {
e.target.value = 0;
}
});
</script>
</body>
</html>

0
margotwood/style.css Normal file
View File

View File

@@ -21,6 +21,7 @@ services:
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_PRESHARED_KEY=${WIREGUARD_PRESHARED_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- UPDATER_PERIOD:24h
- TZ=America/New_York # Timezone for accurate log times
- SERVER_COUNTRIES=United States
restart: always
@@ -40,6 +41,9 @@ services:
- /data/usenet/incomplete-downloads:/incomplete-downloads
# network_mode: "service:gluetun" forces sabnzbd to connect to the internet through the VPN defined in the gluetun container above
network_mode: service:gluetun
depends_on:
gluetun:
condition: service_healthy
restart: always
labels:
- diun.enable=true
@@ -148,9 +152,53 @@ services:
- TZ=America/New_York
labels:
- diun.enable=true
jellystat-db:
image: postgres:15.2
shm_size: 1gb
container_name: jellystat-db
restart: unless-stopped
logging:
driver: json-file
options:
max-file: "5"
max-size: 10m
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${JELLYSTAT_POSTGRES_PASSWORD}
labels:
- diun.enable=true
volumes:
- postgres_data:/var/lib/postgresql/data
jellystat:
image: cyfershepard/jellystat:latest
container_name: jellystat
restart: unless-stopped
logging:
driver: json-file
options:
max-file: "5"
max-size: 10m
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${JELLYSTAT_POSTGRES_PASSWORD}
POSTGRES_IP: jellystat-db
POSTGRES_PORT: 5432
JWT_SECRET: ${JELLYSTAT_JWT_SECRET}
TZ: America/New_York
labels:
- diun.enable=true
volumes:
- jellystat-backup-data:/app/backend/backup-data
ports:
- 3200:3000
depends_on:
- jellystat-db
networks:
default: null
volumes:
gluetun_data: null
sabnzbd_data: null
tautulli: null
huntarr_data: null
postgres_data: null
jellystat-backup-data: null

5
miningwood/Caddyfile Normal file
View File

@@ -0,0 +1,5 @@
:80 {
root * /usr/share/caddy
encode gzip
file_server
}

View File

@@ -0,0 +1,16 @@
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- 8075:80
- 8043:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./:/usr/share/caddy
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data: null
caddy_config: null
networks: {}

140
miningwood/index.html Normal file
View File

@@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mining Wood - Precision. Profit. Power.</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Poppins', sans-serif;
}
</style>
</head>
<body class="bg-white text-gray-800">
<header class="bg-white shadow-md sticky top-0 z-50">
<div class="container mx-auto px-4 py-3 flex justify-between items-center">
<a href="https://miningwood.com" class="flex items-center hover:opacity-80 transition-opacity">
<img src="miningwood-logo.png" alt="Mining Wood Logo" class="h-20 mr-3" />
<h1 class="text-2xl font-bold text-blue-800">Mining Wood</h1>
</a>
<nav class="hidden md:flex items-center space-x-6">
<a href="#trading" class="text-gray-600 hover:text-blue-600 font-medium">Day Trading</a>
<a href="#software" class="text-gray-600 hover:text-blue-600 font-medium">Software</a>
<a href="#about" class="text-gray-600 hover:text-blue-600 font-medium">About</a>
<a href="#contact" class="text-gray-600 hover:text-blue-600 font-medium">Contact</a>
</nav>
</div>
</header>
<main>
<section class="bg-gray-800 text-white text-center py-20">
<div class="container mx-auto px-4">
<h2 class="text-cyan-400 text-sm font-bold uppercase tracking-widest">Precision. Profit. Power.</h2>
<h1 class="text-4xl md:text-6xl font-bold mt-2 mb-4">Combining Elite Day Trading with Cutting-Edge Algorithmic
Software.</h1>
<p class="text-lg max-w-2xl mx-auto mb-8 opacity-90">Mining Wood is an exclusive firm operating at the
intersection of high-frequency finance and bespoke technology. We don't just trade; we build the tools that
master the market.</p>
<div class="flex justify-center items-center space-x-4">
<a href="#software"
class="bg-cyan-400 text-gray-900 font-semibold px-8 py-3 rounded-md hover:bg-cyan-500 transition">Explore
Our Software</a>
<a href="#"
class="bg-transparent border-2 border-blue-500 text-blue-500 font-semibold px-8 py-3 rounded-md hover:bg-blue-500 hover:text-white transition">See
Trading Results</a>
</div>
</div>
</section>
<section class="py-16 bg-gray-50" id="trading">
<div class="container mx-auto px-4 text-center">
<h2 class="text-3xl font-bold mb-2">Our Dual Expertise</h2>
<p class="text-gray-600 max-w-3xl mx-auto mb-12">We are unified by a single purpose: to find and exploit
inefficiencies in the market
through superior technology and rigorous analysis.</p>
<div class="grid md:grid-cols-2 gap-8 text-left">
<div class="bg-white p-8 rounded-lg shadow-md">
<span class="text-3xl mb-4 block">📈</span>
<h3 class="text-xl font-bold text-blue-600 mb-2">Day Trading</h3>
<h4 class="font-semibold text-gray-800 mb-3">Data-Driven Execution</h4>
<p class="text-gray-600">Our team of expert traders utilizes quantitative models and real-time data analysis
to execute high-convection trades across various asset classes, focusing on <strong
class="font-semibold">high-probability setups</strong> and <strong class="font-semibold">risk
management</strong>.</p>
</div>
<div class="bg-white p-8 rounded-lg shadow-md">
<span class="text-3xl mb-4 block">💻</span>
<h3 class="text-xl font-bold text-blue-600 mb-2">Software Development</h3>
<h4 class="font-semibold text-gray-800 mb-3">Proprietary Trading Systems</h4>
<p class="text-gray-600">We design, develop, and maintain a suite of proprietary software
solutions—including backtesting engines, execution platforms, and sophisticated <strong
class="font-semibold">machine learning</strong> models—to give our traders an unmatched edge.
</p>
</div>
</div>
</div>
</section>
<section class="py-16" id="software">
<div class="container mx-auto px-4 text-center">
<h2 class="text-3xl font-bold mb-2">Built to Outperform</h2>
<p class="text-gray-600 max-w-3xl mx-auto mb-12">At the heart of Mining Wood is our technology division. We
specialize in creating
high-speed, reliable tools essential for modern finance.</p>
<div class="grid md:grid-cols-3 gap-8 text-left">
<div class="border border-gray-200 p-6 rounded-lg hover:shadow-lg hover:-translate-y-1 transition-transform">
<h4 class="font-bold text-blue-800 border-b-2 border-cyan-400 inline-block pb-1 mb-4">The 'Vein'
Backtester</h4>
<p class="text-gray-600">Run complex, multi-variable strategies against decades of historical data in
seconds. Optimize parameters
for peak performance before deployment.</p>
</div>
<div class="border border-gray-200 p-6 rounded-lg hover:shadow-lg hover:-translate-y-1 transition-transform">
<h4 class="font-bold text-blue-800 border-b-2 border-cyan-400 inline-block pb-1 mb-4">High-Frequency APIs
</h4>
<p class="text-gray-600">Direct, low-latency connections to major exchanges. Guaranteed speed and
reliability for execution when
every millisecond counts.</p>
</div>
<div class="border border-gray-200 p-6 rounded-lg hover:shadow-lg hover:-translate-y-1 transition-transform">
<h4 class="font-bold text-blue-800 border-b-2 border-cyan-400 inline-block pb-1 mb-4">Risk Monitoring
Dashboard</h4>
<p class="text-gray-600">Instantly visualize real-time exposure, drawdown, and portfolio health across all
active strategies.
Proactive alerting for defined thresholds.</p>
</div>
</div>
</div>
</section>
</main>
<footer class="bg-blue-800 text-white">
<div class="container mx-auto px-4 pt-12 pb-8">
<div class="grid md:grid-cols-3 gap-8 border-b border-blue-700 pb-8">
<div class="md:col-span-1">
<h4 class="text-cyan-400 font-bold mb-3">Mining Wood</h4>
<p class="text-sm">Jacksonville, Florida</p>
</div>
<div>
<h4 class="text-cyan-400 font-bold mb-3">Connect</h4>
<p class="text-sm">Email: <a href="mailto:contact@miningwood.com"
class="underline hover:text-cyan-300">contact
[at] miningwood.com</a></p>
</div>
</div>
<div class="text-center text-sm pt-6">
<p>&copy; 2025 Mining Wood. All Rights Reserved.</p>
</div>
</div>
</footer>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

0
miningwood/style.css Normal file
View File

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://gist.ptrwd.com/acedanger/stock-entry.js"></script>
</body>
</html>

View File

@@ -0,0 +1,327 @@
import warnings
from datetime import datetime, timedelta
import pandas as pd
import numpy as np
warnings.filterwarnings('ignore')
class TechnicalAnalyzer:
def __init__(self, data):
"""
Initialize with price data DataFrame
Expected columns: ['date', 'open', 'high', 'low', 'close', 'volume']
"""
self.data = data.copy()
self.signals = pd.DataFrame()
def calculate_sma(self, period):
"""Simple Moving Average"""
return self.data['close'].rolling(window=period).mean()
def calculate_ema(self, period):
"""Exponential Moving Average"""
return self.data['close'].ewm(span=period).mean()
def calculate_rsi(self, period=14):
"""Relative Strength Index"""
delta = self.data['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_macd(self, fast=12, slow=26, signal=9):
"""MACD Indicator"""
ema_fast = self.calculate_ema(fast)
ema_slow = self.calculate_ema(slow)
macd_line = ema_fast - ema_slow
signal_line = macd_line.ewm(span=signal).mean()
histogram = macd_line - signal_line
return macd_line, signal_line, histogram
def calculate_bollinger_bands(self, period=20, std_dev=2):
"""Bollinger Bands"""
sma = self.calculate_sma(period)
std = self.data['close'].rolling(window=period).std()
upper_band = sma + (std * std_dev)
lower_band = sma - (std * std_dev)
return upper_band, sma, lower_band
def calculate_atr(self, period=14):
"""Average True Range"""
high_low = self.data['high'] - self.data['low']
high_close = np.abs(self.data['high'] - self.data['close'].shift())
low_close = np.abs(self.data['low'] - self.data['close'].shift())
ranges = pd.concat([high_low, high_close, low_close], axis=1)
true_range = np.max(ranges, axis=1)
atr = true_range.rolling(window=period).mean()
return atr
def calculate_volume_indicators(self):
"""Volume-based indicators"""
# Volume Moving Average
vol_sma_20 = self.data['volume'].rolling(window=20).mean()
vol_ratio = self.data['volume'] / vol_sma_20
# On Balance Volume (OBV)
obv = (np.sign(self.data['close'].diff()) *
self.data['volume']).fillna(0).cumsum()
return vol_ratio, obv
def generate_all_indicators(self):
"""Calculate all technical indicators"""
# Moving Averages
self.data['sma_20'] = self.calculate_sma(20)
self.data['sma_50'] = self.calculate_sma(50)
self.data['ema_12'] = self.calculate_ema(12)
self.data['ema_26'] = self.calculate_ema(26)
# RSI
self.data['rsi'] = self.calculate_rsi()
# MACD
macd, signal, histogram = self.calculate_macd()
self.data['macd'] = macd
self.data['macd_signal'] = signal
self.data['macd_histogram'] = histogram
# Bollinger Bands
bb_upper, bb_middle, bb_lower = self.calculate_bollinger_bands()
self.data['bb_upper'] = bb_upper
self.data['bb_middle'] = bb_middle
self.data['bb_lower'] = bb_lower
# ATR
self.data['atr'] = self.calculate_atr()
# Volume indicators
vol_ratio, obv = self.calculate_volume_indicators()
self.data['vol_ratio'] = vol_ratio
self.data['obv'] = obv
return self.data
def identify_entry_signals(self):
"""Identify potential entry points"""
signals = []
for i in range(1, len(self.data)):
entry_score = 0
reasons = []
current = self.data.iloc[i]
previous = self.data.iloc[i-1]
# Moving Average Crossover (Golden Cross)
if (current['sma_20'] > current['sma_50'] and
previous['sma_20'] <= previous['sma_50']):
entry_score += 2
reasons.append("SMA Golden Cross")
# Price above both MAs
if current['close'] > current['sma_20'] > current['sma_50']:
entry_score += 1
reasons.append("Price above MAs")
# RSI oversold recovery
if previous['rsi'] < 30 and current['rsi'] > 30:
entry_score += 2
reasons.append("RSI oversold recovery")
# MACD bullish crossover
if (current['macd'] > current['macd_signal'] and
previous['macd'] <= previous['macd_signal']):
entry_score += 2
reasons.append("MACD bullish crossover")
# Bollinger Band bounce
if previous['close'] <= previous['bb_lower'] and current['close'] > previous['bb_lower']:
entry_score += 1
reasons.append("BB lower band bounce")
# Volume confirmation
if current['vol_ratio'] > 1.5: # 50% above average
entry_score += 1
reasons.append("High volume")
# Strong overall conditions
if (current['rsi'] > 40 and current['rsi'] < 70 and
current['macd'] > 0):
entry_score += 1
reasons.append("Favorable momentum")
if entry_score >= 3: # Minimum threshold for entry
signals.append({
'date': current['date'],
'type': 'ENTRY',
'price': current['close'],
'score': entry_score,
'reasons': reasons
})
return signals
def identify_exit_signals(self):
"""Identify potential exit points"""
signals = []
for i in range(1, len(self.data)):
exit_score = 0
reasons = []
current = self.data.iloc[i]
previous = self.data.iloc[i-1]
# Moving Average bearish cross
if (current['sma_20'] < current['sma_50'] and
previous['sma_20'] >= previous['sma_50']):
exit_score += 2
reasons.append("SMA Death Cross")
# Price below key MA
if current['close'] < current['sma_20']:
exit_score += 1
reasons.append("Price below SMA20")
# RSI overbought
if current['rsi'] > 70:
exit_score += 1
reasons.append("RSI overbought")
# RSI bearish divergence (simplified)
if previous['rsi'] > 70 and current['rsi'] < 70:
exit_score += 2
reasons.append("RSI overbought exit")
# MACD bearish crossover
if (current['macd'] < current['macd_signal'] and
previous['macd'] >= previous['macd_signal']):
exit_score += 2
reasons.append("MACD bearish crossover")
# Bollinger Band upper touch
if current['close'] >= current['bb_upper']:
exit_score += 1
reasons.append("BB upper band resistance")
# Volume spike (could indicate distribution)
if current['vol_ratio'] > 3.0:
exit_score += 1
reasons.append("Extreme volume spike")
if exit_score >= 3: # Minimum threshold for exit
signals.append({
'date': current['date'],
'type': 'EXIT',
'price': current['close'],
'score': exit_score,
'reasons': reasons
})
return signals
def analyze_stock(self):
"""Complete analysis workflow"""
# Generate all indicators
self.generate_all_indicators()
# Get entry and exit signals
entry_signals = self.identify_entry_signals()
exit_signals = self.identify_exit_signals()
# Combine all signals
all_signals = entry_signals + exit_signals
all_signals = sorted(all_signals, key=lambda x: x['date'])
return all_signals, self.data
# Example usage and demo data generation
def generate_sample_data(days=252):
"""Generate sample stock data for demonstration"""
np.random.seed(42) # For reproducible results
start_date = datetime.now() - timedelta(days=days)
dates = [start_date + timedelta(days=i) for i in range(days)]
# Generate realistic price movement
returns = np.random.normal(0.001, 0.02, days) # Daily returns
price = 100 # Starting price
prices = [price]
for ret in returns[1:]:
price *= (1 + ret)
prices.append(price)
# Generate OHLC data
data = []
for i, (date, close) in enumerate(zip(dates, prices)):
high = close * (1 + abs(np.random.normal(0, 0.015)))
low = close * (1 - abs(np.random.normal(0, 0.015)))
open_price = low + (high - low) * np.random.random()
volume = int(np.random.normal(1000000, 300000))
data.append({
'date': date,
'open': open_price,
'high': high,
'low': low,
'close': close,
'volume': max(volume, 100000) # Ensure positive volume
})
return pd.DataFrame(data)
# Demo execution
if __name__ == "__main__":
# Generate sample data
print("Generating sample stock data...")
sample_data = generate_sample_data(180) # 6 months of data
# Initialize analyzer
analyzer = TechnicalAnalyzer(sample_data)
# Run complete analysis
print("Analyzing technical indicators...")
signals, enhanced_data = analyzer.analyze_stock()
# Display results
print("\n=== TECHNICAL ANALYSIS RESULTS ===")
print(f"Analysis period: {len(sample_data)} days")
print(f"Total signals found: {len(signals)}")
# Show recent indicators
print("\n=== LATEST INDICATOR VALUES ===")
latest = enhanced_data.iloc[-1]
print(f"Price: ${latest['close']:.2f}")
print(f"RSI: {latest['rsi']:.2f}")
print(f"MACD: {latest['macd']:.4f}")
print(f"Volume Ratio: {latest['vol_ratio']:.2f}x")
print(f"20-day SMA: ${latest['sma_20']:.2f}")
print(f"50-day SMA: ${latest['sma_50']:.2f}")
# Show recent signals
print("\n=== RECENT SIGNALS ===")
recent_signals = [s for s in signals if s['date']
>= (datetime.now() - timedelta(days=30))]
if recent_signals:
for signal in recent_signals[-5:]: # Last 5 signals
print(f"\n{signal['type']} Signal:")
print(f" Date: {signal['date'].strftime('%Y-%m-%d')}")
print(f" Price: ${signal['price']:.2f}")
print(f" Score: {signal['score']}")
print(f" Reasons: {', '.join(signal['reasons'])}")
else:
print("No recent signals found.")
print("\n=== USAGE NOTES ===")
print("1. Replace sample data with real market data from your preferred source")
print("2. Adjust indicator parameters based on your trading style")
print("3. Modify signal thresholds based on backtesting results")
print("4. Always combine with risk management and position sizing")
print("5. Consider market conditions and fundamental analysis")

View File

@@ -13,7 +13,7 @@ experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.1.0"
version: "v1.2.0"
log:
level: "INFO"
@@ -41,6 +41,8 @@ entryPoints:
certResolver: "letsencrypt"
tcp-2229:
address: ":2229/tcp"
tcp-5432:
address: ":5432/tcp"
serversTransport:
insecureSkipVerify: true

View File

@@ -1,7 +1,7 @@
name: pangolin
services:
pangolin:
image: fosrl/pangolin:1.4.0
image: fosrl/pangolin:1.10.3
container_name: pangolin
restart: unless-stopped
labels:
@@ -18,7 +18,7 @@ services:
timeout: 10s
retries: 15
gerbil:
image: fosrl/gerbil:1.0.0
image: fosrl/gerbil:1.2.1
container_name: gerbil
restart: unless-stopped
labels:
@@ -38,11 +38,13 @@ services:
- SYS_MODULE
ports:
- 51820:51820/udp
- 21820:21820/udp # port for ACCEPT_CLIENTS env variable
- 443:443 # Port for traefik because of the network_mode
- 80:80 # Port for traefik because of the network_mode
- 2229:2229 # port for gitea
- 2229:2229 # port for gitea, served from europa; git.ptrwd.com
- 5432:5432 # port for postgres, served from io
traefik:
image: traefik:v3.3.6
image: traefik:v3
container_name: traefik
restart: unless-stopped
labels: