VPS-এ সেলফ-হোস্টেড স্টক রিসার্চ এজেন্ট তৈরির গাইড
Python, DuckDB এবং systemd টাইমার ব্যবহার করে নিজস্ব স্টক রিসার্চ এজেন্ট তৈরি করুন। মার্কেট ক্লোজ হওয়ার পর স্বয়ংক্রিয়ভাবে ডেটা সংগ্রহ ও LLM দিয়ে বিশ্লেষণের পূর্ণাঙ্গ প্রক্রিয়া দেখুন।
সেলফ-হোস্টেড স্টক রিসার্চ এজেন্ট কী
সেলফ-হোস্টেড স্টক রিসার্চ এজেন্ট হলো আপনার নিজস্ব সার্ভারে চলা একটি ছোট প্রোগ্রাম। এটি নির্দিষ্ট সময় পরপর মার্কেট ডেটা সংগ্রহ করে, তা একটি লোকাল ডেটাবেসে জমা রাখে, ডেটার ওপর স্ক্রিনিং চালায় এবং কী পরিবর্তন হয়েছে তা লিখে দেওয়ার জন্য একটি লার্জ ল্যাঙ্গুয়েজ মডেলকে (LLM) অনুরোধ করে। এটি ডেটা পড়ে এবং ফিল্টার করে। এটি ট্রেড করে না এবং এই গাইডের কোনো কিছুই আর্থিক পরামর্শ নয়।
দুজন ব্যক্তি যদি এটি স্ক্র্যাচ থেকে তৈরি করেন, তবে তারা ভিন্ন ভিন্ন লাইব্রেরি বেছে নিতে পারেন, কিন্তু শেষ পর্যন্ত চারটি একই অংশ তৈরি হবে: একটি ফিড যা প্রাইস এবং ফান্ডামেন্টাল ডেটা সরবরাহ করে, একটি লোকাল স্টোর যা আপনার সংগৃহীত প্রতিটি সারি জমা রাখে, একটি জব যা টাইমারের মাধ্যমে স্টোর রিফ্রেশ করে এবং একটি LLM লেয়ার যা টিকে থাকা সারিগুলোকে বাক্যে রূপান্তর করে। এই গাইডটি Python, DuckDB, systemd টাইমার এবং Claude API (অ্যাপ্লিকেশন প্রোগ্রামিং ইন্টারফেস) ব্যবহার করে সেই কাঠামোটি তৈরি করে। এক্সিকিউশন একটি আলাদা কাজ যার ব্যর্থতার ধরনও আলাদা, এবং এটি ট্রেডিং বটের জন্য সেটআপ করা একটি VPS-এ রাখা উচিত।
চারটি অংশ এবং প্রতিটি অংশের কাজ
The feed হলো একমাত্র অংশ যা বাইরের জগতের সাথে যোগাযোগ করে। এটি জানে কীভাবে একটি ticker এবং তারিখের সীমা (date range) চাইতে হয় এবং কীভাবে ডেটার সারিগুলো (rows) ফেরত দিতে হয়। ডাউনস্ট্রিমের সবকিছুই feed-এর পরিবর্তে আপনার ডাটাবেস থেকে তথ্য পড়ে, তাই feed-এ কোনো সমস্যা হলে আপনার কেবল একদিনের নতুন ডেটা হারানোর ঝুঁকি থাকে, পুরো স্ক্রিন ভেঙে যাওয়ার মতো বড় কোনো বিপর্যয় ঘটে না।
The store হলো পুরো প্রক্রিয়ার মূল লক্ষ্য। কোনো দিনের সমাপনী মূল্য (daily close) রেকর্ড করতে না পারলে সাধারণত তা পরে আবার সংগ্রহ করা যায়। কিন্তু intraday quote, সংশোধনের আগের কোনো অনুমান (estimate), অথবা পুনরায় হিসাব করার আগের কোনো মৌলিক পরিসংখ্যান (fundamentals figure) পরে আর পাওয়া যায় না। কোনো ডেটা যেদিন যা বলেছিল, তার একটি রেকর্ড ধরে রাখার উপায় হলো এই store।
The scheduler নির্ধারণ করে কখন refresh প্রক্রিয়াটি ঘটবে। সার্ভারে এটি একটি systemd timer হিসেবে কাজ করে, আর এই কারণেই কোডের চেয়ে VPS এখানে বেশি গুরুত্বপূর্ণ।
The LLM layer আপনার SQL থেকে তৈরি হওয়া একটি ছোট টেক্সট ব্লক পড়ে এবং তার একটি সারাংশ লেখে। এটি কখনোই ডাটাবেসের সাথে সংযুক্ত হয় না এবং কখনোই কুয়েরি (query) তৈরি করে না। যদি মডেলটি নিজেই SQL লেখে, তবে একটি ভুল টোকেন একটি সাবলীল বাক্যের ভেতরে ভুল সংখ্যা তৈরি করতে পারে, যা যাচাই করার কোনো উপায় থাকে না। যদি SQL সংখ্যাগুলো তৈরি করে, তবে মডেলটি কেবল বর্ণনায় ভুল করতে পারে, আর আপনি সেই বর্ণনাটি আপনার পাঠানো ডেটার সারির সাথে মিলিয়ে যাচাই করে নিতে পারেন।
ল্যাপটপের পরিবর্তে কেন VPS-এ এটি চালাবেন
এর কারণ হলো শিডিউলার। নিউ ইয়র্ক সময় 16:00-তে মার্কিন বাজার বন্ধ হওয়া মানে বার্লিনে তখন 22:00 এবং জাকার্তায় পরের দিন ভোর 04:00। এই দুই সময়েই ল্যাপটপ সাধারণত স্লিপ মোডে থাকে। একটি রান মিস করলে তা কেবল দেরিতে নোটিফিকেশন পাওয়ার চেয়েও বেশি ক্ষতির কারণ হয়: ডেইলি বারগুলো সাধারণত পরে পুনরায় ফেচ করা যায়, কিন্তু যা একবার রিভাইজ হয়ে গেছে তা আর ফিরে পাওয়া সম্ভব নয়, তাই আপনার রেকর্ডে সেই গ্যাপ স্থায়ী হয়ে যায়।
দ্বিতীয় কারণটি ছোট হলেও বাস্তব। সার্ভারে একটি API key থাকে, যা একটি ফাইলে সংরক্ষিত, যার মালিক কোনো লগইন শেল ছাড়া একটি সিস্টেম ইউজার এবং এটি কেবল একটি নির্দিষ্ট কাজের জন্য ব্যবহৃত হয়। আপনি যে ল্যাপটপ দিয়ে ওয়েব ব্রাউজ করেন, তাতে এই পরিবেশ তৈরি করা অনেক বেশি কঠিন। কোড এবং মডেলের ইনপুট থেকে key-টিকে দূরে রাখুন, যা AI এজেন্টে API key সুরক্ষিত রাখা-এর মূল বিষয়।
ডিস্ক, র্যাম এবং টোকেনের আকার নির্ধারণ
ডিস্কের হিসাব করা সহজ। প্রতিদিনের একটি বারের জন্য প্রতিটি টিকারে প্রতি ট্রেডিং দিনে একটি সারি থাকে এবং মার্কিন ট্রেডিং বছরে প্রায় 252 দিন থাকে।
The data behind this chart
[
{
"label": "20 tickers",
"rows_after_1y": "5,040",
"rows_after_10y": "50,400"
},
{
"label": "100 tickers",
"rows_after_1y": "25,200",
"rows_after_10y": "252,000"
},
{
"label": "500 tickers",
"rows_after_1y": "126,000",
"rows_after_10y": "1,260,000"
}
]বিশটি টিকারে এক বছর পর 5,040 টি সারি হয়। 500 tickers লাইনটি দশ বছর পর 1,260,000 টি সারিতে পৌঁছায়। প্রতিটি সারিতে একটি তারিখ এবং কয়েকটি double ভ্যালু থাকে। DuckDB কলামগুলোকে কম্প্রেস করে রাখে, তাই এটি গিগাবাইটের পরিবর্তে মাত্র কয়েক মেগাবাইট জায়গা নেয়। আমার দেওয়া এই হিসাবসহ কোনো অনুমানের ওপরই পুরোপুরি নির্ভর করবেন না। আপনার প্রথম ব্যাকফিল সম্পন্ন হওয়ার পর du -h /opt/research/data/market.duckdb চালান এবং আপনার নিজের প্রাপ্ত সংখ্যাটি ব্যবহার করুন।
ছোট VPS-এর ক্ষেত্রে র্যাম একটি বড় সমস্যা। DuckDB ডিফল্টভাবে একটি কুয়েরির জন্য মেশিনের মেমোরির একটি বড় অংশ এবং সব কোর ব্যবহার করে। এটি একটি অ্যানালিটিক্স সার্ভারের জন্য ঠিক থাকলেও, 2 GB র্যামের এমন বক্সে ভুল, যেখানে অন্যান্য সার্ভিসও চলছে। পুরো প্রাইস টেবিলের ওপর একটি এগ্রিগেশন চালালে কার্নেল প্রসেসটিকে কিল করে দেয় এবং systemd Main process exited, code=killed, status=9/KILL রিপোর্ট করে, যেখানে journalctl -k-এ আউট অফ মেমোরি কিল হওয়ার বিষয়টি দেখা যায়। memory_limit এবং threads স্পষ্টভাবে সেট করে দিন, এতে কুয়েরিটি বন্ধ না হয়ে বরং ধীরগতিতে সম্পন্ন হবে।
টোকেনের হিসাব অনুমান না করে পরিমাপ করা উচিত। প্রতিটি Messages API রেসপন্সে একটি usage অবজেক্ট থাকে, যাতে input_tokens এবং output_tokens অন্তর্ভুক্ত থাকে। প্রতিটি কলের সময় এই দুটি মান একটি টেবিলে লিখে রাখুন। এক সপ্তাহ পর আপনি আপনার প্রকৃত ভলিউম জানতে পারবেন, যা আপনি চেক করার দিন আপনার মডেলের নির্ধারিত মূল্যের সাথে গুণ করবেন। পরিকল্পনা করার জন্য দুটি বিষয় বেশ স্থিতিশীল। প্রতিটি Claude মডেলে আউটপুট টোকেনের দাম ইনপুট টোকেনের চেয়ে বেশি। তাই নোটের দৈর্ঘ্য 200 শব্দে সীমাবদ্ধ রাখা, আপনার পাঠানো ডেটা কমানোর চেয়ে বিলের ওপর বেশি প্রভাব ফেলে। এছাড়া প্রম্পট ক্যাশিং প্রতিদিন একবার চলা কাজের ক্ষেত্রে কোনো সাহায্য করে না, কারণ ক্যাশের স্থায়িত্ব মাত্র কয়েক মিনিট। পরবর্তী রান শুরু হওয়ার আগেই ক্যাশ করা ব্লকটি এক্সপায়ার হয়ে যায় এবং আপনাকে পুনরায় পুরো ইনপুট মূল্য পরিশোধ করতে হয়। ক্যাশিং তখনই লাভজনক হয় যখন একটি রান একই বড় টেক্সট ব্লকের ওপর অনেকগুলো কল করে।
Install the pieces
sudo apt update
sudo apt install -y python3-venv
sudo useradd --system --create-home --home-dir /opt/research --shell /usr/sbin/nologin research
sudo -u research python3 -m venv /opt/research/venv
sudo -u research /opt/research/venv/bin/pip install duckdb pandas yfinance anthropic
sudo install -d -o research -g research -m 750 /opt/research/dataকোনো কোড লেখার আগে ইনস্টলেশন যাচাই করুন:
sudo -u research /opt/research/venv/bin/python -c 'import duckdb, yfinance, anthropic; print("ok")'এটি ok প্রিন্ট করবে। আপনি যদি virtual environment ব্যবহার না করে সরাসরি system Python-এ pip install চালান, তবে Ubuntu 24.04 আপনাকে error: externally-managed-environment দিয়ে বাধা দেবে। কারণ /usr/lib/python3 ডিরেক্টরিটি ডিস্ট্রিবিউশনের মালিকানাধীন এবং pip-কে সেখানে কিছু লিখতে দেওয়া হয় না। venv কোনো সৌজন্যমূলক বিষয় নয়। এটিই একমাত্র ডিরেক্টরি যেখানে pip-এর কাজ করার অনুমতি আছে।
API key এমন একটি ফাইলে রাখুন যা শুধুমাত্র service user পড়তে পারে এবং অন্য কেউ পারে না:
sudo install -d -m 755 /etc/research
sudo install -m 640 -o root -g research /dev/null /etc/research/env
sudoedit /etc/research/envফাইলটিতে কোনো উদ্ধৃতি চিহ্ন (quotes) বা export ছাড়া একটি লাইন লিখুন। কারণ systemd এই ফাইলটি নিজে পার্স করে, কোনো shell-এর কাছে পাঠায় না:
ANTHROPIC_API_KEY=sk-ant-your-key-hereস্টোর: দুটি টেবিল
# /opt/research/store.py
import duckdb
DB = '/opt/research/data/market.duckdb'
SCHEMA = [
"""
CREATE TABLE IF NOT EXISTS prices (
ticker VARCHAR,
day DATE,
open DOUBLE,
high DOUBLE,
low DOUBLE,
close DOUBLE,
volume BIGINT,
PRIMARY KEY (ticker, day)
)
""",
"""
CREATE TABLE IF NOT EXISTS runs (
started_at TIMESTAMPTZ,
model VARCHAR,
input_tokens BIGINT,
output_tokens BIGINT,
hits BIGINT
)
""",
]
def connect(read_only=False):
con = duckdb.connect(DB, read_only=read_only)
con.execute("SET memory_limit='512MB'")
con.execute('SET threads=2')
if not read_only:
for statement in SCHEMA:
con.execute(statement)
return con(ticker, day)-এর প্রাইমারি কি (primary key) রিফ্রেশ প্রক্রিয়াটিকে বারবার চালানোর উপযোগী করে তোলে। INSERT OR REPLACE নির্দিষ্ট টিকার (ticker) এবং দিনের জন্য বিদ্যমান সারিটিকে ওভাররাইট করে, তাই ব্যাকফিল (backfill) দুবার চালালেও টেবিলে তথ্যের দ্বিরুক্তি ঘটে না। এই কি (key) না থাকলে, কোনো ক্র্যাশের পর পুনরায় চালানোর সময় প্রতিটি বার (bar) নীরবে ডুপ্লিকেট হয়ে যাবে এবং পরবর্তীতে আপনার হিসাব করা প্রতিটি গড় ভুল আসবে, অথচ কোথাও কোনো এরর মেসেজ পাওয়া যাবে না।
DuckDB-তে একই সময়ে কেবল একটি প্রসেস ফাইলটিকে রাইট মোডে ওপেন রাখতে পারে। দ্বিতীয় কোনো রাইটার (writer) চেষ্টা করলে তা তৎক্ষণাৎ Could not set lock on file এরর দিয়ে ব্যর্থ হয়, যার সাথে সেই PID-টিও উল্লেখ থাকে যা ফাইলটিকে লক করে রেখেছে; সাধারণত এটি সেই ইন্টারঅ্যাক্টিভ duckdb শেল যা আপনি অন্য কোনো টার্মিনালে ওপেন করে রেখেছেন। রিডাররা read_only=True মোডে ফাইল অ্যাক্সেস করতে পারে, আর এই কারণেই connect ফ্ল্যাগটি ব্যবহার করা হয়। যদি একাধিক প্রসেসের একই সময়ে রাইট করার প্রয়োজন হয়, তবে সেটি অন্য কোনো ইঞ্জিনের কাজ: SQLite-এর WAL মোডে একজন রাইটার কমিট করার সময়ও রিডাররা কাজ চালিয়ে যেতে পারে এবং একটি বিজি টাইমআউট (busy timeout) সেট করা থাকলে অন্য রাইটাররা ব্যর্থ না হয়ে অপেক্ষা করতে পারে। সার্ভার ওয়ার্কলোডের জন্য DuckDB বনাম SQLite এই দুটির তুলনা করে এবং VPS-এ প্রোডাকশন পর্যায়ে SQLite চালানো নিবন্ধে WAL মোড সঠিকভাবে কাজ করার জন্য প্রয়োজনীয় সেটিংস আলোচনা করা হয়েছে।
রিফ্রেশ জব
# /opt/research/refresh.py
import sys
import pandas as pd
import yfinance as yf
from store import connect
TICKERS = ['AAPL', 'MSFT', 'KO', 'SAP', 'TSM']
FIRST_DAY = '2016-01-01'
COLS = ['ticker', 'day', 'open', 'high', 'low', 'close', 'volume']
def fetch(ticker, start):
df = yf.Ticker(ticker).history(start=start, auto_adjust=True)
if df.empty:
return None
df = df.reset_index()
stamps = pd.to_datetime(df['Date'])
if stamps.dt.tz is not None:
stamps = stamps.dt.tz_localize(None)
df['day'] = stamps.dt.date
df['ticker'] = ticker
df = df.rename(columns={'Open': 'open', 'High': 'high', 'Low': 'low',
'Close': 'close', 'Volume': 'volume'})
return df[COLS]
def main():
con = connect()
empty = 0
for ticker in TICKERS:
last = con.execute('SELECT max(day) FROM prices WHERE ticker = ?',
[ticker]).fetchone()[0]
rows_df = fetch(ticker, str(last) if last else FIRST_DAY)
if rows_df is None:
print(ticker + ': feed returned no rows', file=sys.stderr)
empty += 1
continue
con.register('rows_df', rows_df)
con.execute('INSERT OR REPLACE INTO prices '
'SELECT ticker, day, open, high, low, close, volume FROM rows_df')
print(ticker + ': ' + str(len(rows_df)) + ' rows')
con.close()
if empty == len(TICKERS):
print('every ticker returned nothing: the feed is broken', file=sys.stderr)
sys.exit(1)
main()এটি একবার হাতে চালিয়ে দেখুন:
sudo -u research /opt/research/venv/bin/python /opt/research/refresh.pyপ্রথমবার চালানোর সময় এটি কয়েক বছরের ডেটা ব্যাকফিল করে এবং প্রতিটি টিকারের জন্য হাজার হাজার সারির একটি তালিকা দেখায়। এক মিনিট পর আবার চালালে প্রতিটি লাইনে 1 বা 2 সারি দেখাবে, কারণ জবটি শেষ যে দিন পর্যন্ত ডেটা সংরক্ষিত আছে সেখান থেকে শুরু হয়। দ্বিতীয়বার চালানোই হলো আসল পরীক্ষা: যদি তখনও হাজার হাজার সারি দেখায়, তবে বুঝতে হবে max(day) কোনো ডেটা ফেরত দিচ্ছে না এবং ইনসার্ট অপারেশনটি প্রতি রাতে আপনার পুরো হিস্ট্রি পুনরায় লিখছে।
df.empty চেকটি ফাইলের সবচেয়ে গুরুত্বপূর্ণ লাইন। কোনো ভুল বা ডিলিস্ট হয়ে যাওয়া সিম্বলের জন্য Ticker.history() কোনো এরর রেজ করে না। এটি কেবল একটি ওয়ার্নিং দেয় যে সিম্বলটি সম্ভবত ডিলিস্ট হয়ে গেছে এবং কোনো প্রাইস ডেটা পাওয়া যায়নি (লাইব্রেরি ভার্সনভেদে এর ভাষা কিছুটা ভিন্ন হতে পারে) এবং একটি খালি DataFrame ফেরত দেয়। এই চেকটি ছাড়া কোনো জব কিছু না লিখেও সফলভাবে (exit 0) শেষ হয়, এবং systemd-এ সব কিছু ঠিকঠাক দেখালেও টেবিলের ডেটা আর বাড়ে না। আপনি কয়েক সপ্তাহ পর বুঝতে পারবেন যে স্ক্রিনে প্রতিদিন একই ডেটা দেখাচ্ছে।
টাইমস্ট্যাম্প হ্যান্ডলিংয়ের পেছনেও একটি কারণ আছে। ফিড থেকে আসা ইনডেক্সে এক্সচেঞ্জের টাইমজোন থাকতে পারে, এবং অফসেট বাদ দেওয়া আর UTC-তে কনভার্ট করা এক বিষয় নয়। টোকিও সেশনের মধ্যরাতের টাইমস্ট্যাম্প UTC-তে কনভার্ট করলে তা আগের ক্যালেন্ডার দিনে চলে যায়। ফলে UTC কনভার্সন জাপানিজ বারগুলোকে ভুলবশত একদিন পিছিয়ে দেয় এবং প্রাইমারি কি (primary key) নষ্ট করে ফেলে। tz_localize(None) এক্সচেঞ্জের নিজস্ব সেশন ডেট বজায় রাখে, যা একটি ডেইলি বারের জন্য সঠিক।
বাজার বন্ধের সময় কার্যকর হওয়া টাইমার
মার্কিন বাজার নিউ ইয়র্ক সময় 16:00-তে বন্ধ হয় এবং শেষ লেনদেনগুলো সেটেল হতে কয়েক মিনিট সময় নেয়, তাই কাজটি 16:20-তে চালানো হয়। এটি UTC-তে নয়, বরং নিউ ইয়র্ক সময় অনুযায়ী লিখুন। শীতকালে নিউ ইয়র্ক UTC থেকে 5 ঘণ্টা পিছিয়ে এবং গ্রীষ্মকালে 4 ঘণ্টা পিছিয়ে থাকে, তাই নির্দিষ্ট UTC ঘণ্টায় টাইমার সেট করলে বছরে দুবার সময়ের তারতম্য ঘটে এবং বাজার বন্ধ হওয়ার আগেই টাইমার চালু হয়ে যায়। systemd 252 এবং পরবর্তী সংস্করণগুলো সরাসরি OnCalendar-এ টাইমজোন গ্রহণ করে এবং Ubuntu 24.04-এ 255 সংস্করণটি থাকে। systemctl --version দিয়ে আপনার সংস্করণটি নিশ্চিত করুন।
# /etc/systemd/system/research-refresh.service
[Unit]
Description=Refresh market data and run the daily screen
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
User=research
Group=research
WorkingDirectory=/opt/research
EnvironmentFile=/etc/research/env
ExecStart=/opt/research/venv/bin/python /opt/research/refresh.py
ExecStart=/opt/research/venv/bin/python /opt/research/screen.py# /etc/systemd/system/research-refresh.timer
[Unit]
Description=Run the refresh after the US market close
[Timer]
OnCalendar=Mon-Fri 16:20 America/New_York
Persistent=true
RandomizedDelaySec=180
[Install]
WantedBy=timers.targetType=oneshot হলো একমাত্র সার্ভিস টাইপ যা একাধিক ExecStart গ্রহণ করে এবং এটি সেগুলোকে ক্রমানুসারে চালায়, কোনো একটি নন-জিরো (non-zero) কোড নিয়ে বন্ধ হলে পরবর্তীগুলো আর চলে না। আপনি ঠিক এই আচরণটিই চান: একটি রিফ্রেশ ব্যর্থ হলে পুরনো ডেটার ওপর ভিত্তি করে স্ক্রিন আপডেট হওয়া উচিত নয়। কার্নেল আপডেটের জন্য রিবুট হওয়া VPS-এর ক্ষেত্রে Persistent=true গুরুত্বপূর্ণ। এটি ছাড়া 16:15-তে রিবুট হলে কাজটি আর হয় না; এটি থাকলে মেশিন চালু হওয়ার সাথে সাথেই কাজটি সম্পন্ন হয়।
sudo systemctl daemon-reload
sudo systemctl enable --now research-refresh.timer
systemctl list-timers research-refresh.timerlist-timers কমান্ডটি দেখালে সেখানে একটি NEXT কলাম থাকা উচিত, যেখানে পরবর্তী সপ্তাহের কাজের সময় সার্ভারের নিজস্ব স্থানীয় সময় অনুযায়ী দেখানো হবে। তালিকা খালি থাকার অর্থ হলো টাইমারটি এনাবল করা নেই, অথবা ইউনিটে [Install] সেকশনটি নেই, যার ফলে enable-এর পক্ষে timers.target-এ কোনো লিঙ্ক তৈরি করা সম্ভব হয়নি।
স্ক্রিন: প্রথমে SQL, শেষে মডেল
SQL সুনির্দিষ্ট এবং প্রতিবার চালানোর জন্য কোনো খরচ হয় না। মডেলের ক্ষেত্রে এই দুটিই প্রযোজ্য নয়। তাই কুয়েরি (query) ডেটার পরিধি কমিয়ে আনে এবং শুধুমাত্র টিকে থাকা ডেটাগুলোই মডেলে পাঠানো হয়। এটি /opt/research/screen.sql হিসেবে সেভ করুন।
WITH ma AS (
SELECT ticker, day, close,
avg(close) OVER w20 AS ma20,
avg(close) OVER w50 AS ma50,
row_number() OVER (PARTITION BY ticker ORDER BY day) AS n
FROM prices
WINDOW
w20 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 19 PRECEDING AND CURRENT ROW),
w50 AS (PARTITION BY ticker ORDER BY day ROWS BETWEEN 49 PRECEDING AND CURRENT ROW)
)
SELECT ticker, day, close, round(ma20, 2) AS ma20, round(ma50, 2) AS ma50
FROM ma
WHERE n > 50 AND ma20 > ma50
ORDER BY day DESC, ticker
LIMIT 20;n > 50 ফিল্টারটি কোনো সাজসজ্জার বিষয় নয়। ROWS BETWEEN 49 PRECEDING AND CURRENT ROW বিদ্যমান যেকোনো সারির গড় হিসাব করে, তাই একটি টিকারের (ticker) তৃতীয় সারিতে তিন দিনের গড় মান পাওয়া যায় এবং একে তখনও ma50 বলা হয়। এর সাথে ma20-এর তুলনা করলে আপনি প্রতিটি টিকারের ইতিহাসের শুরুতে এমন একটি ক্রসওভার তৈরি করবেন যা বাস্তবে ঘটেনি। রো (row) নম্বরের ওপর ফিল্টার প্রয়োগ করলে সেই সারিগুলো বাদ পড়ে যায় যেখানে উইন্ডোটি পূর্ণ ছিল না।
মডেল যা দেখে এবং যা কখনোই দেখে না
# /opt/research/screen.py
from anthropic import Anthropic
from store import connect
SYSTEM = (
'You are a research assistant. Use only the rows in the message. '
'If a number is not in the rows, say that it is not available. '
'Do not give investment advice, price targets or buy and sell calls. '
'Write at most 200 words.'
)
con = connect()
sql = open('/opt/research/screen.sql').read()
rows = con.execute(sql).fetchall()
block = '\n'.join(' / '.join(str(v) for v in row) for row in rows)
client = Anthropic(max_retries=5) # reads ANTHROPIC_API_KEY from the environment
resp = client.messages.create(
model='claude-sonnet-5',
max_tokens=600,
system=SYSTEM,
messages=[{'role': 'user', 'content': 'Screen hits, ticker / day / close / ma20 / ma50:\n' + block}],
)
print(resp.content[0].text)
con.execute('INSERT INTO runs VALUES (now(), ?, ?, ?, ?)',
['claude-sonnet-5', resp.usage.input_tokens,
resp.usage.output_tokens, len(rows)])
con.close()সিস্টেম প্রম্পটে শব্দের সীমাবদ্ধতা খরচের একটি বড় অংশ নিয়ন্ত্রণ করে। শুধুমাত্র প্রদত্ত সারিগুলো ব্যবহার করার নির্দেশটি বিশ্বাস করার চেয়ে যাচাই করা জরুরি: ব্লক থেকে একটি কলাম মুছে ফেলুন, পুনরায় চালান এবং আউটপুট পড়ুন। যদি সেই কলামের কোনো সংখ্যা তখনও দেখা যায়, তবে মডেলটি শূন্যস্থান পূরণ করেছে এবং আপনার প্রম্পটটি যথেষ্ট সুনির্দিষ্ট নয়। এই পরীক্ষাটি করতে দুই মিনিট সময় লাগে এবং এটিই সত্যতা যাচাইয়ের একমাত্র সৎ উপায়।
মডেলটি কখনোই API key দেখে না, ডাটাবেসের পাথ দেখে না এবং কোনো কুয়েরি (query) চালায় না। এটি শুধু সারিগুলো গ্রহণ করে এবং গদ্য আকারে উত্তর দেয়। এই সীমানাটিই আউটপুটকে যাচাইযোগ্য করে তোলে, কারণ নোটের প্রতিটি সংখ্যা আপনার পাঠানো ব্লকেও থাকা উচিত এবং আপনি সেগুলো লাইন বাই লাইন মিলিয়ে দেখতে পারেন। এই বিষয়ে প্রম্পটিংয়ের দিকটি আরও বিস্তারিত জানতে, আর্থিক বিশ্লেষণের জন্য Claude ব্যবহার বিষয়টি পড়ুন, যা মডেলটি কী পড়তে দক্ষ তা নিয়ে আরও গভীরে আলোচনা করে।
এক রান, শুরু থেকে শেষ
নিউ ইয়র্ক সময় 16:20-এ টাইমার সার্ভিসটি চালু হয়। refresh.py প্রতিটি টিকারের জন্য শেষ সংরক্ষিত দিন থেকে ফিড চেক করে, প্রতিটি টিকারের জন্য এক বা দুটি নতুন বার লেখে এবং প্রতি টিকারের জন্য একটি করে লাইন প্রিন্ট করে। screen.py একই ফাইল খোলে, মুভিং এভারেজ কুয়েরি চালায় এবং কয়েকটি সারি ফলাফল হিসেবে পায়। এই সারিগুলো কয়েকশ টোকেনের একটি টেক্সট ব্লকে পরিণত হয়। একটি API কল সেগুলোকে একটি ছোট নোটে রূপান্তর করে, নোটটি জার্নালে জমা হয় এবং টোকেন সংখ্যা ও হিট সংখ্যাসহ একটি সারি runs-এ যুক্ত হয়।
journalctl -u research-refresh.service -n 50 --no-pagerএকটি সঠিক লগে প্রতি টিকারের জন্য একটি লাইন, তারপর নোট এবং শেষে research-refresh.service: Deactivated successfully থাকে। এক সপ্তাহ পর, ডাটাবেস থেকে আপনার নিজের খরচ বের করে নিন:
SELECT count(*) AS runs,
sum(input_tokens) AS in_tokens,
sum(output_tokens) AS out_tokens
FROM runs;এই মোট সংখ্যাকে আপনার মডেলের প্রতি মিলিয়ন টোকেনের মূল্য দিয়ে গুণ করুন, যা আপনি পড়ার দিন তালিকাভুক্ত ছিল। এটি আপনাকে অন্যের অনুমানের পরিবর্তে প্রকৃত হিসাব দেবে। প্রকাশিত মূল্য পরিবর্তিত হয়। কিন্তু গাণিতিক হিসাব একই থাকে।
কেন ব্যাকটেস্ট ওভারফিট করে এবং কীভাবে তা পর্যবেক্ষণ করবেন
উইন্ডোর দুটি দৈর্ঘ্যের ফাংশন হিসেবে স্ক্রিনটিকে পুনরায় লিখুন, জোড়াগুলোর একটি গ্রিড তৈরি করুন এবং রিটার্নের ভিত্তিতে সেগুলোকে র্যাঙ্ক করুন। সেরা জোড়াটিকে দেখতে চমৎকার মনে হবে। এটিই সমস্যা, ফলাফল নয়। 200টি জোড়ার একটি গ্রিড মানে 200টি পরীক্ষা, এবং আপনি সবচেয়ে ভাগ্যবানটিকে বেছে নিয়েছেন।
আপনি দশ মিনিটেই এটি ঘটতে দেখতে পাবেন। তারিখ অনুযায়ী ডেটাস্টোরকে দুই ভাগে ভাগ করুন। শুধুমাত্র প্রথম অর্ধে গ্রিডটি চালান এবং বিজয়ীটিকে লিখে রাখুন। দ্বিতীয় অর্ধে একই গ্রিড চালান। যদি দুটি বিজয়ী জোড়া একে অপরের থেকে অনেক দূরে থাকে, তবে প্যারামিটারগুলো নয়েজ বা অসংলগ্ন ডেটার সাথে ফিট করছে। যে জোড়াটি শুধুমাত্র টিউনিং করা অর্ধে জেতে, তা আগামীকালের জন্য কোনো অর্থ বহন করে না।
সারভাইভারশিপ বায়াস (Survivorship bias) ওভারফিটিংয়ের চেয়েও খারাপ, কারণ টিউনিং দিয়ে এটি ঠিক করা যায় না। আপনার টিকার লিস্টটি বর্তমান ইনডেক্সের সদস্যদের নিয়ে গঠিত, তাই এতে কেবল সেই কোম্পানিগুলোই আছে যারা টিকে আছে। 2019 সালে ডিলিস্ট হয়ে যাওয়া কোনো টিকারের জন্য ফিডকে জিজ্ঞাসা করলে এটি একটি খালি ফ্রেম ফেরত দেবে, যার অর্থ সেই কোম্পানিটি আপনার স্টোরে বা টেস্টে কখনোই প্রবেশ করে না। আপনি যে ব্যাকটেস্টই চালান না কেন, তা ব্যর্থ কোম্পানিগুলোকে আগেই বাদ দিয়ে দিয়েছে।
পুনরায় প্রকাশিত ফান্ডামেন্টালস (Restated fundamentals) টাইমলাইনকে ভেঙে দেয়। 2019 সালের একটি কোয়ার্টারের জন্য API আজ যে রেভিনিউ ফিগার দেয়, তা সবসময় 2019 সালে প্রকাশিত ফিগার নয়। আজকের ফান্ডামেন্টালস এবং 2019 সালের প্রাইস ব্যবহার করে করা কোনো স্ক্রিন এমন তথ্য ব্যবহার করছে যা তখন অস্তিত্বেই ছিল না। প্রাইসের ক্ষেত্রে এটি সাধারণত নিরাপদ, কিন্তু ফান্ডামেন্টালসের ক্ষেত্রে নয়।
অ্যাডজাস্টেড প্রাইস আপনার অজান্তেই পরিবর্তিত হয়। auto_adjust=True-এর ক্ষেত্রে ক্লোজিং প্রাইসগুলোকে ডিভিডেন্ড এবং স্প্লিটের জন্য পেছনের দিকে অ্যাডজাস্ট করা হয়, তাই পরের মাসে একই কুয়েরি চালালে কিছুটা ভিন্ন হিস্ট্রি পাওয়া যায়। আপনি বাস্তবে যে সারিগুলো ব্যবহার করেছেন তা সংরক্ষণ করাই ফলাফলকে পুনরুৎপাদনযোগ্য (reproducible) করে তোলে, এবং লোকাল স্টোর থাকার এটিও একটি কারণ।
ব্যাকটেস্ট কমিশন এবং স্লিপেজকেও উপেক্ষা করে এবং ধরে নেয় যে আপনার অর্ডার প্রাইসকে প্রভাবিত করবে না। এগুলো এক্সিকিউশনের অংশ, যা এখানে আলোচনার বাইরে এবং VPS-এ ট্রেডিং বট চালানো অংশে বিস্তারিত আলোচনা করা হয়েছে।
ব্যর্থতার ধরন এবং যে বার্তাগুলো আপনি দেখবেন
error: externally-managed-environment যখন pip চলে। আপনি virtual environment-এর বাইরে আছেন। /opt/research/venv/bin/pip-কে পূর্ণ পাথ (full path) দিয়ে কল করুন।
Could not set lock on file, সাথে একটি PID। অন্য কোনো প্রসেস DuckDB ফাইলটিকে রাইট মোডে ওপেন করে রেখেছে, সাধারণত এটি এমন একটি interactive shell যা আপনি বন্ধ করতে ভুলে গেছেন। এটি বন্ধ করুন, অথবা read_only=True ব্যবহার করে দ্বিতীয় সংযোগটি ওপেন করুন।
Main process exited, code=killed, status=9/KILL যা systemctl status-এ দেখা যায়। মেমোরির অভাবে কার্নেল জবটিকে বন্ধ করে দিয়েছে। journalctl -k | grep -i oom দিয়ে এটি নিশ্চিত করুন, তারপর store.py-এ memory_limit-এর মান কমিয়ে দিন।
একটি সফল রান যা কিছুই লেখে না। systemctl status ফাইলটি active (exited) থেকে পড়ে কিন্তু টেবিলের আকার বাড়েনি। ফিডটি খালি ফ্রেম ফেরত দিয়েছে। এই ব্যর্থতাটি সবচেয়ে দীর্ঘ সময় ধরে লুকিয়ে থাকে, তাই প্রতিটি টিকার (ticker) খালি ফিরে আসলে জবটিকে non-zero exit code দিয়ে বন্ধ হওয়ার নির্দেশ দিন।
বাজারের ছুটির দিনে টাইমার চালু হওয়া। systemd এক্সচেঞ্জ ক্যালেন্ডার সম্পর্কে জানে না, তাই Mon-Fri-এর মধ্যে ছুটির দিনগুলোও অন্তর্ভুক্ত থাকে। রানটি সম্পন্ন হয়, ফিডে নতুন কিছু থাকে না, এবং জবটির উচিত একে ত্রুটি হিসেবে না দেখে স্বাভাবিক হিসেবে গণ্য করা।
API থেকে 429। আপনি রেট লিমিট অতিক্রম করেছেন। Anthropic SDK নিজে থেকেই backoff-সহ পুনরায় চেষ্টা করে, এবং Anthropic(max_retries=5) প্রচেষ্টার সংখ্যা বাড়িয়ে দেয়। যদি এটি প্রতিদিন ব্যর্থ হতে থাকে, তবে জবটি একবারে প্রয়োজনের চেয়ে বেশি ডেটা চাইছে।
এটি কী নয়
এটি একটি গবেষণা সহকারী। কোনো ফাইলিংয়ের সারাংশ তৈরি করার সময় মডেলটি সেই ফাইলিংয়ের একটি পাঠ তৈরি করে, এবং টেক্সটে থাকা কোনো সংখ্যার ক্ষেত্রে এটি আত্মবিশ্বাসের সাথে ভুল করতে পারে। এই কারণেই নোটে থাকা প্রতিটি সংখ্যা আপনার পাঠানো তথ্যের সাথে মিলিয়ে দেখা আবশ্যক। আউটপুটটিকে আপনার নিজের পড়ার জন্য একটি সংক্ষিপ্ত তালিকা হিসেবে বিবেচনা করুন। এখানে কোনো আর্থিক পরামর্শ দেওয়া হচ্ছে না এবং এর কোনোটিই কোনো সংকেত (signal) নয়।
ব্যাকটেস্ট (backtest) ধারণা বাতিল করার জন্য কার্যকর, কিন্তু তা নিশ্চিত করার ক্ষেত্রে দুর্বল। যে কৌশল আপনার নিজস্ব ডেটাতে ব্যর্থ হয়, তা নিশ্চিতভাবেই অকার্যকর। যে কৌশলটি পাস করে, তা কেবল আপনার ডেটাতে টিকে থাকতে পেরেছে; রাত 1টার সময় এটি যতটা বড় মনে হয়, বাস্তবে দাবিটি তার চেয়ে অনেক ছোট।
এক্সিকিউশন বা কার্যকর করার বিষয়টি ইচ্ছাকৃতভাবে এই আলোচনার বাইরে রাখা হয়েছে। অর্ডার এবং ব্রোকার ক্রেডেনশিয়ালের ঝুঁকি প্রোফাইল একটি রিড-অনলি রিসার্চ বক্স থেকে আলাদা। এগুলোকে একসাথে মেশালে ট্রেডিং কি (trading keys) এবং LLM প্রম্পট একই মেশিনে চলে আসে। আপনার নিজস্ব সার্ভারে চালানোর মতো অন্যান্য জিনিসের পাশাপাশি এই প্যাটার্নটি কোথায় অবস্থান করে তা দেখতে চাইলে, যেসব self-hosted AI agent চালানো সার্থক হলো এর বিস্তারিত রূপরেখা।
FAQ
আমার কি পেইড মার্কেট ডাটা ফিড প্রয়োজন?
প্রোটোটাইপের জন্য প্রয়োজন নেই। সিস্টেমের গঠন বোঝার জন্য একটি ফ্রি আনঅফিসিয়াল ফিড যথেষ্ট, তবে এটি যেকোনো সময় বন্ধ হয়ে যেতে পারে, কারণ এটি এমন একটি ওয়েবসাইটের ওপর নির্ভরশীল যা আপনাকে কোনো নিশ্চয়তা দেয় না। এটি সাধারণত এক্সেপশন না দেখিয়ে খালি ফ্রেম (empty frames) হিসেবে বন্ধ হয়, তাই আপনার জবকে অবশ্যই রো (row) কাউন্ট চেক করতে হবে। যখন ডাটা থেকে সিদ্ধান্ত নেওয়া শুরু করবেন, তখন একটি ডকুমেন্টড API এবং সাপোর্ট অ্যাড্রেসসহ পেইড ফিডে চলে যান। স্টোর ব্যবহারের ফলে এই পরিবর্তনটি সহজ হয়: শুধুমাত্র fetch ফাংশনটি পরিবর্তিত হয়, আর শিডিউল, স্কিমা এবং স্ক্রিন আগের মতোই থাকে।
আমার কি প্রাইস ডাটা SQLite নাকি DuckDB-তে রাখা উচিত?
DuckDB হলো কলামনার (columnar) এবং এটি অনেকগুলো রো স্ক্যান করে অ্যাগ্রিগেট বের করার জন্য তৈরি, যা দশ বছরের বারের মুভিং এভারেজ বের করার জন্য উপযুক্ত। SQLite হলো রো-অরিয়েন্টেড এবং এটি একই সময়ে একাধিক প্রসেস থেকে অনেক ছোট ছোট রিড ও রাইট করার জন্য ভালো। একটি শিডিউলড জবের জন্য যা কয়েকশ রো অ্যাপেন্ড করে এবং তারপর মিলিয়ন রো স্ক্যান করে, সেখানে DuckDB বেশি কার্যকর। যদি একাধিক প্রসেসকে একই সময়ে রাইট করতে হয়, তবে SQLite-এর WAL মোড রিডারদের কাজ করতে দেয় যখন একজন রাইটার কমিট করে, এবং একটি বিজি টাইমআউট (busy timeout) অন্য রাইটারদের ফেইল না করে অপেক্ষা করতে বাধ্য করে।
প্রতি মাসে LLM কলের খরচ কত?
প্রতিটি রেসপন্স থেকে usage.input_tokens এবং usage.output_tokens একটি টেবিলে লগ করুন, তারপর আপনার সাপ্তাহিক মোট সংখ্যাকে আপনার মডেলের প্রতি মিলিয়ন টোকেনের দাম দিয়ে গুণ করুন (যেদিন চেক করছেন সেই দিনের দাম অনুযায়ী)। এটিই একমাত্র হিসাব যা পরবর্তী কোয়ার্টারেও সঠিক থাকবে। একটি ছোট স্ক্রিনের ওপর প্রতিদিন একবার রান করলে কলের সংখ্যা কম হয়, এবং নোটের দৈর্ঘ্য আপনার নিয়ন্ত্রণে থাকে: সামারি 200 শব্দের মধ্যে সীমাবদ্ধ রাখলে কম রো পাঠানোর চেয়ে বেশি সাশ্রয় হয়, কারণ প্রতিটি Claude মডেলে আউটপুট টোকেনের দাম ইনপুট টোকেনের চেয়ে বেশি।
আমার জব সফল হয়েছে কিন্তু কোনো নতুন রো কেন লেখেনি?
এর দুটি সাধারণ কারণ আছে। মার্কেট বন্ধ ছিল, কারণ একটি systemd Mon-Fri শিডিউলে এক্সচেঞ্জ হলিডে অন্তর্ভুক্ত থাকে। অথবা ফিড প্রতিটি টিকারের জন্য একটি খালি ফ্রেম রিটার্ন করেছে, যা ক্লায়েন্ট লাইব্রেরিগুলো প্রায়ই এক্সেপশন না দেখিয়ে শুধু একটি প্রিন্টেড ওয়ার্নিং হিসেবে দেখায়, তাই প্রসেসটি 0 এক্সিট কোড দেয় এবং systemd গ্রিন রান দেখায়। SELECT max(day) FROM prices-এর সাথে শেষ ট্রেডিং দিনের তুলনা করে পার্থক্যটি বুঝুন, এবং যখন প্রতিটি টিকার খালি আসে তখন জবটিকে নন-জিরো এক্সিট কোড দিতে বাধ্য করুন।
এজেন্ট কি সিদ্ধান্ত নিতে পারে কী কিনতে হবে?
না, এবং এটি করার চেষ্টা করাই হলো সেই জায়গা যেখানে এই প্রজেক্টগুলো ভুল পথে যায়। মডেলটির মার্কেটে কোনো অ্যাক্সেস নেই, আপনার পজিশন বা ট্যাক্স পরিস্থিতি সম্পর্কে কোনো ধারণা নেই, এবং নিজের সংখ্যাগুলো যাচাই করার কোনো উপায় নেই। এটি যা ভালো করতে পারে তা হলো প্রচুর টেক্সট পড়ে আপনাকে জানানো যে আজ কোন বিষয়গুলো আপনার মনোযোগ পাওয়ার যোগ্য। এটি যা লেখে তার কোনোটিই আর্থিক পরামর্শ নয়, এবং সিদ্ধান্ত ও তার দায়ভার আপনার নিজের।