VPS-এ নিজের স্টক রিসার্চ এজেন্ট তৈরির সম্পূর্ণ গাইড
Python, DuckDB এবং systemd ব্যবহার করে কীভাবে নিজস্ব স্টক রিসার্চ এজেন্ট তৈরি করবেন তা জানুন। মার্কেট ডেটা সংগ্রহ থেকে LLM স্ক্রিনিং পর্যন্ত পুরো প্রক্রিয়ার ধাপগুলো এখানে দেখুন।
সেলফ-হোস্টেড স্টক রিসার্চ এজেন্ট কী
সেলফ-হোস্টেড স্টক রিসার্চ এজেন্ট হলো আপনার নিজস্ব সার্ভারে চলা একটি ছোট প্রোগ্রাম, যা নির্দিষ্ট সময় পরপর মার্কেট ডেটা সংগ্রহ করে, তা লোকাল ডেটাবেসে জমা রাখে, ডেটা ফিল্টার করে এবং কী পরিবর্তন হয়েছে তা লিখে দেওয়ার জন্য একটি লার্জ ল্যাঙ্গুয়েজ মডেলকে (LLM) অনুরোধ করে। এটি মূলত ডেটা পড়ে এবং ফিল্টার করে। এটি নিজে কোনো ট্রেড করে না এবং এই গাইডের কোনো কিছুই আর্থিক পরামর্শ নয়।
দুজন ব্যক্তি যদি এটি স্ক্র্যাচ থেকে তৈরি করেন, তবে তারা ভিন্ন ভিন্ন লাইব্রেরি বেছে নিলেও শেষ পর্যন্ত একই চারটি অংশ পাবেন: একটি ফিড যা প্রাইস ও ফান্ডামেন্টাল ডেটা সরবরাহ করে, একটি লোকাল স্টোর যা আপনার সংগ্রহ করা প্রতিটি ডেটা রো (row) সংরক্ষণ করে, একটি জব যা টাইমারের মাধ্যমে স্টোরটি রিফ্রেশ করে এবং একটি LLM লেয়ার যা অবশিষ্ট ডেটা থেকে বাক্য তৈরি করে। এই গাইডটি Python, DuckDB, systemd টাইমার এবং Claude API (অ্যাপ্লিকেশন প্রোগ্রামিং ইন্টারফেস) ব্যবহার করে এই কাঠামোটি তৈরি করবে। এক্সিকিউশন বা ট্রেড করা একটি আলাদা কাজ যার ব্যর্থতার ধরনও আলাদা, এবং এটি ট্রেডিং বটের জন্য সেটআপ করা একটি VPS-এ রাখা উচিত।
চারটি অংশ এবং প্রতিটি অংশের কাজ
The feed হলো একমাত্র অংশ যা বাইরের জগতের সাথে যোগাযোগ করে। এটি জানে কীভাবে একটি ticker এবং তারিখের পরিসীমা (date range) চাইতে হয় এবং কীভাবে ডেটা সারিগুলো ফেরত দিতে হয়। ডাউনস্ট্রিমের সবকিছুই feed-এর পরিবর্তে আপনার ডেটাবেস থেকে তথ্য পড়ে, তাই feed বন্ধ হয়ে গেলেও আপনার স্ক্রিন ভেঙে পড়ার বদলে কেবল একদিনের নতুন ডেটা মিস হবে।
The store হলো পুরো প্রক্রিয়ার মূল উদ্দেশ্য। প্রতিদিনের ক্লোজিং ডেটা যা আপনি রেকর্ড করেননি, তা সাধারণত পরে আবার সংগ্রহ করা যায়। কিন্তু intraday quote, সংশোধনের আগের কোনো অনুমান (estimate), অথবা পুনরায় হিসাব করার আগের কোনো মৌলিক পরিসংখ্যান (fundamentals figure) পরে আর পাওয়া যায় না। ডেটা যেদিন যা বলেছিল, তার একটি রেকর্ড তৈরি করাই হলো store-এর কাজ।
The scheduler নির্ধারণ করে কখন রিফ্রেশ ঘটবে। সার্ভারে এটি একটি systemd timer হিসেবে কাজ করে, আর এই কারণেই কোডের চেয়ে VPS এখানে বেশি গুরুত্বপূর্ণ।
The LLM layer আপনার SQL থেকে তৈরি হওয়া ছোট টেক্সট ব্লকটি পড়ে এবং তার একটি সারাংশ লেখে। এটি কখনোই ডেটাবেসের সাথে সংযুক্ত হয় না এবং কখনোই কুয়েরি তৈরি করে না। যদি মডেলটি SQL লেখে, তবে একটি ভুল টোকেন একটি সাবলীল বাক্যের ভেতরে ভুল সংখ্যা তৈরি করতে পারে, যা যাচাই করার কোনো উপায় থাকে না। কিন্তু যদি SQL সংখ্যাগুলো তৈরি করে, তবে মডেলটি কেবল গদ্যে ভুল করতে পারে, আর আপনি সেই গদ্যটিকে আপনার পাঠানো ডেটা সারির সাথে মিলিয়ে যাচাই করতে পারবেন।
ল্যাপটপের পরিবর্তে কেন VPS-এ এটি চালাবেন
এর কারণ হলো শিডিউলার। নিউ ইয়র্ক সময় 16:00-তে মার্কিন বাজার বন্ধ হওয়ার সময় বার্লিনে সময় হয় 22:00 এবং জাকার্তায় পরের দিন ভোর 04:00। এই উভয় সময়েই একটি ল্যাপটপ স্লিপ মোডে থাকে। একটি রান মিস করা মানে কেবল একটি নোটিফিকেশন দেরি হওয়া নয়: ডেইলি বারগুলো সাধারণত পরে পুনরায় সংগ্রহ করা যায়, কিন্তু যা একবার সংশোধিত হয়ে যায় তা আর ফিরে পাওয়া সম্ভব নয়, ফলে আপনার রেকর্ডে স্থায়ী গ্যাপ থেকে যায়। একই যুক্তি সেই সব এজেন্টের ক্ষেত্রেও প্রযোজ্য যাদের শিডিউল এবং সংরক্ষিত স্টেটকে রিবুট-এর পরেও টিকে থাকতে হয়, আর এটিই হলো আপনার নিজস্ব VPS-এ KiroCrew-কে সবসময় সচল এজেন্ট হিসেবে চালানোর মূল ভিত্তি।
দ্বিতীয় কারণটি ছোট হলেও বাস্তব। সার্ভারে একটি মাত্র 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 টি সারিতে পৌঁছায়। প্রতিটি সারিতে একটি তারিখ এবং কয়েকটি ডাবল ভ্যালু থাকে, এবং DuckDB কলামগুলোকে কম্প্রেস করে সংরক্ষণ করে, তাই এটি গিগাবাইটের পরিবর্তে মাত্র কয়েক দশ মেগাবাইট জায়গা নেয়। আমার দেওয়া এই হিসাবসহ কোনো অনুমানের ওপরই পুরোপুরি নির্ভর করবেন না। প্রথম ব্যাকফিল সম্পন্ন করার পর du -h /opt/research/data/market.duckdb চালান এবং আপনার নিজস্ব ডেটা ব্যবহার করুন।
র্যামের ক্ষেত্রে ছোট VPS-এ সমস্যা হতে পারে। DuckDB ডিফল্টভাবে একটি সিঙ্গেল কুয়েরির জন্য মেশিনের মেমোরির একটি বড় অংশ এবং সবকটি কোর ব্যবহার করে, যা একটি অ্যানালিটিক্স সার্ভারের জন্য ঠিক থাকলেও 2 GB র্যামের মেশিনে অন্যান্য কাজের সাথে চললে সমস্যা তৈরি করে। পুরো প্রাইস টেবিলের ওপর একটি অ্যাগ্রিগেশন চালালে কার্নেল প্রসেসটিকে কিল করে দিতে পারে, এবং systemd তখন Main process exited, code=killed, status=9/KILL রিপোর্ট করে, যেখানে journalctl -k-এ আউট অফ মেমোরি (OOM) কিল হওয়ার বিষয়টি দেখা যায়। memory_limit এবং threads স্পষ্টভাবে সেট করে দিলে কুয়েরিটি বন্ধ না হয়ে বরং ধীরগতিতে সম্পন্ন হবে।
টোকেনের হিসাব অনুমান না করে মেপে দেখা উচিত। প্রতিটি Messages API রেসপন্সে একটি usage অবজেক্ট থাকে, যার মধ্যে input_tokens এবং output_tokens থাকে। প্রতিটি কলের সময় এই দুটি মান একটি টেবিলে লিখে রাখুন। এক সপ্তাহ পর আপনি আপনার প্রকৃত ভলিউম জানতে পারবেন, যা আপনি যে দিনে চেক করছেন সেই দিনের মডেলের তালিকা অনুযায়ী গুণ করে নিতে পারেন। পরিকল্পনা করার জন্য দুটি বিষয় বেশ স্থিতিশীল। প্রতিটি Claude মডেলে আউটপুট টোকেনের দাম ইনপুট টোকেনের চেয়ে বেশি, তাই নোটের দৈর্ঘ্য 200 শব্দের মধ্যে সীমাবদ্ধ রাখলে তা আপনার পাঠানো ডেটা কমানোর চেয়ে বিলের ওপর বেশি প্রভাব ফেলে। এছাড়া প্রম্পট ক্যাশিং প্রতিদিন একবার চলা কাজের ক্ষেত্রে কোনো সাহায্য করে না, কারণ ক্যাশের স্থায়িত্ব মাত্র কয়েক মিনিট: পরবর্তী রান শুরু হওয়ার আগেই ক্যাশ করা ব্লকটি এক্সপায়ার হয়ে যায় এবং আপনাকে পুনরায় পুরো ইনপুট মূল্য পরিশোধ করতে হয়। ক্যাশিং তখনই লাভজনক হয় যখন একটি রান একই বড় টেক্সট ব্লকের ওপর অনেকগুলো কল করে।
উপাদানগুলো ইনস্টল করুন
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-তে একটি নির্দিষ্ট সময়ে কেবল একটি প্রসেস ফাইলকে রাইট মোডে ওপেন রাখতে পারে। দ্বিতীয় কোনো রাইটার চেষ্টা করলে তা সাথে সাথে 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 গ্রহণ করতে পারে এবং এটি সেগুলোকে ক্রমানুসারে চালায়। কোনো একটির এক্সিট কোড শূন্য না হলে এটি পরবর্তীগুলো চালানো বন্ধ করে দেয়। আপনি ঠিক এই আচরণটিই চাইবেন: রিফ্রেশ ব্যর্থ হলে পুরনো ডেটার ওপর ভিত্তি করে স্ক্রিন আপডেট হওয়া উচিত নয়। কার্নেল আপডেটের জন্য 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 বিদ্যমান যেকোনো সারির গড় হিসাব করে, তাই একটি টিকারের তৃতীয় সারিতে তিন দিনের গড় মান পাওয়া যায় এবং একে তখনও 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 দেখে না, ডাটাবেসের পাথ দেখে না এবং কোনো কুয়েরি রান করে না। এটি কেবল সারিগুলো গ্রহণ করে এবং গদ্য আকারে উত্তর দেয়। এই সীমানাটিই আউটপুটকে যাচাইযোগ্য করে তোলে, কারণ নোটে থাকা প্রতিটি সংখ্যা আপনার পাঠানো ব্লকেও থাকা উচিত এবং আপনি সেগুলো লাইন বাই লাইন মিলিয়ে দেখতে পারেন। প্রম্পটিংয়ের এই দিকটি সম্পর্কে আরও জানতে, আর্থিক বিশ্লেষণের জন্য 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 কল করুন।
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) থেকে পড়ে এবং টেবিলটির আকার বাড়েনি। ফিডটি খালি ফ্রেম ফেরত দিয়েছে। এই ব্যর্থতাটি সবচেয়ে দীর্ঘ সময় ধরে লুকিয়ে থাকে, তাই প্রতিটি টিকারে খালি ডেটা আসলে জবটিকে non-zero exit code দিয়ে বন্ধ হওয়ার ব্যবস্থা করুন।
মার্কেট ছুটির দিনে টাইমার চালু হওয়া। systemd এক্সচেঞ্জ ক্যালেন্ডার সম্পর্কে জানে না, তাই Mon-Fri ছুটির দিনগুলোকেও অন্তর্ভুক্ত করে। রানটি সম্পন্ন হয়, ফিডে নতুন কিছু থাকে না, এবং জবটির উচিত একে ত্রুটি হিসেবে না দেখে স্বাভাবিক হিসেবে গণ্য করা।
API থেকে 429। আপনি rate limit অতিক্রম করেছেন। Anthropic SDK নিজে থেকেই backoff-এর মাধ্যমে পুনরায় চেষ্টা করে এবং Anthropic(max_retries=5) প্রচেষ্টার সংখ্যা বাড়িয়ে দেয়। যদি এটি প্রতিদিন ব্যর্থ হতে থাকে, তবে জবটি একবারে প্রয়োজনের চেয়ে বেশি ডেটা চাইছে।
এটি যা নয়
এটি একটি গবেষণা সহকারী। কোনো ফাইলিংয়ের সারসংক্ষেপ তৈরি করার সময় মডেলটি সেই ফাইলিংয়ের একটি পাঠ তৈরি করে, যা টেক্সটে থাকা কোনো সংখ্যার ক্ষেত্রে ভুল হতে পারে। এই কারণেই নোটে থাকা প্রতিটি সংখ্যা আপনার পাঠানো তথ্যের সাথে মিলিয়ে দেখা আবশ্যক। আউটপুটটিকে কেবল পড়ার জন্য একটি সংক্ষিপ্ত তালিকা হিসেবে বিবেচনা করুন। এখানে কোনো আর্থিক পরামর্শ দেওয়া হয়নি এবং এর কোনোটিই কোনো সংকেত (signal) নয়।
ব্যাকটেস্ট (backtest) ধারণা বাতিল করার জন্য কার্যকর, কিন্তু তা নিশ্চিত করার ক্ষেত্রে দুর্বল। যে কৌশল আপনার নিজস্ব ডেটাতে ব্যর্থ হয়, তা নিশ্চিতভাবেই অকার্যকর। আর যে কৌশল উত্তীর্ণ হয়, তা কেবল আপনার ডেটাতে টিকে থাকতে পেরেছে—যা রাত 1টার অনুভূতির তুলনায় অনেক ছোট একটি দাবি।
এক্সিকিউশন বা কার্যকরীকরণ ইচ্ছাকৃতভাবে এই আলোচনার বাইরে রাখা হয়েছে। অর্ডার এবং ব্রোকার ক্রেডেনশিয়ালগুলো রিড-অনলি রিসার্চ বক্সের চেয়ে ভিন্ন ধরনের ঝুঁকির সম্মুখীন হয়। এগুলোকে একত্রিত করলে LLM প্রম্পটের সাথে একই মেশিনে ট্রেডিং কি (trading keys) রাখার ঝুঁকি তৈরি হয়। আপনার নিজস্ব সার্ভারে চালানোর মতো অন্যান্য বিষয়ের পাশাপাশি এই প্যাটার্নটি কোথায় অবস্থান করে তা জানতে চাইলে, যেসব self-hosted AI agents চালানো সার্থক তা বিস্তারিত দেখুন।
FAQ
আমার কি পেইড মার্কেট ডাটা ফিড প্রয়োজন?
প্রোটোটাইপের জন্য প্রয়োজন নেই। সিস্টেমের গঠন বোঝার জন্য একটি ফ্রি আনঅফিসিয়াল ফিড যথেষ্ট, তবে এটি যেকোনো সময় বন্ধ হয়ে যেতে পারে, কারণ এটি এমন একটি ওয়েবসাইটের ওপর নির্ভরশীল যারা আপনাকে কোনো নিশ্চয়তা দেয় না। এটি সাধারণত এক্সেপশন না দিয়ে খালি ফ্রেম হিসেবে বন্ধ হয়, তাই আপনার জবকে অবশ্যই রো (row) কাউন্ট চেক করতে হবে। যখন ডাটা কোনো সিদ্ধান্তের ভিত্তি হিসেবে ব্যবহৃত হতে শুরু করবে, তখন একটি ডকুমেন্টড API এবং সাপোর্ট অ্যাড্রেসসহ পেইড ফিডে চলে যান। স্টোর ব্যবহারের ফলে এই পরিবর্তনটি সহজ হয়: শুধুমাত্র ফেচ ফাংশনটি পরিবর্তিত হয়, আর শিডিউল, স্কিমা এবং স্ক্রিন আগের মতোই থাকে।
আমার কি প্রাইস ডাটা SQLite নাকি DuckDB-তে রাখা উচিত?
DuckDB হলো কলামনার এবং এটি অনেকগুলো রো স্ক্যান করে অ্যাগ্রিগেট বের করার জন্য তৈরি, যা দশ বছরের বার (bar) থেকে মুভিং এভারেজ বের করার জন্য আদর্শ। SQLite হলো রো-ভিত্তিক এবং এটি একই সাথে একাধিক প্রসেস থেকে অনেক ছোট ছোট রিড ও রাইট অপারেশনের জন্য ভালো। একটি সিঙ্গেল শিডিউলড জবের জন্য যা কয়েকশ রো অ্যাপেন্ড করে এবং তারপর মিলিয়ন রো স্ক্যান করে, DuckDB বেশি উপযুক্ত। যদি একাধিক প্রসেসকে একই সময়ে রাইট করতে হয়, তবে SQLite-এর WAL মোড ব্যবহার করুন; এটি একজন রাইটার কমিট করার সময় রিডারদের কাজ চালিয়ে যেতে দেয় এবং একটি বিজি টাইমআউট অন্য রাইটারদের ব্যর্থ হওয়ার পরিবর্তে অপেক্ষা করতে বাধ্য করে।
প্রতি মাসে LLM কলের খরচ কত?
প্রতিটি রেসপন্স থেকে usage.input_tokens এবং usage.output_tokens লগ করে একটি টেবিলে রাখুন, তারপর আপনার সাপ্তাহিক মোট সংখ্যাকে আপনার মডেলের প্রতি মিলিয়ন টোকেনের দাম দিয়ে গুণ করুন (যেদিন চেক করছেন সেই দিনের দাম অনুযায়ী)। এটিই একমাত্র হিসাব যা পরবর্তী কোয়ার্টারেও সঠিক থাকবে। একটি ছোট স্ক্রিনের ওপর প্রতিদিন একবার রান করলে কলের সংখ্যা কম থাকে, এবং নোটের দৈর্ঘ্য আপনার নিয়ন্ত্রণে থাকে: সামারি 200 শব্দের মধ্যে সীমাবদ্ধ রাখলে কম রো পাঠানোর চেয়ে বেশি সাশ্রয় হয়, কারণ প্রতিটি Claude মডেলে আউটপুট টোকেনের দাম ইনপুট টোকেনের চেয়ে বেশি।
আমার জব সফল হয়েছে কিন্তু কোনো নতুন রো লেখেনি কেন?
এর দুটি সাধারণ কারণ আছে। মার্কেট বন্ধ ছিল, কারণ systemd Mon-Fri শিডিউলে এক্সচেঞ্জ হলিডে অন্তর্ভুক্ত থাকে। অথবা ফিডটি প্রতিটি টিকারের জন্য একটি খালি ফ্রেম রিটার্ন করেছে, যা ক্লায়েন্ট লাইব্রেরিগুলো সাধারণত এক্সেপশনের পরিবর্তে একটি প্রিন্টেড ওয়ার্নিং হিসেবে দেখায়, তাই প্রসেসটি 0 এক্সিট কোড দেয় এবং systemd গ্রিন রান দেখায়। শেষ প্রকৃত ট্রেডিং দিনের সাথে SELECT max(day) FROM prices তুলনা করে এর পার্থক্য বুঝুন এবং প্রতিটি টিকার খালি ফিরে আসলে জবটিকে নন-জিরো এক্সিট কোড দিতে বলুন।
এজেন্ট কি সিদ্ধান্ত নিতে পারে যে কী কিনতে হবে?
না, এবং এটি করার চেষ্টা করাই হলো এই প্রজেক্টগুলোর ভুল দিক। মডেলটির মার্কেটে কোনো অ্যাক্সেস নেই, আপনার পজিশন বা ট্যাক্স পরিস্থিতি সম্পর্কে কোনো ধারণা নেই এবং নিজের হিসাব যাচাই করার কোনো উপায় নেই। এটি যা ভালো পারে তা হলো প্রচুর টেক্সট পড়ে আপনাকে জানানো যে আজ কোন বিষয়গুলো আপনার মনোযোগ পাওয়ার যোগ্য। এটি যা কিছু লেখে তা কোনো আর্থিক পরামর্শ নয়, এবং সিদ্ধান্ত ও এর দায়ভার আপনারই থাকে।